在上一篇文章【JS:深入理解JavaScript-执行上下文】中介绍了执行上下文是如何工作的。在这篇文章里会介绍执行上下文中的ThisBinding,也就是JavaScript中的this。
有四种可执行代码可以创建执行上下文,分别是global code
function code
module
和eval code
。接下来分别介绍这global code
function code
可执行代码中的this(ThisBinding)到底指的是什么。
global code的this
在JS引擎运行global code之前,会创建一个全局执行上下文压入执行栈的栈底,这个全局执行上文的ThisBinding绑定的是全局对象,在浏览器里指的就是window。
function code的this
在文章【JS:深入理解JavaScript-词法环境】提到过JavaScript是静态作用域,词法环境是由代码结构决定的,开发把代码写成什么样,词法环境就是怎么样,跟方法在哪里调用没有关系。但是对于函数的this刚好反过来,跟代码在哪里定义没有关系,而跟代码在哪里调用有关系。一般我们调用函数有以下四种方式:
- 普通函数调用,比如foo()或者(functon(){})()
- 作为对象方法调用,比如obj.foo()
- 构造函数调用,比如new foo()
- 使用call、apply、bind等方法调用
在介绍着几种函数调用之前,我们先来看下ECMAScript对this的规范:
ECMAScript规范: 严格模式时,函数内的this绑定严格指向传入的thisArgument。非严格模式时,若传入的thisArgument不为undefined或null时,函数内的this绑定指向传入的thisArgument;为undefined或null时,函数内的this绑定指向全局的this。
普通函数调用
普通函数的调用,包括函数调用foo()和立即调用函数表达式(functon(){})(),传到函数里的thisArgument是undefined。根据ECMAScript规范,如果在非严格模式下,普通函数里的this就是全局对象,而在严格模式下就为undefined。
比如有以下代码:
var a = 2;
function foo(val) {
console.log(this.a); //2
console.log(val); //3
}
foo(3);
/**
foo(3) 相当于是:foo().call(undefined,3) 的简化版本。
为了代码方便,所以有直接普通函数调用,thisArgument 默认是undefined,不用每次都敲这个undefined。
**/
如果把foo方法改为严格模式:
var a = 2;
function foo() {
"use strict"
console.log(this.a)
}
foo();
执行上面这段代码,会报错:
VM162:4 Uncaught TypeError: Cannot read property 'a' of undefined
at foo (<anonymous>:4:22)
对象方法调用
作为对象方法调用,传到函数里的thisArgument是该对象。比如有如下代码:
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); //2
需要注意的是,只有离函数最近的这个对象,才是该函数的this,比如有代码:
function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); //42
还有一种比较看起来像对象方法调用,实际上是普通函数调用:
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
}
var bar = obj.foo;
var a = "global variable";
bar(); // global variable
构造函数调用
new functionname()构造函数调用,this指的是构造出来的新对象。
function foo(a) {
this.a = a;
}
var bar = new foo( 2 );
console.log( bar.a ); // 2
使用call、apply、bind等方法调用
call、apply、bind调用,可以显示传递对象给函数的thisArg,默认这几个函数的第一个形参是thisArg:
Function.prototype.apply( thisArg, argArray )
Function.prototype.call( thisArg , arg1, [ arg2, ... ] )
Function.prototype.bind( thisArg , [ arg1, [ arg2, ... ] ] )
需要注意的是当thisArg为null或者undefined,在非严格模式下,this是全局对象。
var obj = {
a: 1
};
function print() {
console.log(this);
}
print.call(null);//window
print.call(undefined);//window
print.call(obj);//obj
箭头函数的this
箭头函数在调用的时候不会绑定this,它会去词法环境链上寻找this(parent scope),所以箭头函数的this取决于它定义的位置(包裹箭头函数的第一个普通函数的this),也就是箭头函数会跟包着它的作用域共享一个词法作用域。
window.a = 10
const foo = () => {
console.log(this.a)
}
foo.call({a: 20}) // 10
let obj = {
a: 20,
foo: foo
}
obj.foo() //10
function bar() {
foo()
}
bar.call({a: 20}) //10
回调函数的this
window.a = 10
let obj = {
a: 20,
foo: function () {
console.log(this.a)
}
}
setTimeout(obj.foo, 0) //10
上面代码运行结果是10,作为回调函数的时候,传递的是函数体,并不是函数名。在执行栈里,obj.foo已经执行完成被弹出执行栈,此时执行栈里只有全局执行上下文,setTimeout回调函数体执行的时候this为全局对象。
要想避免这种情况,有两种方法,第一种方法是使用bind返回的指定好this绑定的函数作为回调函数传入:
- 使用bind指定this
setTimeout(obj.foo.bind({a: 20}), 0) // 20
- 使用箭头函数
window.a = 10 function foo() { return () => { console.log(this.a) } } const arrowFn = foo.call({a: 20}) arrowFn() // 20 setTimeout(arrowFn, 0) //20
案例分析
var deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
createCardPicker: function () {
return function () {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
return { suit: this.suits[pickedSuit], card: pickedCard % 13 };
};
},
};
var cardPicker = deck.createCardPicker();
var pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);
在这个例子中,在 createCardPicker
里面本身会返回一个匿名函数,这个匿名函数在倒数第二行代码的时候才会执行,这个时候deck
并不是this
的指向,在非严格模式的情况下,这里的this
指的是全局对象window
(如果是严格模式,那么this
指向的是undefined
), 所以在指向代码的时候会报错而不是弹出相关的信息。
为了修复这里的error,我们可以把 return 的方法改为箭头函数:
var deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
createCardPicker: function () {
// NOTE: 箭头函数的this,指向包裹它最近普通函数的this
return () => {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
return { suit: this.suits[pickedSuit], card: pickedCard % 13 };
};
},
};
var cardPicker = deck.createCardPicker();
var pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);
或者,我们可以用 bind 来绑定 this:
var deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
createCardPicker: function () {
return function () {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
return { suit: this.suits[pickedSuit], card: pickedCard % 13 };
}.bind(this);
},
};
var cardPicker = deck.createCardPicker();
var pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);
总结
-
箭头函数中没有this绑定,this的值取决于其创建时所在词法环境链中最近的this绑定
-
非严格模式下,函数普通调用,this指向全局对象
-
严格模式下,函数普通调用,this为undefined
-
函数作为对象方法调用,this指向该对象
-
函数作为构造函数配合new调用,this指向构造出的新对象
-
非严格模式下,函数通过call、apply、bind等间接调用,this指向传入的第一个参数, 传入的第一个参数若为undefined或null,this指向全局对象
-
格模式下函数通过call、apply、bind等间接调用,this严格指向传入的第一个参数