原文 Understanding JavaScript Function Invocation and “this”

理解函数调用与’this’(翻译)

这么些年来,我见过非常多的关于JavaScript函数调用的困惑。尤其是,许多人对于函数调用时this的语义一直无法搞清。

核心

首先,让我们来看函数调用的原始核心,其被称为call的函数内部的一个方法,这个call方法也是相当直截了当。

  1. 产生一个参数列表(argList
  2. 参数列表的第一个值就是thisValue
  3. this的值赋给thisValue,然后使用argList作为参数列表调用函数

例如:

1
2
3
4
5
function hello(thing) {
console.log(this + " says hello " + thing);
}

hello.call("Yehuda", "world") //=> Yehuda says hello world

正如你所见到的,我们调用hello方法,并且传入'Yehuda'作为this的值和一个字符串'world'。这是最原始的javscript函数的调用方式,你能将任何其他的函数调用想象成这种方式的缩略形式。
^[在es5标准中,call方法以另一种形式描述,更加底层与原始,但他(译注:当前所指的call方法)是在原始基础上包了薄薄的一层,如果你想要了解更多关于这方面的只是,请看这篇文章。]

简单的函数调用

显然,在任何调用函数的时候使用call方法是非常让人烦恼的,JavaScript允许我们直接调用函数,当我们这样调用的时候,调用过程如下:

1
2
3
4
5
6
7
8
9
function hello(thing) {
console.log("Hello " + thing);
}

// this:
hello("world")

// desugars to:
hello.call(window, "world");

这种行为在es5的严格模式中有所改变:

1
2
3
4
5
// this:
hello("world")

// desugars to:
hello.call(undefined, "world");

这种短形式可以解释为:函数调用类似于 fn(…args)fn.call(window [ES5-strict: undefined], …args)是相同的。
注意在立即执行函数中也是成立的:(function() {})()(function() {}).call(window [ES5-strict: undefined)是相同的。
^[其实我这里有点误解了,es5标准中指出,undefined值是一直传给thisValue的,在非严格模式下会将全局对象window传入]

成员函数

另一个常见的调用函数的场景是作为对象的成员(==person.hello()==),在这种场景下,调用被解释为:

1
2
3
4
5
6
7
8
9
10
11
12
var person = {
name: "Brendan Eich",
hello: function(thing) {
console.log(this + " says hello " + thing);
}
}

// this:
person.hello("world")

// desugars to this:
person.hello.call(person, "world");

要注意的是,在这里,不论hello方法是怎么被赋予给对象的,还记得我们将hello函数单独在外面声明吗,我们可以看看会发生什么。

1
2
3
4
5
6
7
8
9
10
function hello(thing) {
console.log(this + " says hello " + thing);
}

person = { name: "Brendan Eich" }
person.hello = hello;

person.hello("world") // still desugars to person.hello.call(person, "world")

hello("world") // "[object DOMWindow]world"

函数没有单独的this概念,其总是在caller被调用时声明。

使用Function.prototype.bind

因为可以很方便地使用一个引用来改变function中this的指针,人们曾经试图使用闭包的技巧将函数的this值绑定到已有的值。

1
2
3
4
5
6
7
8
9
10
var person = {
name: "Brendan Eich",
hello: function(thing) {
console.log(this.name + " says hello " + thing);
}
}

var boundHello = function(thing) { return person.hello.call(person, thing); }

boundHello("world");

尽管boundHello方法依然解释为boundHello.call(window, "world"),我们仍然能够使用call方法将this值变为我们想要的值。
我们将这个技巧做一下调整:

1
2
3
4
5
6
7
8
var bind = function(func, thisValue) {
return function() {
return func.apply(thisValue, arguments);
}
}

var boundHello = bind(person.hello, person);
boundHello("world") // "Brendan Eich says hello world"

为了理解这里,你只需要了解两个信息。第一,arguments是一个类数组的对象代表所有的传入该方法的参数。第二,apply方法与call方法的原理十分类似,替代的是将列举出来的参数使用一个数组一次性传递进去。
bind方法简单地返回一个新函数,当它被调用时,新的函数简单地调用传递进去的原始函数,并且将this的值做了一下替换,同样传递所有参数。
因为这是一个惯用语法,es5在Function对象上引进了一个新的bind方法代表了如下的行为:

1
2
var boundHello = person.hello.bind(person);
boundHello("world") // "Brendan Eich says hello world"

这在当你传递一个函数作为回调函数时用处最大:

1
2
3
4
5
6
7
8
var person = {
name: "Alex Russell",
hello: function() { console.log(this.name + " says hello world"); }
}

$("#some-div").click(person.hello.bind(person));

// when the div is clicked, "Alex Russell says hello world" is printed

这当然有点笨拙,TC39(针对ECMAScript下一个版本的委员会)继续开发更优雅,仍然向后兼容的解决方案。

PS:I Cheated

在一些场合,我的解释与标准还是有一些出入的地方。最重要的可能就是,我把func.call称作原始的。其实,标准有一个原始的解释(通常认为[[Call]])被func.call[obj.]func()使用。
尽管如此,让我们看看func.call的定义吧

  1. 如果func的IsCallable是false,抛出类型错误异常
  2. 让参数列表变为空列表
  3. 如果方法被调用的时候传入不止一个参数,那么就从左到右插入进argList
  4. 执行函数的[[Call]]方法的,并传入thisArgargList中剩下的参数,然后返回

正如你所见的,这个定义是绑定到[[Call]]操作上的非常简单地JavaScript原生语言。
如果你看了整个函数调用的定义,前面七部操作定义了thisValueargList,最后一步就是讲thiaValueargList传入[[Call]]方法,并返回结果。
一旦argList与thisValue值得确定,函数调用总是能按照预期进行。
我在call的原始性上有些许误导,但是必要的含义是相同的,具体可参见第一篇引用的标准文章。
未完待尽……(作者注)