你是不是写函数时总搞不清 this 指向谁?明明在对象里定义的方法,调用时 this 就变成 window 了?或者用回调函数时,想访问外层的 this 却拿不到,只能用 var that = this 这种 “土办法”?
这事儿的核心其实是两个东西:作用域链和 this 绑定规则。之前聊变量提升时提过作用域,但没细说它和 this 的关系,今天咱们就把这俩 “难兄难弟” 掰扯明白,以后再遇到 this 相关的 bug,保准你一分钟定位问题。
先复习下作用域链,这是理解 this 的基础。简单说,作用域链就是函数查找变量的 “路线图”。当函数要访问一个变量时,会先在自己的局部作用域里找,找不到就去定义时的外层作用域找,一层一层往上爬,直到全局作用域,再找不到就报 ReferenceError。
比如这段代码:
let globalVar = "全局变量";
function outer () {
let outerVar = "外层变量";
function inner () {
console.log (outerVar); // 能找到外层变量
console.log (globalVar); // 能找到全局变量
}
inner ();
}
outer ();
inner 函数的作用域链就是:inner 局部作用域 → outer 作用域 → 全局作用域。但作用域链管的是变量查找,this 管的是 “函数调用时的上下文”,两者完全不是一回事,很多人就是把这俩搞混了才踩坑。
小索奇认为,搞懂 this 的关键就一个:this 的指向不是在定义时决定的,而是在调用时决定的。谁调用了函数,this 就指向谁(特殊情况除外)。咱们分最常见的 4 种场景一个个说。
第一种,默认绑定。就是函数直接调用,没有任何前缀,这时候 this 默认指向全局对象(浏览器里是 window,Node 里是 global)。比如:
function showThis () {
console.log (this); // 指向 window
}
showThis ();
但如果在严格模式下,默认绑定会失效,this 会变成 undefined:
"use strict";
function strictShow () {
console.log (this); //undefined
}
strictShow ();
这是个很容易忽略的点,有时候代码在非严格模式下好好的,加了严格模式就报错,很可能是 this 变成 undefined 导致的。
第二种,隐式绑定。函数通过对象的属性调用,比如 obj.fn (),这时候 this 就指向这个对象。比如:
const person = {
name: "小索奇",
sayName () {
console.log (this.name); // 指向 person,打印 “小索奇”
}
};
person.sayName ();
但隐式绑定有个巨坑:函数赋值后调用,this 会丢失。比如:
const say = person.sayName;
say (); // 此时 this 指向 window,打印 undefined
因为 say 变量只是拿到了函数本身,调用时没有对象前缀,就触发了默认绑定。很多人在给 DOM 事件绑定方法时会踩这个坑,比如把对象的方法直接传给 onclick,调用时 this 就变成了 DOM 元素,不是原来的对象。
第三种,显式绑定。就是用 call、apply、bind 这三个方法强制指定 this 的指向,这也是解决 this 丢失的常用办法。比如刚才的例子,用 bind 就能固定 this:
const boundSay = person.sayName.bind (person);
boundSay (); // 打印 “小索奇”
call 和 apply 的区别是传参方式不同,call 是逐个传参,apply 是传数组:
function add (a, b) {
console.log (this.name + "计算:" + (a + b));
}
add.call (person, 1, 2); // 小索奇计算:3
add.apply (person, [1, 2]); // 小索奇计算:3
bind 和它们的区别是,bind 不会立刻执行函数,而是返回一个绑定了 this 的新函数,call 和 apply 会立刻执行。
第四种,new 绑定。用 new 关键字调用构造函数时,this 会指向新创建的实例对象。比如:
function Person (name) {
this.name = name; //this 指向 new 出来的实例
}
const p = new Person ("即兴小索奇");
console.log (p.name); // 即兴小索奇
new 操作做了四件事:创建空对象、把 this 指向空对象、执行构造函数代码、返回这个对象。记住这四步,就懂 new 绑定的原理了。
除了这四种,ES6 的箭头函数是个 “异类”—— 它根本没有自己的 this。箭头函数的 this 是在定义时就绑定好的,继承自外层作用域的 this,之后不管怎么调用,this 都不会变。
比如解决回调函数里的 this 丢失问题,用箭头函数就特方便:
const obj = {
data: [1, 2, 3],
process () {
// 普通回调函数,this 指向 window
this.data.forEach (function (item) {
console.log (this.data); //undefined,this 丢失了
});
// 箭头函数,this 继承外层的 obj
this.data.forEach (item => {
console.log (this.data); // [1,2,3],正确
});
}
};
obj.process ();
小索奇在写 React 类组件时,经常用箭头函数定义方法,就是为了避免手动绑定 this,比如:
class MyComponent extends React.Component {
// 箭头函数自动绑定 this 为组件实例
handleClick = () => {
this.setState ({ count: 1 });
};
render () {
return 点击;
}
}
这比在 constructor 里写 this.handleClick = this.handleClick.bind (this) 清爽多了。
总结下 this 的绑定优先级:new 绑定 > 显式绑定(call/apply/bind)> 隐式绑定 > 默认绑定。记不住的话,遇到 this 问题就按这个顺序排查,先看是不是 new 调用,再看是不是显式绑定,以此类推。
说到这儿,你是不是也踩过 this 的坑?比如在 setTimeout 回调里用 this 访问不到对象属性,或者把对象方法传给事件监听后 this 变了?
最后留个小问题:下面这段代码里,最后打印的 this.name 是啥?
const person = {
name: "小索奇",
sayName: () => {
console.log (this.name);
}
};
const anotherPerson = { name: "即兴小索奇" };
person.sayName.call (anotherPerson);
提示一下,箭头函数的 this 是绑定死的哦!欢迎在评论区写下你的答案~
我是【即兴小索奇】,点击关注,后台回复 领取,获取更多相关资源
转载自CSDN-专业IT技术社区
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/m0_64880608/article/details/151759181