Js的this关键字

Js中的this关键字,是个比较令人困惑的机制,起初我仔细研读了MDN里的介绍,但是终究不知其所以然,本着刨根究底的劲拜读了大神的《你不知道的Js》,感觉豁然开朗,在此分享个人的读后感。

什么是this?

它是一个在每个函数作用域中自动定义的特殊标识符关键字。

this不是编写时绑定,而是运行时绑定。它依赖于函数调用的上下文条件。
换句话说:this实际上是在函数被调用时建立的一个绑定,它指向什么是完全由函数被调用的调用点来决定的。

当一个函数被调用时,会建立一个称为执行环境的活动记录。这个记录包含:
1.函数是从何处(调用栈 —— call-stack)被调用的
2.函数是如何被调用的
3.被传递了什么参数
……
这个记录的属性之一,就是在函数执行期间将被使用的 this 引用。

调用点

调用点位于当前执行中的函数之前的调用,它是影响this绑定的唯一因素。
一张图让你轻松了解调用栈、调用点:

适用调用点的规则

在《你不知道的Js》中给出了4种规则,并且在同时满足时,它们各自的优先级。下面我来一一给大家分析:

默认绑定

最常见的是:独立函数调用,这种this规则相当于是没有其他规则适用时的默认规则。
例:

function foo() {
    console.log( this.a );
}

var a = 2;

foo(); // 2

foo()被调用时,this指向的是全局对象。

若函数体内容在严格模式下,此处this会被设置为undefined。
例:

function foo() {
    "use strict";

    console.log( this.a );
}

var a = 2;

foo(); // TypeError: `this` is `undefined`

值得注意的是:foo() 的调用点的 strict mode 状态与this绑定无关。
例:

function foo() {
    console.log( this.a );
}

var a = 2;

(function(){
    "use strict";

    foo(); // 2
})();

隐含绑定

先看代码:

function foo() {
    console.log( this.a );
}

var obj = {
    a: 2,
    foo: foo
};

obj.foo(); // 2

书作者认为:不论一开始在obj中声明,或是后来作为引用添加,foo函数并非被obj真正拥有或包含。因此,obj对象仅仅在函数被调用的时间点上“拥有”或“包含”这个函数引用

隐含绑定规则:当一个方法引用存在一个环境对象时,这个对象应当被用于这个函数调用的 this 绑定。换句话说,就是改变目标对象使它自身包含一个对函数的引用,而后使用这个函数引用属性来间接地(隐含地)将 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 = "oops, global"; // `a` 也是一个全局对象的属性

bar(); // "oops, global"

解析:var bar = obj.foo,bar其实是引用foo本身。而调用点是bar(),因此此时 默认绑定 适用。

例二:

function foo() {
    console.log( this.a );
}

function doFoo(fn) {
    // `fn` 只不过 `foo` 的另一个引用

    fn(); // <-- 调用点!
}

var obj = {
    a: 2,
    foo: foo
};

var a = "oops, global"; // `a` 也是一个全局对象的属性

doFoo( obj.foo ); // "oops, global"    

解析:参数传递仅仅是一种隐含的赋值,而传递函数是一个隐含的引用赋值,所以最终结果与之前一样。

如果接收你所传递回调的函数是语言内建的呢?没有区别,同样的结果。
例三:

function foo() {
    console.log( this.a );
}

var obj = {
    a: 2,
    foo: foo
};

var a = "oops, global"; // `a` 也是一个全局对象的属性

setTimeout( obj.foo, 100 ); // "oops, global"

可以看出,回调函数丢掉自身的 this 绑定是十分常见的事情,甚至会被一些第三方框架覆盖。

明确绑定

相当于通过 call 和 apply 直接指明你想让 this 是什么。
例:

function foo() {
    console.log( this.a );
}

var obj = {
    a: 2
};

foo.call( obj ); // 2

虽然强制 this 指向了obj,但是并未解决回调函数丢失自身 this 绑定 等问题。
那么我们可以这样:把 foo.call( obj ) 包裹在一个函数里。给一个高大尚的命名——硬绑定

硬绑定

直接看例子:

function foo() {
    console.log( this.a );
}

var obj = {
    a: 2
};

var bar = function() {
    foo.call( obj );
};

bar(); // 2
setTimeout( bar, 100 ); // 2

// `bar` 将 `foo` 的 `this` 硬绑定到 `obj`
// 所以它不可以被覆盖
bar.call( window ); // 2

将 foo.call( obj ) 包裹在bar的函数体里,这样,不论如何调用bar,this 永远和 obj 绑定。
其他例子:

由于硬绑定很常用,因此已作为 ES5 的内建工具提供:Function.prototype.bind。
注:在 ES6 中,bind(..) 生成的硬绑定函数有一个名为 .name 的属性,它源自于原始的 目标函数(target function)

API调用的环境

许多Js库里的函数和原生Js的函数都提供了一个可选参数,通常称之为环境。这样设计是为了确保回调函数使用特定的 this 而不必非得使用 bind(..)。
例:

function foo(el) {
    console.log( el, this.id );
}

var obj = {
    id: "awesome"
};

// 使用 `obj` 作为 `this` 来调用 `foo(..)`
[1, 2, 3].forEach( foo, obj ); // 1 awesome  2 awesome  3 awesome

从内部来说,几乎可以确定这种类型的函数是通过 call(..) 或 apply(..) 来使用 明确绑定 以节省你的麻烦。

new绑定

例:

function foo(a) {
    this.a = a;
}

var bar = new foo( 2 );
console.log( bar.a ); // 2

可以看出:我们构建了一个新的对象并把这个新对象作为 foo(..) 调用的 this,关于 new 的机制详细情况可参考我之前写的《Js的new关键字》。

this绑定规则优先级总结

通用规则

特例

规则难免会有特例。

当传递 null 或 undefined 作为 call、apply 或 bind 的 this 绑定参数时,会使用默认绑定的规则。

然而需要的注意的是:如果这样处理函数调用,当遇到不归自己管控的第三方包时,并且那些函数的确用了this引用,那么默认绑定规则意味着可能不经意引用或改变 global 对象(在浏览器中是 window)。
更安全的方式是:传递完全为空的对象(通过Object.create(null)创建)作为 call、apply 或 bind 的 this 绑定参数。

间接引用

例:

function foo() {
    console.log( this.a );
}

var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };

o.foo(); // 3
(p.foo = o.foo)(); // 2

赋值表达式 p.foo = o.foo 的 结果值 是一个刚好指向底层函数对象的引用。如此,起作用的调用点就是 foo(),而非你期待的 p.foo() 或 o.foo()。根据上面的规则,默认绑定 适用。

ES6的箭头函数

例:

function foo() {
    // 返回一个箭头函数
    return (a) => {
        // 这里的 `this` 是词法上从 `foo()` 采用的
        console.log( this.a );
    };
}

var obj1 = {
    a: 2
};

var obj2 = {
    a: 3
};

var bar = foo.call( obj1 );
bar.call( obj2 ); // 2, 不是3!

代码等价于:

箭头函数从封闭它的(函数或全局)作用域采用 this 绑定。本质是使用广为人知的词法作用域来禁止了传统的 this 机制。

总结

在写这篇读后感最初的想法是打算在书作者的基础上对 this 进行总结并加入个人的想法,但是可能确实涉及的东西比较多,相互关联,因此可能并没有总结得很到位。感觉我只是把书重新读了一遍,然后筛选了一些我认为比较重要的知识点。详细的大家可以去参考大神的著作。

参考文章

MDN-this
你不知道的Js-this与对象原型