this全面解析

120 阅读3分钟

this的绑定规则有四条,按优先级排列为:默认绑定、隐式绑定、显式绑定、new绑定

一. 调用位置

如想明白this,一定要清楚调用位置是什么

调用位置:函数在代码中被调用的位置(而不是声明的位置)
调用栈:在当前正在执行的函数的前一个调用中

function baz() {
    // 当前调用栈是 baz
    // 当前调用位置是全局作用域
    bar()  // <-- bar的调用位置
}

function bar() {
    // 当前调用栈是 baz -> bar
    // 当前调用位置在 baz 中

    foo()   // <-- foo的调用位置
}

function foo() {
    // 当前调用栈是 baz -> bar -> foo
    // 当前调用位置是在 bar 中
}

baz()  // <-- baz的调用位置

二、绑定规则

1. 默认绑定

非严格模式下,默认绑定才能绑定到全局对象严格模式下为undefined

2. 隐式绑定

当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。因为调用foo()时this被绑定到obj,因此this.a === obj.a

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

var obj2 = {
    a: 42,
    foo: foo
}

var obj1 = {
    a: 2,
    obj2: obj2
}

obj1.obj2.foo()   // 42

隐式丢失

this的绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说会应用默认绑定,从而把this绑定到全局对象挥着undefined上,取决于是否是严格模式

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

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

var bar = obj.foo  // 函数别名(只是一个foo函数的引用地址)
var a = "oops global"  // a 是全局对象的属性
bar(); // oops global,调用位置为全局

一种更微妙、更常见并且更出乎意料的情况发生在传入回调函数时:

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,调用位置为全局
setTimeout(obj.foo, 100)  // oops global

/* function setTimeout(fn, delay) {
    fn(); // 调用位置,被设置为默认绑定 全局
} */

参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值

3. 显式绑定

a. 硬绑定

```javascript
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
```
```javascript
function foo(something) {
    console.log(this.a, something);
    return this.a + something;
}

var obj = {
    a: 2
}

/* var bar = function() {
    return foo.apply(obj, arguments);
} */

function bind(fn, obj) {
    return function() {
        return fn.apply(obj, arguments);
    }
}

var bar = bind(foo, obj)

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

b. API调用的"上下文"

第三方库的许多函数,以及JS语言和宿主环境中许多新的内置函数,都提供了以一个可选的参数,通常被称为"上下文"(context),其作用和bind(...)一样,确保你的回调函数使用指定的this

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

var obj = {
    id: "awesome"
}

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

4. new绑定

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

  1. 创建(或者说构造)一个全新的对象
  1. 这个新对象会被执行[[原型]]连接
  2. 这个新对象会绑定到函数调用的this
  3. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象
function newMethos(Func, ...args) {
    // 创建一个新对象
    const obj = {};
    if (Func.prototype) {
        // 将新创建对象的__proto__指向Func.prototype
        Object.setPrototypeOf(Func.prototype)
    }
    const res = Func.apply(obj, args)

    if (typeof res === "function" || (typeof res === "object" && res !== null)) {
        return res;
    }
    return obj;
}

使用new来调用foo(...)时,我们会构造一个新对象并把它绑定到foo(...)调用中的this上。new时最后一种可以影响函数调用时this绑定行为的方法,我们称之为new绑定

小结

如果要判断一个运行中函数的this绑定,就需要找到这个函数的直接调用位置。找到之后就可以顺序应用下面这四条规则来判断this的绑定对象,按优先级排序

  1. new 调用?绑定到新创建的对象
  2. callapply (或者 bind) 调用(硬绑定)?绑定到指定的对象
  3. 上下文对象调用?绑定到那个上下文对象
  4. 默认:在严格模式下绑定到undefined,否则绑定到全局对象 如果想更安全地忽略this绑定,你可以使用一个DMZ对象,比如 ø = Object.create(null),以保护全局对象

ES6中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定this,具体来说,箭头函数会继承外层函数调用的this绑定(无论this绑定到什么)。这其实和ES6之前代码中的 self = this 机制一样