结合this的4种绑定规则 深入理解this指向的判定

此文适合用于进阶对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
文章目录
  1. 概述
  2. 默认绑定
  3. 隐式绑定
  4. // 判定this的指向
    1. 总结
      1. 一、先去找出this关键字所在的函数声明或函数表达式
      2. 二、找出该函数被调用时的表达式
      3. 三、观察该调用表达式的形态,确定this的指向
    2. 闭包
      1. 补充闭包常见demo
  5. 隐式丢失
    1. 函数别名的情况
    2. 参数传递的情况
    3. 内置函数的情况
    4. 间接引用的情况
    5. 其他情况
  6. 显示绑定
    1. es6显示绑定API
  7. new绑定
  8. 严格模式的严谨修正