原文发表在: holmeshe.me , 本文是汉化重制版。
还记得早先用ajax胡乱做项目的时候踩过好多坑,然后对JS留下了“非常诡异”的印象。最近换了一个工作,工作语言就是JS。然后发现这个语言真不得了,前面后面都能干,基本成了全栈的同义词。所以刚好就趁这个机会系统学习一下这个语言。因为是给程序员看的,这个系列不会讲基本的“if-else”,循环,或者面向对象。相反,我会关注差异,希望能给您在Pull Request里走查代码式的学习体验!
本篇我会讨论这个难懂的, 无数程序英雄都踩坑的this。
先来看看JS里面的
this是啥
打上码:
var hero = {
name :"Hulk",
motto :"Rahhhhh!!",
speak:function () {
alert(this.name + ":" + this.motto);
}
}
hero.speak();
运行结果:
Hulk:Rahhhhh!!
this的在运行时的值
上面打上的其实是一个典型的面向对象的代码片段。在这个🌰 里,我们用this来指向函数的调用者(一个对象)。但JS不是一个严格的面向对象,所以 this 的行为会不同于其他的语言。说明白点,this 会指向 "运行时上下文"。***上下文是什么鬼?打上另一块码,顺便强行科普一句英文,老铁戳心:Right in the feels!。
打上码:
var name = "Iron man", motto = "Right in the feels!";
function speak() {
alert(this.name + ":" + this.motto);
}
var hero = {
name :"Hulk",
motto :"Rahhhhh!!",
speak:speak //here we assign the function to the member method
}
hero.speak();
speak();// it reads the two variables in the global context(window)
运行结果:
Hulk:Rahhhhh!!
Iron man:Right in the feels!
在上面的代码中,第一个 speak() 是在 hero 的上下文里被调用的,而第二个是在 window。所以 this 会被相应的赋值。证明一下,如果我们直接对 window 调用 speak(),像这样 window.speak() ,我们可以得到和🌰 中第二次调用一样的结果。这是因为 this 解引了全局变量 name。
但是,没人这么写代码吧,所以上面举的并不是现实生活中碰得到的坑。下面我们看一些更真实的🌰 。
问题 1 - 回调函数
最常见的导致 "上下文混乱" (这里,我并不太想用上下文切换,所以随便造了个词)是把回调函数当作变量赋值(一般情况是传参)。
打上码:
var name = "Iron man", motto = "Right in the feels!";
function thinkBeforeSpeak(callback) {
setTimeout(callback, 3000);
}
var hero = {
name :"Hulk",
motto :"Rahhhhh!!",
speak :function speak() {
alert(this.name + ":" + this.motto);
}
}
thinkBeforeSpeak(hero.speak);
运行结果:
Iron man:Right in the feels!
上面的🌰 中,被测试的那个函数被赋值给其他变量两次,所以函数的上下文又变成 window...然后老铁的心脏又受不了了。
解决办法 1,bind()
var name = "Iron man", motto = "Right in the feels!";
function thinkBeforeSpeak(callback) {
setTimeout(callback, 3000);
}
var hero = {
name :"Hulk",
motto :"Rahhhhh!!",
speak :function speak() {
alert(this.name + ":" + this.motto);
}
}
thinkBeforeSpeak(hero.speak.bind(hero));
运行结果:
Hulk:Rahhhhh!!
你可能已经注意到,下面这行是关键点:
hero.speak.bind(hero);
这行把 this 给绑定到指定对象,绿巨人 (Hulk)。
解决办法2,apply()
var name = "Iron man", motto = "Right in the feels!";
function thinkBeforeSpeak(callback) {
setTimeout(callback, 3000);
}
var hero = {
name :"Hulk",
motto :"Rahhhhh!!",
speak :function speak() {
alert(this.name + ":" + this.motto);
}
}
thinkBeforeSpeak(hero.speak.apply(hero, []));
运行结果:
Hulk:Rahhhhh!!
apply() 和 bind() 差不多,然而 apply() 还有一个额外的参数用于接收给回调函数的传参。
其实还有一个 call(),它的额外参数是一套可变参数(这个词用英语其实会准确些,variable number arguments)。这个函数和 apply() 太像,不举例。
问题 2 - 嵌套函数
另一个常见的坑是在使用嵌套函数(一个函数定义在另一个里面)的时候。这个时候嵌套函数里的 this 不会指向外围函数(假设外围函数是个成员函数)的调用对象。
打上码:
var name = "Iron man", motto = "Right in the feels!";
var hero = {
name :"Hulk",
motto :"Rahhhhh!!",
speak :function speak() {
function innervoice() {
alert(this.name + ":" + this.motto);
}
innervoice();
}
}
hero.speak();
运行结果:
Iron man:Right in the feels!
我也是。
this 又变成 window 了。好像每次JS引擎一懵*,就把this设置成windows了事。
解决办法,箭头函数=>
=>是 ES6 引进的,它能自动绑定 this:
var name = "Iron man", motto = "Right in the feels!";
var hero = {
name :"Hulk",
motto :"Rahhhhh!!",
speak :function speak() {
var fn = () => { alert(this.name + ":" + this.motto); }
fn();
}
}
hero.speak();
运行结果:
Hulk:Rahhhhh!!
这次的结果在运行时总算对了。
顺带一提,()=>{...;} 表示一个箭头函数,而且这个函数没参数。
啥?还可以用 that 来解决?还是不要了。
问题3 - 构造函数
在实际用 this 的时候,构造函数也是个天坑。但是要理解这个问题需要对 JS 的面向对象基础有一定了解,所以我把这个问题放到后面的文章来讨论。
今天我们讨论了各种拯救老铁的姿势,希望下次你在遇到凌乱的this问题时,也可以一样帮老铁,不加班。
好,今天先写到这。如果您觉得这篇不错,请关注本专栏。也可以去Medium上随意啪啪啪我的其他文章。感谢阅读!👋