This是什么

this 不是编写时绑定,而是运行时绑定。它依赖于函数调用的上下文条件。this 绑定与函数声明的位置没有任何关系,而与函数被调用的方式紧密相连

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

This绑定规则

默认绑定

第一种规则源于函数调用的最常见的情况:独立函数调用。可以认为这种 this 规则是在没有其他规则适用时的默认规则

  1. function foo() {
  2. console.log( this.a );
  3. }
  4. var a = 2;
  5. foo(); // 2 --> 当被调用时,对此方法调用的 this 实施了 默认绑定,所以使 this 指向了全局对象

如果采用 strict modethis 将被设置为 undefined

隐式绑定

也就是确定 调用点是否有一个环境对象(context object)

  1. function foo() {
  2. console.log( this.a );
  3. }
  4. var obj = {
  5. a: 2,
  6. foo: foo //foo()函数不被 obj 所真正“拥有”或“包含”,只是一个引用属性而已
  7. };
  8. obj.foo(); // 2 调用点使用obj环境来引用函数,所以你可以说obj对象在函数被调用的时间点上“拥有”或“包含”这个 函数引用

当一个方法引用(foo)存在一个环境对象(obj)时,隐含绑定 就此发生:这个对象(obj)应当被用于这个函数 (foo())调用的 this 绑定。

隐式丢失

this 绑定最烦人的就是当一个 隐含绑定 丢失了它的绑定,退回到 默认绑定 的情况

  1. function foo() {
  2. console.log( this.a );
  3. }
  4. var obj = {
  5. a: 2,
  6. foo: foo
  7. };
  8. var bar = obj.foo; // bar表面上看obj.foo的引用,但实际也只是foo的另一个引用而已
  9. var a = "oops, global"; // `a` 也是一个全局对象的属性
  10. bar(); --> 这个调用点完全符合 默认绑定规则 // "oops, global"

还有一个更微妙的发生方式如下代码:

  1. function foo() {
  2. console.log( this.a );
  3. }
  4. function doFoo(fn) {
  5. // `fn` 只不过 `foo` 的另一个引用
  6. fn(); // <-- 因此 这里才是 调用点!
  7. }
  8. var obj = {
  9. a: 2,
  10. foo: foo
  11. };
  12. var a = "oops, global"; // `a` 也是一个全局对象的属性
  13. doFoo( obj.foo ); // "oops, global"

及时是内建的语言回调函数也是同一个结果

  1. function foo() {
  2. console.log( this.a );
  3. }
  4. var obj = {
  5. a: 2,
  6. foo: foo
  7. };
  8. var a = "oops, global"; // `a` 也是一个全局对象的属性
  9. setTimeout( obj.foo, 100 ); // "oops, global" --> obj.foo在内建的回调函数中,
  10. // 也只是一个不同名的引用而已
  11. // 实际函数的调用点在 执行的时候也只是在全局作用下,因此读取到的a属性是 'oops,global'

我们的回调函数丢掉他们的 this 绑定是十分常见的事情。但是 this 使我们吃惊的另一种方式是,接收我们回调的函数故意改变调用的 this

不管哪一种意外改变 this 的方式,你都不能真正地控制你的回调函数引用将如何被执行,所以你(还)没有办法控制调用点给你一个故意的绑定。

明确绑定

绝大多数被提供的函数,当然还有你将创建的所有的函数,都可以访问 call(..)apply(..)

它们接收的第一个参数都是一个用于 this 的对象,之后使用这个指定的 this 来调用函数。因为你已经直接指明你想让 this 是什么,所以我们称这种方式为 明确绑定(explicit binding)

  1. function foo() {
  2. console.log( this.a );
  3. }
  4. var obj = {
  5. a: 2
  6. };
  7. foo.call( obj ); // 2
  8. 强制将 objthis绑定

如果你传递一个简单基本类型值(stringboolean,或 number 类型)作为 this 绑定,那么这个基本类型值会被包装在它的对象类型中(分别是 new String(..)new Boolean(..),或 new Number(..))。这通常称为“封箱(boxing)”。

硬绑定

  1. function foo() {
  2. console.log( this.a );
  3. }
  4. var obj = {
  5. a: 2
  6. };
  7. var bar = function() {
  8. foo.call( obj );// 强制this绑定到obj 并调用foo函数,以后无论怎么调用bar函数,都有此硬绑定存在
  9. };
  10. bar(); // 2
  11. setTimeout( bar, 100 ); // 2
  12. // `bar` 将 `foo` 的 `this` 硬绑定到 `obj`
  13. // 所以它不可以被覆盖
  14. bar.call( window ); // 2

我们创建了一个函数 bar(),在它的内部手动调用 foo.call(obj),由此强制 this 绑定到 obj 并调用 foo。无论你过后怎样调用函数 bar,它总是手动使用 obj 调用 foo。这种绑定即明确又坚定,所以我们称之为 硬绑定(hard binding)

硬绑定_ 将一个函数包装起来的最典型的方法,是为所有传入的参数传出的返回值创建一个通道:

function foo(something) {
    console.log( this.a, something );
    return this.a + something;
}

var obj = {
    a: 2
};

var bar = function() {
    return foo.apply( obj, arguments ); //bar接收参数,用obj硬绑定this,并且使用foo函数返回值
};

var b = bar( 3 ); // 2 3
console.log( b ); // 5

硬绑定已作为 ES5 的内建工具提供:Function.prototype.bind

...
var bar = foo.bind( obj );//用函数的bind函数替代
...

new 绑定

实际上 JavaScript 的机制和 new 在 JS 中的用法所暗示的面向类的功能 没有任何联系
_
在 JS 中,构造器 仅仅是一个函数,它们偶然地与前置的 new 操作符一起调用。它们不依附于类,它们也不初始化一个类。它们甚至不是一种特殊的函数类型。它们本质上只是一般的函数,在被使用 new 来调用时改变了行为

实际上不存在“构造器函数”这样的东西,而只有函数的构造器调用(就是在前面加new)。

当在函数前面被加入 new 调用时,也就是构造器调用时,下面这些事情会自动完成:

  1. 一个全新的对象会凭空创建(就是被构建)
  2. 这个新构建的对象会被接入原形链([[Prototype]]-linked)
  3. 这个新构建的对象被设置为函数调用的 this 绑定
  4. 除非函数返回一个它自己的其他 对象,否则这个被 new 调用的函数将 自动 返回这个新构建的对象。
function foo(a) {
    this.a = a;
}

var bar = new foo( 2 ); // 新创建了一个bar对象,来作为foo函数的this绑定调用
console.log( bar.a ); // 2

判定this

  1. 函数是通过 new 被调用的吗(new 绑定)?如果是,this 就是新构建的对象。
    var bar = new foo()
  2. 函数是通过 callapply 被调用(明确绑定),甚至是隐藏在 bind 硬绑定 之中吗?如果是,this 就是那个被明确指定的对象。
    var bar = foo.call( obj2 )
  3. 函数是通过环境对象(也称为拥有者或容器对象)被调用的吗(隐含绑定)?如果是,this 就是那个环境对象。
    var bar = obj1.foo()
  4. 否则,使用默认的 this默认绑定)。如果在 strict mode 下,就是 undefined,否则是 global 对象。
    var bar = foo()

绑定的特例

被忽略的 this

如果你传递 nullundefined 作为 callapplybindthis 绑定参数,那么这些值会被忽略掉,取而代之的是 默认绑定 规则将适用于这个调用

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

var a = 2;

foo.call( null ); // 2

更安全的 this

某些“更安全”的做法是:为了 this 而传递一个特殊创建好的对象,这个对象保证不会对你的程序产生副作用。建立一个“DMZ”(非军事区)对象 —— 只不过是一个完全为空,没有委托的对象。
创建 完全为空的对象 的最简单方法就是 Object.create(null)
Object.create(null){} 很相似,但是没有指向 Object.prototype 的委托,所以它比 {} “空得更彻底”。

function foo(a,b) {
    console.log( "a:" + a + ", b:" + b );
}

// 我们的 DMZ 空对象
var ø = Object.create( null );

// 将数组散开作为参数
foo.apply( ø, [2, 3] ); // a:2, b:3

// 用 `bind(..)` 进行 currying
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2, b:3

Object.create(null) 语义上可能会比 null 更清晰的表达“我想让 this 为空”

词法 this

不适用于这些规则特殊的函数:箭头函数(arrow-function),箭头函数从封闭它的(函数或全局)作用域采用 this 绑定

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

var obj1 = {
    a: 2
};

var obj2 = {
    a: 3
};

var bar = foo.call( obj1 );//foo函数显式绑定obj1作为this,所以箭头函数以这个为准
bar.call( obj2 ); // 2, 不是3!

foo() 中创建的箭头函数在词法上捕获 foo() 被调用时的 this,不管它是什么。因为 foo()this 绑定到 obj1bar(被返回的箭头函数的一个引用)也将会被 this 绑定到 obj1。一个箭头函数的词法绑定是不能被覆盖的(就连 new 也不行!)

最常见的用法是用于回调,比如事件处理器或计时器:

function foo() {
    setTimeout(() => {
        // 这里的 `this` 是词法上从 `foo()` 采用
        console.log( this.a );
    },100);
}
var obj = {
    a: 2
};
foo.call( obj ); // 2