JS this绑定

280 阅读7分钟

重要的写在开头

this 绑定我认为与两点有关:

第一就是这个函数它通过什么样的方式被调用,是直接调用,还是以对象的方法进行调用,还是以其它的形式调用,这是会影响 this 指向的第一点。

第二是这个函数它的声明位置是否为严格模式,这也会对 this 的指向有影响,就比如说我们通过直接调用函数的方式去调用一个声明在 class 内部的函数,由于 class 内部是严格模式,就会导致 this 指向 undefined,还有箭头函数也是这样的情况,箭头函数的 this 指向就与它声明位置所在的作用域有关,具体后面会介绍。

this的绑定规则

1. 默认绑定

函数直接调用,这是最常见的方式,可以把这个规则看作是无法应用其他规则时的默认规则

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

可以看到此时this是指向全局对象(在浏览器中该对象是window)的,但要注意只有在非严格模式下,默认绑定是指向全局对象(window)的,若是严格模式,直接调用函数里的 this 会指向 undefined

2. 隐式绑定

函数是否是通过对象方法的方式进行调用,此时this会指向该对象

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

注意,对象属性引用链中只有最后一层会影响调用位置

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" // 最后是以函数直接调用的方式进行调用而非对象方法调用

2022/3/18 今天又踩了一个坑,还是绑定丢失的问题,具体案例是手写 promise

class MyPromise {
  、、、省略部分代码

  constructor(executor) {
    // 问题就发生在这个 executor,这里把 this.resolve 传入,注意是传入而不是通过 this.resolve() 的方式执行
    // 所以如果 resolve 是通过普通函数声明的方法去写,在这里实际相当于默认调用而非方法调用
    executor(this.resolve, this.rejected)
  }
  
  // 注意这里使用箭头函数写的,箭头函数的 this 只与外部函数作用域有关,这样才能正确拿到相关的属性 status
  resolve = (value) => {
    if (this.status === PENDING) {
      this.status = FULFILLED
      this.value = value
    }
  }

  、、、
}

3. 显式绑定

通过call、apply、bind(es6中实现的硬绑定)可以实现显示绑定

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

注意,硬绑定的this无法再被优先级小于等于它的规则修改

call、apply、bind 的相同和不同

  • call 和 apply 都会调用函数,bind 则不会调用函数
  • call 和 bind 传递参数时是以 arg1,arg2 ... 这样的形式传参,apply 则是以数组的形式传参,如何进行记忆,call 可以理解为打电话,打电话肯定是给单个个体打,所以传入参数的形式是一个一个的传,而不是以数组的形式传

4. new绑定

new绑定会将this绑定到新创建的对象上

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

需要注意,上面代码中的new foo(2)不同于其他语言的调用构造函数创建类的实例对象,在js中,实际上并不存在“构造函数”这种说法,有的只是函数的构造调用new foo()的形式)

绑定规则优先级

优先级并非先调用谁再调用谁的意思,而是当一个函数有多种绑定规则可以使用时,它只会调用优先级最高的

优先级从高到低: new > 显式 > 隐式 > 默认

默认绑定优先级最低无需多说,下面主要贴一下隐式、显式、new优先级判定的代码

隐式与显式的判断

function foo() {
  console.log(this.a);
}
var obj1 = {
  a: 2,
  foo: foo
};
var obj2 = {
  a: 3,
  foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call(obj2); // 3
obj2.foo.call(obj1); // 2

显式与new的判断

正常情况下,call、apply都无法与new一起使用,但可以通过硬绑定和new进行判断

function foo(something) {
  this.a = something;
}
var obj1 = {};
var bar = foo.bind( obj1 );
bar( 2 ); // 让foo此时指向obj1
console.log( obj1.a ); // 2
var baz = new bar(3); // new修改了this指向,指向新创建出来的foo对象,并将该新创建的foo对象的a赋值为3
console.log( obj1.a ); // 2,因为上面this指向新的对象,所以obj1的a没有再被修改
console.log( baz.a ); // 3

通过上面的代码就可以得出new优先级要高于显式绑定

绑定规则的例外

掌握绑定规则和优先级的判断足以分辨出大多数情况下this的指向问题,但总有一些情况是意想不到的。

1. 被忽略的this

当在call、apply、bind函数中传入undefined或null时,传入的值会被忽略,此时会使用默认的绑定规则

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

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 的返回值是目标函数的引用,所以在这里相当于函数直接调用

3. 箭头函数

箭头函数并不适用于以上的四种规则,它是根据声明所在外层(函数或者全局)作用域来决定this指向,外层指哪箭头函数就指哪,所以判断箭头函数的this就转化成了判断其外部函数this绑定的问题

在这里可以说明一下箭头函数和普通函数的 this 指向的区别了,箭头函数的 this 在定义时或者说声明时就被确定了,普通函数的 this 则与调用的方式有关。

2022.8.15 更新 注意这里的外层作用域是指函数或者全局作用域,对于对象的那种块级作用域是无效的,下面用一段代码说明这个问题,下面我也重新补充了

var name = '123'
var obj = {
  name: '456',
  getName: () => {
    console.log(this.name);
  }
}
obj.getName() // 123,此时外层作用域为全局作用域,别跟隐式绑定搞混了

需要注意

  1. 箭头函数中的this无法通过 call apply bind 方法进行修改,因为从本质上来讲,箭头函数没有自己的 this,它的 this 继承自作用域链的上一层的 this,new也不行(正常情况下以new fn()的方式调用函数会将this绑定到这个新的对象上,但是箭头函数无法使用new进行构造调用,所以new不行)。
// 通过 bind 进行修改,结果等价于 call apply 这里就不再一一举例
var des = () => {
  console.log('我执行了 ==', this)
}
var a = {
  name: '大傻子'
}

var bar = des.bind(a)
des() // window
bar() // window,这就说明结论是正确的 
  1. 若存在嵌套函数关系,那么箭头函数的this只与直接包含它的函数有关
    function foo() {
      setTimeout(() => {
        // 这里的 this 在此法上继承自 foo()
        console.log(this.a);
      }, 100);
    }
    
    function foo2() {
      foo()
    }
    var obj1 = {
      a: 2
    };
    var obj2 = {
      a: 3
    };
    var a = 1
    foo.call(obj1) // 2
    foo2.call(obj1) // 1,这里绑定的是 foo2,foo 的调用是默认绑定,所以为 1
    ···
    
  2. 因为箭头函数不适用绑定规则,所以若对象方法使用箭头函数去声明,箭头函数中的this绑定仍遵循自己的原则,即只与外层作用域有关
    var name = '123'
    var obj = {
      name: '456',
      getName: () => {
        console.log(this.name);
      }
    }
    obj.getName() // 123,此时外层作用域为全局作用域,别跟隐式绑定搞混了
    
    再看一个例子
    var a = {
      name: 'xiaohong',
      aa() {
        console.log(this.name);
      },
      bb() {
        let bbb = new Baby(() => {
          // 指向 a 对象,说明 this 只与声明时的外层作用域有关
          console.log(this);
        })
      }
    }
    function Baby(executor) {
      this.name = 'baby'
      executor()
    }
    a.bb()