此文适合用于进阶对this的理解,不适合用来入门。
概述
根据《你不知道的JavaScript》,this的绑定规则有4种:
1.默认绑定
2.隐式绑定
3.显式绑定
4.new绑定
这种划分是按照JavaScript的运行机理划分的,这有助于我们判定复杂的this的指向。
值得补充的是:
我们可以粗浅地理解为:进入函数的“执行上下文”会发生三件事情:1 确定this指向 2 创建活动对象并完成“变量提升” 3 确定作用域链 。
可见,如果想要完全理解this的指向,可以完全不用理会“作用域”的概念,它们是不同的体系。
“变量提升”也是不同于this的问题体系,联系在一起讲好像不会增加理解的难度,所以后文会涉及到“变量提升”。
书中提出了“执行流”这种虚拟的概念,来比较死板地判断this的指向,判断效果还是挺靠谱的。
默认绑定
非严格模式下,全局执行上下文this默认指向window;而严格模式下,全局执行上下文this默认为undefined。
本小节是以“非严格模式”作为前提讲解的了;严格模式同理。
全局执行上下文中,this默认绑定全局对象;而在浏览器下,全局对象就是 window。
即简而言之:
结论1:全局执行上下文中,this默认绑定到window。
// demo 1-1:
console.log(this === window); // true
console.log(this.document === document); // true
this.a = 91;
console.log(window.a); // 91
结论2:函数独立调用时,this默认绑定到window。
// demo 1-2:
function foo(){
console.log(this === window);
}
foo(); // foo是this关键字所在的函数,执行流到此,之后执行流会进入该函数的声明内
// 结果:foo()返回true
独立调用,foo()
等价于window.foo()
结论3:IIFE的本质就是 函数声明+立即的函数独立调用,所以我们把IIFE视为独立调用的情况即可:
// demo 1-3:
(function () {
console.log(this === window); // true
})();
等价于:
// demo 1-4:
function aname() {
console.log(this === window); // true
}
aname(); // 声明之后要立即执行
隐式绑定
一般地,如果是对象的方法调用,对于这次函数的执行,this隐式绑定到该对象上:
// demo 2-1:
var a = 0;
var obj = {
a : 2,
foo:function(){
console.log(this.a);
}
}
obj.foo(); // foo是this关键字所在的函数,执行流到此,之后执行流会进入该函数的声明内
// 结果:obj.foo()返回2
变量提升等价于:
// demo 2-2:
function foo () {
console.log(this.a);
}
var a = 0;
var obj;
obj = {
a : 2,
foo: foo
}
obj.foo(); // foo是this关键字所在的函数,执行流到此,之后执行流会进入该函数的声明内
// 结果:obj.foo()返回2
再对隐式绑定知识做一个巩固:
// demo 2-3:
function foo(){
console.log(this.a);
};
var obj1 = {
a:1,
foo:foo,
obj2:{
a:2,
foo:foo
}
}
obj1.foo(); // foo是this关键字所在的函数,执行流到此,之后执行流会进入该函数的声明内;结果为1(访问到了obj1.a)
obj1.obj2.foo();// foo是this关键字所在的函数,执行流到此,之后执行流会进入该函数的声明内;结果为2(访问到了obj1.obj2.a)
// 判定this的指向
我们已经对 this的4种绑定规则 的前2种进行了介绍,但是如果demo变得再复杂一点点,this的指向就比较难以判定了。
请在已有上文知识的基础上,思考下面的函数返回结果是如何得出的:
// exer 1-1 :代码可以对比demo 2-1 的代码
var a = 0;
var obj = {
a : 2,
foo:function(){
function test() {
console.log(this.a);// 0
}
test(); // test是this关键字所在的函数,执行流到此,之后执行流会进入该函数的声明内
}
}
obj.foo(); // 实际上,foo还不是this关键字所在的函数,test才是;执行流先执行foo,再执行test
// 结果:obj.foo()返回0
变量提升等价于:
// exer 1-2 :
var a = 0;
var obj;
function foo () {
function test() {
console.log(this.a);// 0
}
test(); // test是this关键字所在的函数,执行流到此,之后执行流会进入该函数的声明内
}
obj = {
a : 2,
foo: foo
}
obj.foo(); // 实际上,foo还不是this关键字所在的函数,test才是;执行流先执行foo,再执行test
// 结果:obj.foo()返回0
虽然test()函数被嵌套在obj.foo()函数声明中, 但test()函数是独立调用的, 因而它不是对象的方法调用
。
既然是独立调用,this默认绑定到window。
由demo 1-3 我们知道IIFE的本质有利于攻破含IIFE的问题,在上面的exer 1-2 中,我们可以再把test等价于一个IIFE函数:
// exer 1-3 :
var a = 0;
var obj;
function foo () {
(function () {
console.log(this.a);// 0
})(); // 该IIFE是this关键字所在的函数,执行流到此,之后执行流会进入该函数的声明内
}
obj = {
a : 2,
foo: foo
}
obj.foo(); // 实际上,foo还不是this关键字所在的函数,IIFE才是;执行流先执行foo,再执行IIFE
// 结果:obj.foo()返回0
总结
我们似乎发现了 判定this的指向 的方法:
我们以exer 1-2 为例,进行说明:
// exer 1-2 :
var a = 0;
var obj;
function foo () {
function test() {
console.log(this.a);// 0
}
test(); // test是this关键字所在的函数,执行流到此,之后执行流会进入该函数的声明内
}
obj = {
a : 2,
foo: foo
}
obj.foo(); // 实际上,foo还不是this关键字所在的函数,test才是;执行流先执行foo,再执行test
// 结果:obj.foo()返回0
一、先去找出this关键字所在的函数声明或函数表达式
即,this关键字在哪个函数的声明内?!或者在哪个函数的表达式内?! this往上一级的那个函数便是了
我们发现要找的这个函数是test,this关键字在test的函数声明内。(而不是foo,foo只是test的上级函数)
二、找出该函数被调用时的表达式
例子代码中,第8行的 test() 是我们要找的调用表达式(此时test函数才被调用)
三、观察该调用表达式的形态,确定this的指向
观察 test() 这个调用表达式的形态
如果形态像“独立调用”fn()
,那么this指向window:
如,demo 1-2 和 exer 1-2 。
如果表达式形态可以说是IIFE,那么也可以视为函数的独立调用,this指向window:
如,demo 1-3 和 exer 1-3 。
如果形态像obj.fn()
,那么this指向obj:
如,exer 2-1中的obj.foo()
。然后另外,容易误导人的是像exer 1-2 中的obj.foo()
这句,我们说了在exer 1-2中,foo不是我们想要的,所以应该不必理会这句,而应该去找test函数被调用的那句表达式。
如果形态像obja.objb.fn()
,那么那么this指向obja.objb
:
如,demo2-3 中的obj1.obj2.foo()
,this指向obj1.obj2
以上的 判定this的指向 的方法是在任何情况下通用的, 我们拿闭包来练练手,顺便讲一下闭包:
闭包
其实闭包在本文不必按特殊情况考虑,鉴于某些新手可能不熟悉闭包,特拿出来一种闭包做例子说明:
var a = 0;
function foo(){
function test(){
console.log(this.a);
}
return test;
};
var obj = {
a : 2,
foo:foo
}
obj.foo()(); // test是this关键字所在的函数,执行流到此,之后执行流会进入该函数的声明内
// 结果:obj.foo()()返回0
等价于:
function test () {
console.log(this.a);
}
var a = 0;
test(); // test是this关键字所在的函数,执行流到此,之后执行流会进入该函数的声明内
即函数的“独立调用”,那么this指向window
补充闭包常见demo
因此,这也是一个老生常谈,(类似上面的例子),由于闭包的this常常默认绑定到window对象。如果在这种情况下,需要访问外层嵌套函数的this,常见的做法是使用var that = this
,然后在闭包中使用that替代this,使用作用域查找的方式来找到外层嵌套函数的this值 :
var a = 0;
function foo(){
var that = this;
this.sth = 'hello'
function test(){
console.log(that.a, that.sth);
}
return test;
};
var obj = {
a : 2,
foo:foo
}
obj.foo()();//2 hello
隐式丢失
隐式丢失是指被隐式绑定的函数丢失绑定对象,从而默认绑定到window的现象。这是一种程序员常见的逻辑上出错的情况。
同样地, 以上的 判定this的指向 的方法在隐式丢失中依然适用。
首先,按照常见的类型,隐式丢失可以又分为:
1.函数别名的情况
2.参数传递的情况
3.内置函数的情况
4.间接引用的情况
5.其他情况
函数别名的情况
var a = 0;
function foo(){
console.log(this.a);
};
var obj = {
a : 2,
foo:foo
}
//把obj.foo赋予别名bar,造成了隐式丢失,因为只是把foo()函数赋给了bar,而bar与obj对象则毫无关系
var bar = obj.foo;
bar(); // bar和foo都是this关键字所在的函数,执行流到此,之后执行流会进入bar的函数表达式内;考虑到bar被调用时这句表达式的形态,结果为0依然可以被理解
等价于:
var a = 0;
var bar = function foo(){
console.log(this.a);
}
bar();//0
参数传递的情况
var a = 0;
function foo(){
console.log(this.a);
};
function bar(fn){
fn(); // fn和foo都是this关键字所在的函数,执行流到此,之后执行流会进入fn的函数表达式内;考虑到fn被调用时这句表达式的形态,结果为0依然可以被理解
}
var obj = {
a : 2,
foo:foo
}
//把obj.foo当作参数传递给bar函数,之后有隐式的函数赋值fn=obj.foo。与上例类似,只是把foo函数赋给了fn,而fn与obj对象则毫无关系
bar(obj.foo);//0
等价于:
var a = 0;
function foo(){
console.log(this.a); // 0
};
function bar(fn){
fn(); // fn和foo都是this关键字所在的函数,执行流到此,之后执行流会进入fn的函数表达式内;考虑到fn被调用时这句表达式的形态,结果为0依然可以被理解
}
bar(foo);// 0
内置函数的情况
内置函数的情况的本质是参数传递的情况:
var a = 0;
function foo(){
console.log(this.a);
};
var obj = {
a : 2,
foo:foo
}
setTimeout(obj.foo,100); // obj.foo作为setTimeout的内置函数
// 结果: setTimeout(obj.foo,100)返回0
等价于:
var a = 0;
function foo(){
console.log(this.a); // 0
};
function setTimeout(fn, 100){
// 考虑setTimeout的原生代码的本质,等待100秒后执行:
fn(); // fn和foo都是this关键字所在的函数,执行流到此,之后执行流会进入fn的函数表达式内;考虑到fn被调用时这句表达式的形态,结果为0依然可以被理解
}
setTimeout(foo, 100); // 0
间接引用的情况
间接引用的情况的本质是函数别名的情况:
var a = 2;
function foo() {
console.log( this.a );
}
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
//将o.foo函数赋值给p.foo函数,然后立即执行,此时p.foo发生了隐式丢失
(p.foo = o.foo)(); // 2
// IIFE和p.foo和o.foo都是this关键字所在的函数,执行流到此,之后执行流会进入IIFE的函数表达式内,结果为2依然可以被理解
等价于:
var a = 2;
function foo() {
console.log( this.a );
}
var p = { a: 4 };
p.foo = foo;
var aname = p.foo; //第一对括号的作用
aname(); // IIFE和p.foo和o.foo都是this关键字所在的函数,执行流到此,之后执行流会进入IIFE的函数表达式内,结果为2依然可以被理解
而如果我们想要一个没有隐式丢失的类似demo:
var a = 2;
function foo() {
console.log( this.a );
}
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
//将o.foo函数赋值给p.foo函数,之后p.foo函数再执行
p.foo = o.foo;
p.foo(); // 4
// p.foo和o.foo都是this关键字所在的函数,执行流到此,之后执行流会进入p.foo的函数表达式内,结果为4是因为隐式绑定
// 这没有隐式丢失,但结果依然可以按照我们的理解方法来理解
其他情况
在javascript引擎内部,obj和obj.foo储存在两个内存地址,在这里我们简称为M1和M2。只有obj.foo()这样形式的表达式调用时,是从M1调用M2,因此this指向obj。但是,下面三种情况,都是直接取出M2进行运算,然后就在全局执行上下文执行运算结果(还是M2),因此this指向全局对象
var a = 0;
var obj = {
a : 2,
foo:foo
};
function foo() {
console.log( this.a );
};
(obj.foo = obj.foo)();//0
(false || obj.foo)();//0
(1, obj.foo)();//0
也可以像上面 间接引用的情况 理解为: IIFE和obj.foo都是this关键字所在的函数,执行流到此,之后执行流会进入IIFE的函数表达式内;考虑到IIFE的表达式,结果为0依然可以被理解
显示绑定
- 显示绑定
- 通过
call()、apply()、bind()
- es6的
map()、forEach()、filter()、some()、every()
方法:专门提供1参数来设置this。( 其原理还是基于es5这几个api )
- 通过
var a = 0;
function foo(){
console.log(this.a);
}
var obj1 = {
a:1
};
var obj2 = {
a:2
};
foo.call(obj1);//1
foo.call(obj2);//2
用显式绑定就不用考虑会有隐式丢失的问题了:因为显式绑定优先于隐式绑定,如果程序员用显式绑定,在心里就已经知道隐式绑定无效了:
var a = 0;
function foo(){
console.log(this.a);
}
var obj100 = {
a : 100,
foo: foo
}
var obj1 = {
a:1
};
var obj2 = {
a:2
};
obj100.foo.call(obj1);//1
obj100.foo.call(obj2);//2
obj100.foo() // 100,普通的显式绑定无法解决隐式丢失问题
es6显示绑定API
es6中新增了许多内置函数,具有显式绑定的功能,如数组的5个迭代方法:map()、forEach()、filter()、some()、every()
var id = 'window';
function foo(el){
console.log(el,this.id);
}
var obj = {
id: 'fn'
};
[1,2,3].forEach(foo);//1 "window" 2 "window" 3 "window"
[1,2,3].forEach(foo,obj);//1 "fn" 2 "fn" 3 "fn"
new绑定
new绑定通常指的是构造器中的this。
1.构造函数通常不使用return关键字,它们通常初始化新对象,当构造函数的函数体执行完毕时,它会隐式返回。在这种情况下,构造函数调用表达式的计算结果就是这个新对象的值:
function fn() {
this.a = 2;
}
var test = new fn();
console.log(test); // {a:2}
2.如果构造函数显式地没有返回值 或者 显式地返回为基本类型时,那么这时将忽略返回值,将隐式默认值返回:
function fn() {
this.a = 2;
return;
}
var test = new fn();
console.log(test); // {a:2}
3.如果构造函数显式地使用return语句返回一个对象,那么调用表达式的值就是这个对象:
function C2() {
this.a = 26;
return {
a: 24
};
}
o = new C2();
console.log(o.a); // 24
[注意]尽管有时候构造函数看起来像一个方法调用,它依然会使用这个新对象作为this。也就是说,例如在表达式new o.m()中,this并不是指向o:
var o = {
m: function() {
this.a = 'test a';
return this;
}
}
var obj = new o.m();
console.log(obj.constructor === o); // false
console.log(obj.constructor === o.m); // true
console.log(obj.a) // 'test a'
严格模式的严谨修正
在非严格模式下,独立调用的函数的this指向window;而在严格模式下,独立调用的函数的this指向undefined:
function fn(){
'use strict';
console.log(this);//undefined
}
fn();
function fn(){
console.log(this);//window
}
fn();
在非严格模式下,使用函数的call()或apply()方法时,第一个参数若使用null或undefined,值会被转换为全局对象;而在严格模式下,函数的this值始终是表达式中所指定的值:
var color = 'red';
function displayColor(){
console.log(this.color);
}
displayColor.call(null);//red
var color = 'red';
function displayColor(){
'use strict';
console.log(this.color);
}
displayColor.call(null);//TypeError: Cannot read property 'color' of null