理解函数调用与’this’(翻译)
这么些年来,我见过非常多的关于JavaScript函数调用的困惑。尤其是,许多人对于函数调用时this
的语义一直无法搞清。
核心
首先,让我们来看函数调用的原始核心,其被称为call
的函数内部的一个方法,这个call
方法也是相当直截了当。
- 产生一个参数列表(
argList
) - 参数列表的第一个值就是
thisValue
- 将
this
的值赋给thisValue
,然后使用argList
作为参数列表调用函数
例如:
1 | function hello(thing) { |
正如你所见到的,我们调用hello
方法,并且传入'Yehuda'
作为this
的值和一个字符串'world'
。这是最原始的javscript函数的调用方式,你能将任何其他的函数调用想象成这种方式的缩略形式。
^[在es5标准中,call
方法以另一种形式描述,更加底层与原始,但他(译注:当前所指的call方法)是在原始基础上包了薄薄的一层,如果你想要了解更多关于这方面的只是,请看这篇文章。]
简单的函数调用
显然,在任何调用函数的时候使用call
方法是非常让人烦恼的,JavaScript允许我们直接调用函数,当我们这样调用的时候,调用过程如下:
1 | function hello(thing) { |
这种行为在es5的严格模式中有所改变:
1 | // this: |
这种短形式可以解释为:函数调用类似于 fn(…args) 与 fn.call(window [ES5-strict: undefined], …args)是相同的。
注意在立即执行函数中也是成立的:(function() {})() 与 (function() {}).call(window [ES5-strict: undefined)是相同的。
^[其实我这里有点误解了,es5标准中指出,undefined值是一直传给thisValue的,在非严格模式下会将全局对象window传入]
成员函数
另一个常见的调用函数的场景是作为对象的成员(==person.hello()==),在这种场景下,调用被解释为:
1 | var person = { |
要注意的是,在这里,不论hello方法是怎么被赋予给对象的,还记得我们将hello函数单独在外面声明吗,我们可以看看会发生什么。
1 | function hello(thing) { |
函数没有单独的this
概念,其总是在caller
被调用时声明。
使用Function.prototype.bind
因为可以很方便地使用一个引用来改变function中this
的指针,人们曾经试图使用闭包的技巧将函数的this
值绑定到已有的值。
1 | var person = { |
尽管boundHello方法依然解释为boundHello.call(window, "world")
,我们仍然能够使用call
方法将this
值变为我们想要的值。
我们将这个技巧做一下调整:
1 | var bind = function(func, thisValue) { |
为了理解这里,你只需要了解两个信息。第一,arguments
是一个类数组的对象代表所有的传入该方法的参数。第二,apply
方法与call
方法的原理十分类似,替代的是将列举出来的参数使用一个数组一次性传递进去。bind
方法简单地返回一个新函数,当它被调用时,新的函数简单地调用传递进去的原始函数,并且将this
的值做了一下替换,同样传递所有参数。
因为这是一个惯用语法,es5在Function
对象上引进了一个新的bind
方法代表了如下的行为:
1 | var boundHello = person.hello.bind(person); |
这在当你传递一个函数作为回调函数时用处最大:
1 | var person = { |
这当然有点笨拙,TC39
(针对ECMAScript下一个版本的委员会)继续开发更优雅,仍然向后兼容的解决方案。
PS:I Cheated
在一些场合,我的解释与标准还是有一些出入的地方。最重要的可能就是,我把func.call称作原始的。其实,标准有一个原始的解释(通常认为[[Call]]
)被func.call
和[obj.]func()
使用。
尽管如此,让我们看看func.call
的定义吧
- 如果func的IsCallable是false,抛出类型错误异常
- 让参数列表变为空列表
- 如果方法被调用的时候传入不止一个参数,那么就从左到右插入进
argList
- 执行函数的
[[Call]]
方法的,并传入thisArg
和argList
中剩下的参数,然后返回
正如你所见的,这个定义是绑定到[[Call]]
操作上的非常简单地JavaScript原生语言。
如果你看了整个函数调用的定义,前面七部操作定义了thisValue
与argList
,最后一步就是讲thiaValue
与argList
传入[[Call]]
方法,并返回结果。
一旦argList与thisValue值得确定,函数调用总是能按照预期进行。
我在call的原始性上有些许误导,但是必要的含义是相同的,具体可参见第一篇引用的标准文章。
未完待尽……(作者注)