前端进阶手册:全方位解读 JavaScript 中的 this

前言

this 关键字是 JavaScript 中最复杂的机制之一。它是一个很特别的关键字,被自动定义在 所有函数的作用域中。但是即使是非常有经验的 JavaScript 开发者也很难说清它到底指向 什么。

任何足够先进的技术都和魔法无异。    

实际上,JavaScript 中 this 的机制并没有那么先进,但是开发者往往会把理解过程复杂化, 毫无疑问,在缺乏清晰认识的情况下,this 对你来说完全就是一种魔法。

“this”是沟通过程中极其常见的一个代词。所以,在交流过程中很难区分 我们到底把“this”当作代词还是当作关键字。清晰起见,我总一直使用 this 表示关键字,使用“this”或者 this 来表示代词。

1.为什么要用this

如果对于有经验的 JavaScript 开发者来说 this 都是一种非常复杂的机制,那它到底有用在 哪里呢?真的值得我们付出这么大的代价学习吗?的确,在介绍怎么做之前我们需要先明 白为什么。

下面我们来解释一下为什么要使用 this:

前端进阶手册:全方位解读 JavaScript 中的 this

 

看不懂这段代码?不用担心!我们很快就会讲解。现在请暂时抛开这些问题,专注于为 什么。

这段代码可以在不同的上下文对象(me 和 you)中重复使用函数 identify() 和 speak(), 不用针对每个对象编写不同版本的函数。

如果不使用 this,那就需要给 identify() 和 speak() 显式传入一个上下文对象。

前端进阶手册:全方位解读 JavaScript 中的 this

 

然而,this 提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将 API 设计 得更加简洁并且易于复用。

随着你的使用模式越来越复杂,显式传递上下文对象会让代码变得越来越混乱,使用 this 则不会这样。当我们介绍对象和原型时,你就会明白函数可以自动引用合适的上下文对象 有多重要。

2.误解

我们之后会解释 this 到底是如何工作的,但是首先需要消除一些关于 this 的错误认识。

太拘泥于“this”的字面意思就会产生一些误解。有两种常见的对于 this 的解释,但是它 们都是错误的。

2.1指向自身 

人们很容易把 this 理解成指向函数自身,这个推断从英语的语法角度来说是说得通的。

那么为什么需要从函数内部引用函数自身呢?常见的原因是递归(从函数内部调用这个函 数)或者可以写一个在第一次被调用后自己解除绑定的事件处理器。

JavaScript 的新手开发者通常会认为,既然函数看作一个对象(JavaScript 中的所有函数都 是对象),那就可以在调用函数时存储状态(属性的值)。这是可行的,有些时候也确实有 用,但是在本书即将介绍的许多模式中你会发现,除了函数对象还有许多更合适存储状态 的地方。

不过现在我们先来分析一下这个模式,让大家看到 this 并不像我们所想的那样指向函数 本身。

我们想要记录一下函数 foo 被调用的次数,思考一下下面的代码:

前端进阶手册:全方位解读 JavaScript 中的 this

 

console.log 语句产生了 4 条输出,证明 foo(..) 确实被调用了 4 次,但是 foo.count 仍然 是 0。显然从字面意思来理解 this 是错误的。

执行 foo.count = 0 时,的确向函数对象 foo 添加了一个属性 count。但是函数内部代码 this.count 中的 this 并不是指向那个函数对象,所以虽然属性名相同,根对象却并不相 同,困惑随之产生。

负责的开发者一定会问“如果我增加的 count 属性和预期的不一样,那我增 加的是哪个 count ?”实际上,如果他深入探索的话,就会发现这段代码在 无意中创建了一个全局变量 count,它的值为 NaN。当然, 如果他发现了这个奇怪的结果,那一定会接着问:“为什么它是全局的,为 什么它的值是 NaN 而不是其他更合适的值?”

遇到这样的问题时,许多开发者并不会深入思考为什么 this 的行为和预期的不一致,也不 会试图回答那些很难解决但却非常重要的问题。他们只会回避这个问题并使用其他方法来 达到目的,比如创建另一个带有 count 属性的对象。

前端进阶手册:全方位解读 JavaScript 中的 this

 

// foo 被调用了多少次?console.log( data.count ); // 4

从某种角度来说这个方法确实“解决”了问题,但可惜它忽略了真正的问题——无法理解 this 的含义和工作原理——而是返回舒适区,使用了一种更熟悉的技术:词法作用域。

 

词法作用域是一种非常优秀并且有用的技术。我丝毫没有贬低它的意思。但是如果你仅仅是因为无法猜对 this 的用法,就放弃学习 this 而去使用词法作用域,就不能算是一种很好 的解决办法了。

 

如果要从函数对象内部引用它自身,那只使用 this 是不够的。一般来说你需要通过一个指 向函数对象的词法标识符(变量)来引用它。

 

思考一下下面这两个函数:

前端进阶手册:全方位解读 JavaScript 中的 this

 

第一个函数被称为具名函数,在它内部可以使用 foo 来引用自身。

但是在第二个例子中,传入 setTimeout(..) 的回调函数没有名称标识符(这种函数被称为 匿名函数),因此无法从函数内部引用自身。

 

还有一种传统的但是现在已经被弃用和批判的用法,是使用 arguments. callee 来引用当前正在运行的函数对象。这是唯一一种可以从匿名函数对象 内部引用自身的方法。

 

然而,更好的方式是避免使用匿名函数,至少在需要 自引用时使用具名函数(表达式)。arguments.callee 已经被弃用,不应该再 使用它。

 

所以,对于我们的例子来说,另一种解决方法是使用 foo 标识符替代 this 来引用函数 对象:

 

前端进阶手册:全方位解读 JavaScript 中的 this

然而,这种方法同样回避了 this 的问题,并且完全依赖于变量 foo 的词法作用域。

 

另一种方法是强制 this 指向 foo 函数对象:

 

前端进阶手册:全方位解读 JavaScript 中的 this

 

这次我们接受了 this,没有回避它。

 

2.2它的作用域 

 

第二种常见的误解是,this 指向函数的作用域。这个问题有点复杂,因为在某种情况下它 是正确的,但是在其他情况下它却是错误的。

 

需要明确的是,this 在任何情况下都不指向函数的词法作用域。在 JavaScript 内部,作用 域确实和对象类似,可见的标识符都是它的属性。但是作用域“对象”无法通过 JavaScript 代码访问,它存在于 JavaScript 引擎内部。

 

思考一下下面的代码,它试图(但是没有成功)跨越边界,使用 this 来隐式引用函数的词 法作用域:

 

前端进阶手册:全方位解读 JavaScript 中的 this

 

这段代码中的错误不止一个。虽然这段代码看起来好像是我们故意写出来的例子,但是实 际上它出自一个公共社区中互助论坛的精华代码。这段代码非常完美(同时也令人伤感) 地展示了 this 多么容易误导人。

 

首先,这段代码试图通过 this.bar() 来引用 bar() 函数。这是绝对不可能成功的,我们之 后会解释原因。调用 bar() 最自然的方法是省略前面的 this,直接使用词法引用标识符。

 

此外,编写这段代码的开发者还试图使用 this 联通 foo() 和 bar() 的词法作用域,从而让 bar() 可以访问 foo() 作用域里的变量 a。这是不可能实现的,你不能使用 this 来引用一 个词法作用域内部的东西。

 

每当你想要把 this 和词法作用域的查找混合使用时,一定要提醒自己,这是无法实现的。

 

3.this到底是什么

 

排除了一些错误理解之后,我们来看看 this 到底是一种什么样的机制。

 

之前我们说过 this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调 用时的各种条件。this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。

 

当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包 含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this 就是记录的 其中一个属性,会在函数执行的过程中用到。

 

4.小结

 

对于那些没有投入时间学习 this 机制的 JavaScript 开发者来说,this 的绑定一直是一件非常令人困惑的事。this 是非常重要的,但是猜测、尝试并出错和盲目地从 Stack Overflow 上复制和粘贴答案并不能让你真正理解 this 的机制。

 

学习 this 的第一步是明白 this 既不指向函数自身也不指向函数的词法作用域,你也许被 这样的解释误导过,但其实它们都是错误的。

 

this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。