全面认识this指向

372 阅读6分钟

概述

this的绑定规则主要有以下几种:

  1. 默认绑定 (函数调用)
  2. 隐式绑定 (方法调用)
  3. 显式绑定 (简介调用)
  4. new绑定 (构造函数调用)
  5. 箭头函数绑定

在 ES5 中,其实 this 的指向,始终坚持一个原理: this 永远指向最后调用它的那个对象

默认绑定

这是最常用的函数调用类型,独立函数调用。当其他绑定规则不适用时默认使用该默认绑定规则。

调用位置就是函数在代码中被调用的位置(而不是声明的位置)。

例如:

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

var a = 1;

bar();  // 1

这一段在浏览器上输出是1,因为调用bar函数的环境是全局环境,此时this指的就是全局的环境。

来把a的定义位置换一下,

function bar () {
    var a = 1;  // 在函数内部定义局部变量
    console.log(this.a);
}

bar();  // undefined

此时输出的不再是1,因为bar是在全局环境中调用,所以this指向的是全局的环境,没有访问到bar函数内部的作用域。

如果使用严格模式(strict mode),那么全局对象将无法使用默认绑定,因此 this 会绑定 到 undefined:

function bar() { 
    "use strict";
    console.log( this.a );
}

var a = 1;

bar(); // TypeError: this is undefined

隐式绑定

需要考虑调用位置是否具有上下文对象。

当函数引用有上下文对象时,隐式绑定规则会把函数中的this绑定到这个上下文对象。

看下面这个例子:

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

var test = {
    a: 2,
    bar: bar
};

test.bar();

但是注意:对象属性引用链中只有最后一层在调用中起作用

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

var test1 = {
    a: 2,
    bar: bar
};

var test2 = {
    a: 4,
    test1: test1
};

test2.test1.bar();  // 2

隐式丢失

隐式绑定规则会出现一个问题,就是隐式丢失。如果被隐式绑定的函数丢失绑定对象,则调用默认绑定规则。

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
};
var bar = obj.foo; // 函数别名!
var a = "global"; // a是全局对象的属性
bar(); // "global"

还有一种情况发生在传入回调函数中。参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值。回调函数丢失 this 绑定是非常常见的。

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

function doFoo(fn) {
    // fn其实引用的是foo 
    fn(); // <-- 调用位置!
}
var obj = {
    a: 2,
    foo: foo
};
var a = "global"; // a是全局对象的属性 
doFoo( obj.foo ); // "global"

this在内部函数中

var numbers = {
    numberA: 5,
    numberB: 10,
    sum: function () {
        console.log(this === numbers); // => true
        function calculate() {
            // 严格模式下, this 是 window or undefined
            console.log(this === numbers); // => false
            return this.numberA + this.numberB;
        }
        return calculate();
    }
};
numbers.sum(); // => NaN,  严格模式下,结果为 NaN 或者 throws TypeError  

这里的sum是通过numbers的方法调用的,所以sum中的this指向的是numbers,但是内部函数中的calculate函数却是属于函数调用,应用第一种默认绑定规则,此时的 this 指向的是全局对象

显式绑定

如果想要在某个对象上强制调用函数,可以使用call或apply来显式绑定this。

它们的第一个参数是一个对象,它们会把这个对象绑定到 this,接着在调用函数时指定这个 this。

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2
};
foo.call(obj); // 2
// 或foo.apply(obj); // 2

可惜,显式绑定仍然无法解决我们之前提出的丢失绑定问题。

硬绑定

是显式绑定的一个变种,创建函数bar(),并在它的内部手动调用foo.call(obj),强制把foo的this绑定到了obj。

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

var obj = {
    a: 2
};

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

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

// 硬绑定的bar不可能再修改它的this
bar.call( window ); // 2

API调用的“上下文”

JS许多内置函数提供了一个可选参数,被称之为“上下文”(context),其作用和bind(..)一样,确保回调函数使用指定的this。这些函数实际上通过call(..)和apply(..)实现了显式绑定。

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

var obj = {
    id: "awesome"
}

var myArray = [1, 2, 3]
// 调用foo(..)时把this绑定到obj
myArray.forEach( foo, obj );
// 1 awesome 2 awesome 3 awesome

这些函数实际上就是通过 call(..) 或者 apply(..) 实现了显式绑定,这样你可以少些一些代码。

new绑定

使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

  1. 创建(或者说构造)一个新对象。
  2. 这个新对象会被执行[[Prototype]]连接。
  3. 这个新对象会绑定到函数调用的this。
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。

使用new来调用foo(..)时,会构造一个新对象并把它(bar)绑定到foo(..)调用中的this。

function foo(a) {
    this.a = a;
}
var bar = new foo(2);
console.log(bar.a); // 2

箭头函数绑定

箭头函数内的 this与其他类型的JavaScript函数有很大的不同。

箭头函数中没有 this 绑定,必须通过查找作用域链来决定其值。如果箭头函数被非箭头函数包含,则 this 绑定的是最近一层非箭头函数的 this;否则,this 的值会被设置为 undefined。

var a = 2;
var bar = {
  a: 4,
  test:() => {
    console.log(this.a, this); 
  }
}

bar.test(); // 2  Window

注意: 箭头函数的绑定无法被修改

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

如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值在调用时会被忽略,实际应用的是默认绑定规则:

function foo() {
  console.log(this.a);
}
var a = 2;
foo.call(null); // 2

间接引用

间接引用容易发生在赋值时,

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()。

call、apply、bind的区别

在显示绑定中提到使用call和apply,下面来看一下这几个函数之间的用法区别。

call和apply的区别

其实 apply 和 call 的用法基本一致,只是传入的参数不同

  • call 方法接受的是若干个参数列表
  • apply 接收的是一个包含多个参数的数组。

bind和call、apply的区别

  • .apply() 和 .call(),它俩都立即执行了函数,
  • .bind() 不会立即调用执行,返回了一个新方法,绑定了预先指定好的 this ,并可以延后调用

例如: call或apply使用时:

returnThis.call(boss1);

bind使用时:

var bossreturnThis = returnThis.bind(boss1);

bossreturnThis() // 需要再手动调用一次

总结一下:判断函数的this指向时,不要思考this是从哪里来的,而是想想函数当前是怎么被调用的;对于箭头函数,只要看它在哪里创建,定义时的this指向

参考: 《你不知道的JavaScript 上卷》