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

此文适合用于进阶对this的理解,不适合用来入门。

概述

根据《你不知道的JavaScript》,this的绑定规则有4种:

1.默认绑定

2.隐式绑定

3.显式绑定

4.new绑定

这种划分是按照JavaScript的运行机理划分的,这有助于我们判定复杂的this的指向。

值得补充的是,

如果想要完全理解this的指向,可以完全不用理会 作用域和执行上下文等等概念,因为this和作用域等东西是两个互相独立开来的不同体系。而我们之所以讨论到变量提升,是为了综合这两个体系的知识来提高我们的判别能力。

如果想要完全理解this的指向,就一定要明白编译和执行的具体过程,另外要知道“执行流”这种工作方式。

默认绑定

全局环境中,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种绑定规则 的前两种进行了介绍,但是如果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指向objb:

如,demo2-3 中的obj1.obj2.foo(),this指向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;
    function test(){
        console.log(that.a);
    }
    return test;
};
var obj = {
    a : 2,
    foo:foo
}
obj.foo()();//2

隐式丢失

隐式丢失是指被隐式绑定的函数丢失绑定对象,从而默认绑定到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()方法把对象绑定到this上,叫做显式绑定;而对于被调用的函数来说,叫做间接调用

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,普通的显式绑定无法解决隐式丢失问题

硬绑定

foo.call(obj);的上级就有一个匿名函数,所以外部的代码每次都只能改到匿名函数的this,而改不到foo函数声明内的this。体现出一种硬绑定foo的效果,foo的this指向obj不受外部代码的干扰:

var a = 0;
function foo(){
    console.log(this.a);
}
var obj = {
    a:2
};
var obj2 = {
    a:4
};
var bar= function(){
    console.log(this) // 原本是window,这个this才会被外界代码干扰
    foo.call(obj); // 这句表达式才是硬绑定的核心
}
//在bar函数内部手动调用foo.call(obj)。因此,无论之后如何调用函数bar,它总会手动在obj上调用foo
bar();//2,只能干扰bar函数表达式内的this,不能干扰到foo的函数声明内的this
setTimeout(bar,100);//2,隐式丢失只能干扰bar函数表达式内的this,不能干扰到foo的函数声明内的this
bar.call(window);//2,只能干扰bar函数表达式内的this,不能干扰到foo的函数声明内的this

js原生显示绑定API

javascript中新增了许多内置函数,具有显式绑定的功能,如数组的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() {
    return this;
  }
}
var obj = new o.m();
console.log(obj.constructor === o); // false
console.log(obj.constructor === o.m); // true

严格模式的严谨修正

在非严格模式下,独立调用的函数的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. 1. 概述
  2. 2. 默认绑定
  3. 3. 隐式绑定
  4. 4. // 判定this的指向
    1. 4.1. 总结
      1. 4.1.1. 一、先去找出this关键字所在的函数声明或函数表达式
      2. 4.1.2. 二、找出该函数被调用时的表达式
      3. 4.1.3. 三、观察该调用表达式的形态,确定this的指向
    2. 4.2. 闭包
      1. 4.2.1. 补充闭包常见demo
  5. 5. 隐式丢失
    1. 5.1. 函数别名的情况
    2. 5.2. 参数传递的情况
    3. 5.3. 内置函数的情况
    4. 5.4. 间接引用的情况
    5. 5.5. 其他情况
  6. 6. 显示绑定
    1. 6.1. 硬绑定
    2. 6.2. js原生显示绑定API
  7. 7. new绑定
  8. 8. 严格模式的严谨修正
|