JavaScript 方法调用和 this 解析
/ / 点击 / 阅读耗时 11 分钟一直以来,我对 JavaScript 的方法调用存有困惑。尤其是很多人一直抱怨,方法中的 this 语义含糊不清。
在我看来,只要理解了方法调用原语就可以解开这些疑惑,其他所有的方法调用都可以认为是原语上的语法糖。事实上,ECMAScript 规范就是这么做的,这篇文章可以认为是简化版的规范说明,基本思路是一样的。
原语
方法调用的原语是 call 这个方法:
- 接受一个参数列表 
argList - 第一个参数是 
thisValue - 触发函数调用,设置 
thisValue为this, 剩余的参数作参数列表 
举例:
1  | function hello(thing) {  | 
你会发现,this 的值被设置为 Yehuda, 只有一个参数 thing 被设置为 “world” 。这就是 JavaScript 的方法调用原语。可以认为其他的方法调用都是这种形式的语法糖。
注:在 ES5规范 中,call 被当做更底层的原语,但是仅仅是一层比较浅的封装,因此这里做了简化。
简单的方法调用
如果一直使用 call 来做函数调用,会显得很麻烦,因此 JavaScript 允许我们使用 hello("world") 这种语法去做函数调用,JavaScript 会帮我们去糖:
1  | function hello(thing) {  | 
在 ECMAScript 5 中,如果使用 strict mode,行为会有所改变:
1  | // this:  | 
更简短的描述可以认为 fn(...args) 和 fn.call(window [ES5-strict: undefined], ...args) 的调用是一样的。
内部函数的声明也是这样,诸如 (function() {})() 的函数声明和 (function() {}).call(window [ES5-strict: undefined) 也相同。
注:事实上,这里有一点不是很准确,ECMAScript 5 规范中声明了 undefined 一定会被传递,但是当方法在非严格模式中被调用的时候,应该把 thisValue 的值设置为全局变量,这可以让严格模式下的调用者避免破坏已存在的非严格模式的库。
成员方法
另一种常见的函数调用形式就是作为成员函数被调用,person.hello()。这种情景下,函数调用会变成如下形式:
1  | var person = {  | 
请注意,hello 函数作为谁的成员并不重要,我们生命 hello 为单独的函数,下面看一下如何动态绑定对象:
1  | function hello(thing) {  | 
注意,hello 并不会一直持有 this 的绑定,他总是根据调用者的调用形式,在调用的时候动态设定的。
使用 Function.prototype.bind
有时候绑定 this 为固定值是非常常见和方便的,因此有些时候人们会利用闭包来实现 this 值的绑定。
1  | var person = {  | 
尽管 boundHello 仍然被去糖,变成 boundHello.call(window, "world") 形式的调用,我们曲线救国,重新绑定了 this 的值为我们想要绑定的对象。
我们可以微调一下,让这种调用更通用:
1  | var bind = function(func, thisValue) {  | 
为了理解这种调用,需要注意两点:
arguments是一个数组,代表了函数调用的所有参数apply和call基本一致,除了他接受的参数是数组形式,而不是需要一个个的列举出来
bind 函数返回了一个新的函数。当函数被调用的时候,bind 仅仅是触发了参数中传递进来的函数,this 的值也是通过第二个参数重新绑定。
既然这种形式得到大家的共识,因此 ES5 在 Function 对象上添加了 bind 函数:
1  | var boundHello = person.hello.bind(person);  | 
当把一个纯函数作为 callback 传递的时候是非常有用的:
1  | var person = {  | 
这种形式并不优雅,因此 TC39(一个委员会致力于下一代 ECMAScript 的标准)一直在研究一种方案,技能向前兼容,又足够优雅。
jQuery
jQuery 大量使用了匿名回调,内部调用了 call 来设置 callback 的 this 为更有用的值。例如,jQuery 触发 callback 的时候把设置了事件监听的元素作为 this 的值,而不是不经过处理,直接把 window 作为 this 的值。
这非常有用,因为匿名回调函数里的默认 this 的值通常并没有什么用,对于 JavaScript 的初学者来说,this 的值经常容易造成困难并且很难解释清楚。
如果你掌握了把方法调用去糖为 func.call(thisValue, ...args) 的基本法则,你将会如鱼得水。
PS: 偷梁换柱
在一些地方,我简化了 JavaScript 规范中的繁文缛节。比如,我把 func.call 称作原语。实际上,规范中, 对于 func.call 和 [obj.]func() 有原语的概念([[Call]])。
然而,我们可以看一下 func.call 的定义:
- 如果 
IsCallable(func)为 false, 抛出TypeError异常 argList设置为空列表- 如果方法被调用的参数多余一个,从 
arg1开始,从左至右的顺序组成argList - 返回 func 内部的 
[[Call]]的调用结果,把thisArg作为this的值,argList作为参数列表 
显而易见,这个定义其实就是 JavaScript 对原语 [[Call]] 的绑定。
如果你浏览一下函数触发的定义,前面几步是设置 thisValue 和 argList, 最后一步是返回 func 内部的 [[Call]] 的调用结果,把 thisArg 作为 this 的值,argList 作为参数列表。
一旦 thisValue 和 argList 确定下来,含义都是一样的。
我把 call 称作原语其实是偷梁换柱,正如前面所讲,这和规范中的描述其实都是一致的。
PS:有一些情况我并没有覆盖到,比如使用 with 关键字的情况。