重要的写在开头
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,此时外层作用域为全局作用域,别跟隐式绑定搞混了
需要注意
- 箭头函数中的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,这就说明结论是正确的
- 若存在嵌套函数关系,那么箭头函数的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 ··· - 因为箭头函数不适用绑定规则,所以若对象方法使用箭头函数去声明,箭头函数中的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()