目录
- 1、核心
- 2、绑定规则
- 3、绑定例外
- 4、优先级
- 5、箭头函数
- 6、总结
- 7、面试题
- 8、参考书籍、视频
序
很久没更新文章了,起因是源于gpt,这东西太强大了,对博客造成了很大的冲击。后来想想,有些东西总结下来是对自己的沉淀,遂重新提起笔来...
2021年的时候,刚开始写ReactNative,当时最苦恼就是this的绑定的逻辑。对于this我最初的理解就是它本身,和之前OC中的self大概是一样的吧。开始的时候遇到个例,发现都乱了,可能是A的方法调用到B的方法上了,B调用刷新可能刷到C上了...
后来我发现了一个很省事的方法,直接用ES6箭头函数就没事儿了,或者在初始化的时候直接用bind方法先绑定上也不会出现啥问题,用了大半年,不亦乐乎。
直到我发现一本用了大篇幅讲 this 的书 《你不知道的JavaScript》,又搜到一个叫codewhy的前端讲师的 高级JS视频,才算彻底把这个搞明白了。
书上有一段话我觉得写的特别好,也送给正在读博客的你
遇到这样的问题时,许多开发者并不会深入思考为什么 this 的行为和预期的不一致,也不会试图回答那些很难解决但却非常重要的问题。他们只会回避这个问题并使用其他方法来达到目的。
从某种角度来说这个方法确实“解决”了问题,但可惜它忽略了真正的问题——无法理解 this 的含义和工作原理——而是返回舒适区。
一、核心
1、函数在调用时,JavaScript会默认给this绑定一个值;
2、this的绑定和定义的位置(编写的位置)没有关系;
3、this的绑定和调用方式以及调用的位置有关系;
4、this是在运行时被绑定的;
二、绑定规则
- 2.1、默认绑定
- 函数直接调用(不绑定任何对象或采用对象引用方式调用)
- this指向: 严格模式-> window;非严格模式-> undefined
- 2.1.1、简单函数调用
function foo() {
console.log(this); // window / undefined
}
foo();
- 2.1.2、函数调用链
function foo1() {
console.log(this); // window / undefined
foo2();
}
function foo2() {
console.log(this); // window / undefined
foo3();
}
function foo3() {
console.log(this); // window / undefined
}
foo();
- 2.1.3、将函数作为参数,传入到另一个函数中
function foo(func) {
func();
}
function foo1() {
console.log(this); // window / undefined
}
foo(foo1);
- 2.2、隐式绑定
- 通过某个对象发起的函数调用
- 隐式绑定丢失:应用默认绑定
- 2.2.1、通过对象调用函数
function foo() {
console.log(this); // obj对象
}
var obj = {
name:"qiangzi",
foo: foo
}
obj.foo();
- 2.2.2、对象属性引用链
function foo() {
console.log(this); // obj1对象
}
var obj1 = {
name:"qiangzi-1",
foo: foo
}
var obj2 = {
name:"qiangzi-1",
obj1: obj1
}
obj2.obj1.foo();
- 2.2.3、隐式绑定丢失(应用默认绑定)
function foo() {
console.log(this); // window / undefined
}
var obj1 = {
name: "qinagzi-1",
foo: foo
}
// 将obj1的foo赋值给foo2
var foo2 = obj1.foo;
foo2();
- 2.3、显示绑定
在分析 '隐式绑定' 时,必须在一个对象内部包含一个只想函数的属性,通过这个属性间接引用函数,从而把this间接(隐式)绑定到这个对象中。
如果不想在对象内部包含函数引用,同是又希望对象上强制调用函数,该如何做呢?
可以使用函数的 call(..) 和 apply(..) 方法,在调用函数同时,会将this绑定到这个传入的对象上。
直接指定 this 的绑定对象,称为 显示绑定。
call(..) 和 apply(..) ,解决之前 2.2.3 绑定丢失问题,可以使用 bind 硬绑定
- 2.3.1、call、apply
function foo() {
console.log(this);
}
var obj1 = {
name:"qiangzi-1",
foo: foo
}
var obj2 = {
name:"qiangzi-2",
foo: foo
}
foo.call(obj1); // obj1对象
foo.apply(obj2); // obj2对象
* 从this绑定的角度来说,call和apply函数效果是一样的,区别体现在其他参数上。
- 2.3.2、硬绑定:显示绑定变种(解决绑定丢失问题),创建一个包裹函数,内部手动调用显示绑定
function foo() {
console.log(this);
}
var obj = {
name: "qiangzi"
}
var bar = function() {
foo.call(obj);
}
bar(); // obj
setTimeout(bar, 100); // obj
// 硬绑定的bar不可能在修改它的this
bar.call(window); // obj
- 2.3.3、bind函数(包裹函数的应用)
function foo(something) {
console.log( this.a, something);
return this.a + something;
}
var obj = { a:2 };
var bar = function() {
return foo.apply( obj, arguments );
};
var b = bar( 3 ); // 2 3
console.log( b ); // 5
- 2.4、new绑定
使用 new 来调用函数,会自动执行下面的操作
- 创建(或者说构造)一个全新的对象。
- 这个新对象会被执行 [[ 原型 ]] 连接。
- 这个新对象会绑定到函数调用的 this。
- 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。
// 创建Person
function Person(name) {
console.log(this); // Person {}
this.name = name; // Person {name: "qiangzi"}
}
var p = new Person("qiangzi");
console.log(p);
三、绑定例外
- 3.1、忽略显示绑定
在显示绑定中,传入一个null或undefined,这个显示绑定会被忽略,使用默认规则
function foo() {
console.log(this);
}
var obj = {
name: "qiangzi-01"
}
foo.call(obj); // obj对象
foo.call(null); // window
foo.call(undefined); // window
var bar = foo.bind(null);
bar(); // window
- 3.2、间接函数引用
创建一个函数的 间接引用,这种情况使用默认绑定规则。
- 3.2.1、值赋值结果:(num2 = num1)的结果是num1的值;
var num1 = 100;
var num2 = 0;
var result = (num2 = num1);
console.log(result); // 100
- 3.2.2、函数赋值结果:赋值(obj2.foo = obj1.foo)的结果是foo函数;函数直接调用,默认绑定。
function foo() {
console.log(this);
}
var obj1 = {
name: "obj1",
foo: foo
};
var obj2 = {
name: "obj2"
}
obj1.foo(); // obj1对象
(obj2.foo = obj1.foo)(); // window
四、优先级
new绑定 > 显示绑定(bind)> 隐式绑定 > 默认绑定
new绑定和call、apply是不允许同时使用的,所以不存在谁的优先级更高
4.1、默认绑定的优先级是四条规则中最低的
- 因为存在其他规则时,就会通过其他规则的方式来绑定this;
4.2、显示绑定 > 隐示绑定
function foo() {
console.log(this);
}
var obj1 = {
name: "obj1",
foo: foo
}
var obj2 = {
name: "obj2",
foo: foo
}
// 隐式绑定
obj1.foo(); // obj1
obj2.foo(); // obj2
// 隐式绑定和显示绑定同时存在(根据打印结果,说明显式绑定优先级更高)
obj1.foo.call(obj2); // obj2,
obj2.foo.call(obj1); // obj1,
4.3、new绑定 > 隐式绑定
function foo() {
console.log(this);
}
var obj = {
name: "why",
foo: foo
}
new obj.foo(); // foo对象, 说明new绑定优先级更高
五、箭头函数
箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决定this。
思考:为什么在setTimeout的回调函数中可以直接使用this呢?
- 5.1、箭头函数
function foo() {
return () => {
console.log(this.a);
};
}
let obj1 = {
a: 2
};
let obj2 = {
a: 22
};
let bar = foo.call(obj1); // foo this指向obj1
bar.call(obj2); // 输出2🌟🌟🌟🌟 这里执行箭头函数 并试图绑定this指向到obj2
- 5.2、箭头函数-setTimeout 1
var obj = {
data: [],
getData: function() {
setTimeout(() => { // 箭头函数向上层作用域查找, getData在调用时被绑定到了obj
var res = ["abc", "cba", "nba"];
this.data.push(...res);
}, 1000);
}
}
obj.getData();
- 5.2、箭头函数-setTimeout 2
var obj = {
data: [],
getData: () => {
setTimeout(() => {
console.log(this); // 箭头函数向上层作用域查找, getData还是箭头函数,再向上找到全局 window
}, 1000);
}
}
obj.getData();
六、总结
判断一个运行中函数的 this 绑定,直接找到这个函数的调用位置,顺序应用下面这四条规则来判断 this 的绑定对象。
1、由 new 调用:绑定到新创建的对象。
2、由 call 或者 apply(或者 bind)调用:绑定到指定的对象。
3、由上下文对象调用:绑定到那个上下文对象。
4、默认:在严格模式下绑定到 undefined,否则绑定到全局对象。
- 上下文对象调用
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
七、面试题
这些面试题来源于 CodeWhy 的公众号,虽然有些故意为之,但是也不失为检测对this绑定的理解程度,盖上答案自己去检测一下看看吧。
- 7.1、面试题一
var name = "window";
var person = {
name: "person",
sayName: function () {
console.log(this.name);
}
};
function sayName() {
var sss = person.sayName;
sss();
person.sayName();
(person.sayName)();
(b = person.sayName)();
}
sayName();
分析
function sayName() {
var sss = person.sayName;
// 独立函数调用,没有和任何对象关联
sss(); // window
// 关联
person.sayName(); // person
(person.sayName)(); // person
(b = person.sayName)(); // window
}
- 7.2、面试题二
var name = 'window'
var person1 = {
name: 'person1',
foo1: function () {
console.log(this.name)
},
foo2: () => console.log(this.name),
foo3: function () {
return function () {
console.log(this.name)
}
},
foo4: function () {
return () => {
console.log(this.name)
}
}
}
var person2 = { name: 'person2' }
person1.foo1();
person1.foo1.call(person2);
person1.foo2();
person1.foo2.call(person2);
person1.foo3()();
person1.foo3.call(person2)();
person1.foo3().call(person2);
person1.foo4()();
person1.foo4.call(person2)();
person1.foo4().call(person2);
分析
// 隐式绑定,肯定是person1
person1.foo1(); // person1
// 隐式绑定和显示绑定的结合,显示绑定生效,所以是person2
person1.foo1.call(person2); // person2
// foo2()是一个箭头函数,不适用所有的规则
person1.foo2() // window
// foo2依然是箭头函数,不适用于显示绑定的规则
person1.foo2.call(person2) // window
// 获取到foo3,但是调用位置是全局作用于下,所以是默认绑定window
person1.foo3()() // window
// foo3显示绑定到person2中
// 但是拿到的返回函数依然是在全局下调用,所以依然是window
person1.foo3.call(person2)() // window
// 拿到foo3返回的函数,通过显示绑定到person2中,所以是person2
person1.foo3().call(person2) // person2
// foo4()的函数返回的是一个箭头函数
// 箭头函数的执行找上层作用域,是person1
person1.foo4()() // person1
// foo4()显示绑定到person2中,并且返回一个箭头函数
// 箭头函数找上层作用域,是person2
person1.foo4.call(person2)() // person2
// foo4返回的是箭头函数,箭头函数只看上层作用域
person1.foo4().call(person2) // person1
- 7.3、面试题三
var name = 'window'
function Person (name) {
this.name = name
this.foo1 = function () {
console.log(this.name)
},
this.foo2 = () => console.log(this.name),
this.foo3 = function () {
return function () {
console.log(this.name)
}
},
this.foo4 = function () {
return () => {
console.log(this.name)
}
}
}
var person1 = new Person('person1')
var person2 = new Person('person2')
person1.foo1()
person1.foo1.call(person2)
person1.foo2()
person1.foo2.call(person2)
person1.foo3()()
person1.foo3.call(person2)()
person1.foo3().call(person2)
person1.foo4()()
person1.foo4.call(person2)()
person1.foo4().call(person2)
分析
// 隐式绑定
person1.foo1() // peron1
// 显示绑定优先级大于隐式绑定
person1.foo1.call(person2) // person2
// foo是一个箭头函数,会找上层作用域中的this,那么就是person1
person1.foo2() // person1
// foo是一个箭头函数,使用call调用不会影响this的绑定,和上面一样向上层查找
person1.foo2.call(person2) // person1
// 调用位置是全局直接调用,所以依然是window(默认绑定)
person1.foo3()() // window
// 最终还是拿到了foo3返回的函数,在全局直接调用(默认绑定)
person1.foo3.call(person2)() // window
// 拿到foo3返回的函数后,通过call绑定到person2中进行了调用
person1.foo3().call(person2) // person2
// foo4返回了箭头函数,和自身绑定没有关系,上层找到person1
person1.foo4()() // person1
// foo4调用时绑定了person2,返回的函数是箭头函数,调用时,找到了上层绑定的person2
person1.foo4.call(person2)() // person2
// foo4调用返回的箭头函数,和call调用没有关系,找到上层的person1
person1.foo4().call(person2) // person1
- 7.4、面试题四
var name = 'window'
function Person (name) {
this.name = name
this.obj = {
name: 'obj',
foo1: function () {
return function () {
console.log(this.name)
}
},
foo2: function () {
return () => {
console.log(this.name)
}
}
}
}
var person1 = new Person('person1')
var person2 = new Person('person2')
person1.obj.foo1()()
person1.obj.foo1.call(person2)()
person1.obj.foo1().call(person2)
person1.obj.foo2()()
person1.obj.foo2.call(person2)()
person1.obj.foo2().call(person2)
分析
// obj.foo1()返回一个函数
// 这个函数在全局作用于下直接执行(默认绑定)
person1.obj.foo1()() // window
// 最终还是拿到一个返回的函数(虽然多了一步call的绑定)
// 这个函数在全局作用于下直接执行(默认绑定)
person1.obj.foo1.call(person2)() // window
person1.obj.foo1().call(person2) // person2
// 拿到foo2()的返回值,是一个箭头函数
// 箭头函数在执行时找上层作用域下的this,就是obj
person1.obj.foo2()() // obj
// foo2()的返回值,依然是箭头函数,但是在执行foo2时绑定了person2
// 箭头函数在执行时找上层作用域下的this,找到的是person2
person1.obj.foo2.call(person2)() // person2
// foo2()的返回值,依然是箭头函数
// 箭头函数通过call调用是不会绑定this,所以找上层作用域下的this是obj
person1.obj.foo2().call(person2) // obj
八、参考:
- 视频:《codewhy-js高级》
- 书籍:《你不知道的JavaScript》
- 博客:你不知道的js中关于this绑定机制的解析[看完还不懂算我输]
- 博客:前端面试之彻底搞懂this指向
网友评论