JavaScript中的this

一、理解常规函数中的this

​ this指的是运行当前这段函数代码的对象,或者说指的是函数运行时所在的环境.

1
2
3
4
5
6
7
8
9
10
var obj = {
foo: function () { console.log(this.bar) },
bar: 1
};

var foo = obj.foo;
var bar = 2;

obj.foo() // 1
foo() // 2

​ 对于obj.foo()来说, foo运行在obj环境, 调用者是obj, 所以this指向obj; 对于foo()来说, foo运行在全局环境, 调用者是window, 所以this指向代表全局环境的window. 不同环境不同作用域读取到的变量往往不同, 所以二者的运行结果不一样.

​ 这里我们要谈到一个叫执行上下文的东西. JavaScript中执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。对于每个执行上下文,都有三个重要属性:变量对象(Variable object,VO), 作用域链(Scope chain)和this.

​ 一个函数定义之后可以在很多不同的地方被调用, 函数内部的this就在函数运行时指明了执行上下文, 也就是表明了是哪个家伙 在哪里 调戏(调用)了我. 这时候函数内部就能知道作用域是什么,有哪些变量是自己可以读取到的. 简单地说,要知道this指向什么, 我们只需要搞清楚函数是在什么时候什么地方被谁如何被调用的即可, 并不需要关注函数在哪里定义或声明.

this是使用call()方法调用函数时传递的第一个参数(call方法一共2个参数, 第一个由于绑定this, 第二个是要传入的参数组成的数组) 他可以在函数调用时修改, 在函数没有调用的时候, this的值是无法确定的.

​ 接下来具体看看函数的调用, 以及不同情况下this指向哪里, 我们会借助call()方法来帮助理解.

(一)存粹的函数调用

​ 第一种方法最常见, 例子如下:

1
2
3
4
5
function test(name) {
trueconsole.log(name);
console.log(this);
}
test("Jerry");

​ 这种方法我们平常使用的最多,但是这种函数调用方法只是一种简写,它完整的写法是下面这样的:

1
2
3
4
5
function test(name) {
console.log(name);
console.log(this);
}
test.call(undefined, "Tom");

​ 注意上面的call方法, call方法接收的第一个参数就是this, 这里我们传了一个undefined. 那么函数执行之后console.log()出来的会是undefined吗? 不是! (传入null或者undefined默认指window对象)

​ 所以在这里调用test函数的是window, this就是指向的window全局对象. 执行上下文也就是全局执行上下文了.

(二)作为对象方法的调用

​ 函数还可以作为某个对象的方法调用,这时候一般this就指这个对象. (用call()方法可以例外哦)

例子:

1
2
3
4
5
6
7
8
const obj = {
name: "Jerry",
greet: function() {
console.log(this.name);
true}
}
obj.greet(); // 第一种调用方式
obj.greet.call(obj) //第二种调用方式

​ 以上例子中的第一种调用方式实际上只是第二种方式的语法糖, 第二种才是完整的调用方法, 第二种方法厉害的地方就在于它可以手动指定this.

手动指定this的例子:

1
2
3
4
5
6
7
const obj = {
name: "Jerry",
greet: function() {
truetrueconsole.log(this.name);
}
}
obj.greet.call({name: "Trump"}); // 打出来是 Trump

​ 上面例子call方法调用函数时传入的是一个对象{"name": "Trump"}, 这个对象就是手动指定的this, 因此greet()函数中console.log(this.name)打印出来的就是Trump了. 这个例子也验证了文章第一段所说的:

我们只需要搞清楚函数是在什么时候什么地方如何被调用的即可, 并不需要关注函数在哪里定义或声明.

(三)构造函数中的this

​ 构造函数中的this有一点特殊, 每个构造函数在new之后都会返回一个实例对象, this就指这个实例对象.

例子:

1
2
3
4
5
6
function Test() {
truethis.name = "Tom";
}
let p = new Test();
console.log(typeof p); // object
console.log(p.name); // Tom

(四)window.setTimeout() 和window.setInterval()中函数的调用

​ 他们两个函数中的this有些特殊,里面的this默认是window对象.

简单总结一下:函数完整的调用方法是使用call方法,包括test.call(context, name)obj.greet.call(context,name),这里的context就是函数调用时的上下文,也就是this,只不过这个this是可以通过call方法来修改的;构造函数稍微特殊一点,它的this直接指向new之后返回的对象;window.setTimeout()window.setInterval()默认的是this是window对象。


二、理解箭头函数中的this

箭头函数表达式的语法比函数表达式更简洁,并且没有自己的thisargumentssupernew.target。这些函数表达式更适用于那些本来需要匿名函数的地方,并且它们不能用作构造函数。

(一)箭头函数默认不绑定this

​ 在箭头函数出现之前,每个新定义的函数都有它自己的 this值(在构造函数的情况下是一个新对象,在严格模式的函数调用中为 undefined,如果该函数被作为“对象方法”调用则为基础对象等)。this被证明是令人厌烦的面向对象风格的编程。箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this。

不使用箭头函数的例子:

1
2
3
4
5
6
const obj = {
a: function() {
console.log(this);
}
}
obj.a(); // 打印出的是obj对象

使用箭头函数的例子:

1
2
3
4
const obj ={
a: () => console.log(this)
}
obj.a(); // 打印出来的是window

​ 上面这个例子中, 从作用域链上层(这里的上层也就是window)继承了this

再来个例子:

1
2
3
4
5
6
7
8
function test() {
const myObj = {
name: "hejian",
greeting: () => console.log(this)
}
return myObj.greeting();
}
test(); // 打印出来的是window

​ 以上这个例子, 如果箭头函数像普通函数一样默认绑定this的话, 它的this应该指向myObj. 但是由于 箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this, 所以这里就向作用域链的上级test()这一层作用域中查询,test()是个普通函数默认有this, 那么查询到这里就结束了. 那么这个this指向哪里呢? 答案就是指向window! 为了方便理解例子的最后一行可以用这种方式写: test.call(undefined)

(二)不能用call方法修改箭头函数的this

​ 由于 箭头函数没有自己的this指针,通过 call() apply() 方法调用一个函数时,只能传递参数(不能绑定this),他们的第一个参数会被忽略。(这种现象对于bind方法同样成立)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let adder = {
base : 1,
add : function(a) {
var f = v => v + this.base;
return f(a);
},
addThruCall: function(a) {
var f = v => v + this.base;
var b = {
base : 2
};
return f.call(b, a);
}
};

console.log(adder.add(1)); // 输出 2
console.log(adder.addThruCall(1)); // 仍然输出 2(而不是3 ——译者注)

如上所示, 企图用call方法手动指定箭头函数的this是行不通的.


参考:

  1. 掘金 - JS中的箭头函数与this
  2. JavaScript - all about this keyword
  3. MDN - 使用对象
  4. MDN - 使用函数