JavaScript 上下文环境和作用域,以及 call、apply 和 bind【转载+翻译+整理】
——看到这篇文章,翻译国外的,虽说写得有点矫情,但总体来看,还是相当不错的~
本文内容
- 我在哪儿?你又是谁 ?
- this?
- 用 apply 和 call 掌控上下文环境
- bind 之美
本文将说明上下文环境(Context)和作用域(Scope),首先,掌控上下文环境的两种方法,其次,深入一种高效的方案,它能有效解决我所碰到的 90% 的问题。
作用域是 JavaScript 基础之一,在构建复杂程序时,可能是最令我头痛的东西。记不清,有多少次在函数之间传递控制后忘记 this 究竟是哪个对象,甚至,我经常以各种不同的混乱方式来“曲线救国”,试图伪装成正常的代码,以我自己的理解方式来找到所需要访问的变量。
我在哪儿?你又是谁 ?
JavaScript 代码的每一个字节都是运行在上下文环境中,你可以把这些上下文环境想象为代码的邻居,它们可以给每一行代码指明:从何处来,朋友和邻居又是谁。没错,这是很重要的信息,因为 JavaScript“社会”有相当严格的规则,规定谁可以跟谁“交往”。上下文环境是门卫把守的社区,而非其内开放的小门。
通常,我们可以把这些社会边界称为作用域,并且有充分理由为每位邻居的宪章里立法(国有国法,家有家法;再小的作用域也有法),而这个宪章就是我们所说的作用域链(Scope Chain)。在特定的邻里关系内,代码只能访问它作用域链内的变量。与超出它邻里的变量比起来,代码更喜欢跟本地(local 局部)的打交道。
具体地说,执行一个函数会创建一个不同的运行上下文环境,它将局部作用域添加到它所定义的作用域链内。JavaScript 通过作用域链,由局部向全局攀升,在特定的上下文中解析标识符,表明本级变量会优先于作用域链内上一级拥有相同名字的变量。因此,当我的好友们谈论”Mike West”(本文原作者)时,他们说的是我,而非 Bluegrass Singer 或是 Duke Professor,尽管后两者著名多。
现在,看些例子来探索上面所说的含义。
代码段 1:
var ima_celebrity = "Everyone can see me! I'm famous!",
the_president = "I'm the decider!";
function pleasantville() {
var the_mayor = "I rule Pleasantville with an iron fist!",
ima_celebrity = "All my neighbors know who I am!";
function lonely_house() {
var agoraphobic = "I fear the day star!",
a_cat = "Meow.";
}
}
- 我们的全明星 ima_celebrity 家喻户晓(所有人都认识她)。她在政治上很活跃,因其知名度和社会地位,敢于叫嚣总统 the_president。她会为每个粉丝签名,回答他们的回答问题,但从不会跟粉丝们私下联系。ima_celebrity 相当清楚粉丝们的存在,并有他们自己某种程度上的个人生活,但也可以肯定,ima_celebrity 并不知道粉丝们在干嘛,甚至连粉丝的名字都不知道。
- 而在欢乐市 pleasantville 内,市长 the_mayor 是众所周知的。他经常在他的城镇内散步,跟选民们聊天、握手并亲吻小孩。因为 pleasantville 还算比较大且重要的邻居,市长在他办公室内放置一台红色电话,一条可以直通总统 7×24 热线。the_mayor 还可以看到市郊外山上的孤屋 lonely_house,但从不在意里面住着的究竟是谁。
- 而孤屋 lonely_house 是一个自我的世界。广场恐惧症者时常在里面喃喃自语,玩纸牌,他还养了一只小猫 a_cat。lonely_house 偶尔会给市长 the_mayor 打电话咨询一些本地的噪音管制,甚至在本地新闻看到 ima_celebrity 后,会写些粉丝言语给她(当然,这是 pleasantville 内的 ima_celebrity)。
this?
每一个运行的上下文环境,除了建立一个作用域链外,还提供一个 this 关键字。它的一般用法是,this 作为一个独特的功能,为邻里们提供一个可访问到它的途径。但总依赖于这个行为并不可靠:取决于我们如何进入一个特定邻居的具体情况,this 表示的完全可能是其他东西。事实上,我们如何进去邻居家本身,通常恰恰就是 this 所指。有四种情形特别值得注意:
调用对象的方法
在经典的面向对象编程中,我们需要识别和引用当前对象。this 极好地扮演了这个角色,为我们的对象提供了自我查找的能力,并指向它们本身的属性。
代码段 2:
var deep_thought = {
the_answer: 42,
ask_question: function () {
return this.the_answer;
}
};
var the_meaning = deep_thought.ask_question(); // 42
该例子,建立了一个名为 deep_thought 对象,其属性 the_answer 为 42,并创建了一个名为 ask_question 方法。当执行 deep_thought.ask_question() 时,JavaScript 为函数调用建立一个上下文环境,通过”.“运算符把 this 指向被引用的对象(当前对象),本例中是 deep_thought 对象。之后,这个方法就可以通过 this 找到它自身的属性,返回 this.the_answer的值。
构造函数
类似地,当定义一个函数,通过构造器,使用 new 关键字创建对象时,this 可用来引用刚创建的对象。让我们写一个能反映该情形的例子:
代码段 3:
function BigComputer(answer) { this.the_answer = answer; this.ask_question = function () { return this.the_answer; } } var deep_thought = new BigComputer(42); var the_meaning = deep_thought.ask_question(); // 42
首先,定义声明并创建一个函数 BigComputer 对象,而不是直接创建 deep_thought 对象。通过 new 关键字实例化 deep_thought 为一个实例变量,当 new BigComputer() 被执行,后台创建了一个全新的对象,它的 this 关键字被设置,并指向新对象的引用。这个函数可以在 this 上设置属性和方法。
尽管如此,此处 deep_thought.the_question() 执行结果跟前面的一样。那这里发生了什么?为何 this 在 ask_question 内与 BigComputer 内会有所不同?简单地说,此处,我们是通过 new 进入 BigComputer 的,所以 this 表示的是“新(new)的对象”。另一方面,我们通过 deep_thought 进入 ask_question,所以当我们执行该方法时,this 表示的是 “deep_thought 所引用的对象”。this 并不像其他的变量一样从作用域链中读取,而是在上下文环境中重置。
因此,比较代码段 3 和代码段 2,说明 this 会在上下文环境中被重置。代码段 2 中的 deep_thought 已经是对象了,而代码段 3 中的 deep_thought 是 new 出来的新的实例对象,你当然可以 new 很多个。
函数调用
假设,没有任何相关对象的奇幻东西,我们只是调用一个普通的函数,这种情形下 this 表示的又是什么?
代码段 4:
function test_this() {
return this;
}
var i_wonder_what_this_is = test_this();
这种情况下,我们并不通过 new 来创建实例对象,从而提供上下文,更没有以某种形式偷偷地提供上下文。此时, this 默认尽可能引用最全局的东西:对网页来说,这就是 window 对象。
事件处理函数
比调用一般函数的更复杂的状况是事件处理函数。假设,我们用函数去处理一个 onclick 事件。当事件触发,我们的函数运行时,this 表示的是什么呢?不凑巧,这个问题不会有简单的答案。
如果我们写的是内联(inline)事件处理函数,那么,this 引用的是全局 window 对象。
代码段 5:
function click_handler() {
alert(this); // window 对象
}
<button id='thebutton' onclick='click_handler()'>
Click me!</button>
可如果我们是通过添加事件处理函数,那么,this 引用的是生成该事件的 DOM 元素。此处只是为了简单起见,实际中会使用真正的 addEvent 添加事件处理的函数。
代码段 6:
function click_handler() {
alert(this); // 按钮的 DOM 节点,当前 DOM
}
function addhandler() {
document.getElementById('thebutton').onclick = click_handler;
}
window.onload = addhandler;
<button id='thebutton'>
Click me!</button>
最后,再看一个更复杂情况。我们需要询问 deep_thought 一个问题,不像代码段 5 那样直接运行 click_handler,而是通过点击按钮,会发生什么?如下所示:
代码段 6:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
<script type="text/javascript">
function BigComputer(answer) {
this.the_answer = answer;
this.ask_question = function () {
alert(this.the_answer);
}
}
function addhandler() {
var deep_thought = new BigComputer(42);
var the_button = document.getElementById('thebutton');
the_button.onclick = deep_thought.ask_question;
}
window.onload = addhandler;
</script>
</head>
<body>
<button id='thebutton'>
Click me!</button>
</body>
</html>
很完美吗?想象一下,当我们点击按钮后,deep_thought.ask_question 被执行,我们得到了“42”。但是,为什么浏览器却给我们一个 undefined? 错在哪里呢?
显然,this 指向的不是 BigComputer。我们给 ask_question 传递一个引用,它作为一个事件处理函数来执行,与作为对象方法来运行的上下文环境并不一样。简言之,ask_question 中的 this 指向了产生事件的 DOM 元素,而不是 BigComputer 对象。DOM 元素并不存在一个 the_answer 属性,所以我们得到的是 undefined。这跟我们的本意相差甚远。setTimeout 也有类似的行为,它在延迟函数执行的同时跑到了一个全局的上下文中去了。
这个问题会在程序的所有角落时不时突然冒出,如果不细致地追踪程序的话,还是一个非常难以排错的问题,尤其在你的对象有跟 DOM 元素或者 window 对象同名属性的时候。
用 apply 和 call 掌控上下文环境
在点击按钮的时候,我们真正需要的是能够咨询 deep_thought 一个问题。更进一步说,在应答事件和setTimeout的调用时,能够在自身的本原上下文中调用对象的方法。JavaScript方法中的 apply 和 call 方法。在我们执行函数调用时,可以“曲线”帮我们达到这样的目的,允许我们覆盖 this 的默认值。先来看看 call:
代码段 7:
var first_object = {
num: 42
}
var second_object = {
num: 24
}
function multiply(mult) {
return this.num * mult;
}
multiply.call(first_object, 5); // 42 * 5
multiply.call(second_object, 5); // 24 * 5
本例中,先定义了两个对象,first_object 和 second_object,它们分别有自己的 num 属性。然后,定义一个 multiply 函数,它只接受一个参数,并返回该参数与 this 所指对象的 num 属性的乘积。如果我们调用函数自身 multiply (5),返回的答案极大可能是 undefined,因为,除非有明确的指定,全局 window 对象并没有 num 属性。我们需要一些途径来告诉 multiply 里面的 this 关键字应该引用什么。而 multiply 的 call 方法正是我们所需要的。
call 的第一个参数表明 multiply 内 this 所指的对象,其余参数表示 multiply 自己的参数,如同函数的自身调用一样。所以,当执行 multiply.call(first_object, 5) 时,multiply 被调用,5 传入,而 this 为 first_object 的引用。multiply.call(second_object, 5) 同理,5 传入,而 this 变为 second_object 的引用。
apply 跟 call 一样,但可以让你把参数包裹进一个数组再传递给调用函数,在程序性生成函数调用时尤为有用。本例若使用 apply :
multiply.apply(first_object, [5]); // 42 * 5
multiply.apply(second_object, [5]); // 24 * 5
apply 和 call 非常有用,但对于事件处理函数所改变的上下文问题,也只是问题的一半而已。在搭建处理函数时,我们想当然地认为,只需简单地通过使用 call 来改变 this 的含义即可。
代码段 8:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
<script type="text/javascript">
function BigComputer(answer) {
this.the_answer = answer;
this.ask_question = function () {
alert(this.the_answer);
}
}
function addhandler() {
var deep_thought = new BigComputer(42);
var the_button = document.getElementById('thebutton');
the_button.onclick = deep_thought.ask_question.call(deep_thought);
}
window.onload = addhandler;
</script>
</head>
<body>
<button id='thebutton'>
Click me!</button>
</body>
</html>
但还是有问题,理由很简单:call 立即执行了函数。
其实,可以用一个匿名函数封装,
the_button.onclick = function () {
deep_thought.ask_question.call(deep_thought);
}
这样,任何问题都没有,但比起下面的 bind 来,显得不够优雅。
我们给 onclcik 一个函数执行后的结果,而非函数的引用,因此,需要利用另一个 JavaScript 特色,以解决这个问题。
bind 之美
我并不是 Prototype JavaScript framework 的忠实粉丝,但对它的总体代码质量印象深刻。具体而言,JavaScript 为 Function 对象增加一个简洁的补充,对我管理函数调用执行后的上下文产生了极大的正面影响:bind 跟 call 执行相同的常见任务——改变函数执行的上下文,不同之处在于,bind 返回的是函数引用可以备用,而不是 call 立即执行而产生的最终结果。
如果需要简化一下 bind 函数以抓住概念的重点,可以先把它应用到前面讨论的代码段 7 的例子中,看它如何工作。这是一个相当优雅的解决方案。
代码段 9:
var first_object = {
num: 42
};
var second_object = {
num: 24
};
function multiply(mult) {
return this.num * mult;
}
Function.prototype.bind = function (obj) {
var method = this;
var temp = function () {
return method.apply(obj, arguments);
};
return temp;
}
var first_multiply = multiply.bind(first_object);
first_multiply(5); // 返回 42 * 5
var second_multiply = multiply.bind(second_object);
second_multiply(5); // 返回 24 * 5
首先,我们定义了 first_object、second_object 和 multiply。然后,为 Function.prototype 定义一个 bind 方法,这样,程序里的函数都会有(继承)一个 bind 方法可用。
当执行 multiply.bind(first_object) 时,JavaScript 为 bind 方法创建一个运行的上下文环境,把 this 设置为 multiply 函数的引用,并把第一个参数 obj 设置为 first_object 的引用。目前为止,一切顺利。
这个解决方案的真正天才之处在于 method 的创建,设置为 this 的引用所指(即 multiply 函数自身)。当下一行的匿名函数被创建,method 通过它的作用域链访问,obj 亦然(不要在此使用 this,因为新创建的函数执行后,this 会被新的、局部的上下文覆盖)。这个 this 的别名让 apply 执行 multiply 函数成为可能,而传递obj则确保上下文的正确。用计算机科学的话说,temp 是一个闭包(closure),它可以保证,需要在 first_object 的上下文中执行 multiply,bind 调用的最终返回可以用在任何的上下文中。
这才是前面说到的事件处理函数和 setTimeout 情形所真正需要的。
下面代码完全解决了这些问题,绑定 deep_thought.ask_question 方法到 deep_thought 的上下文中,因此能在任何事件触发时都能正确运行:
代码段 10:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
<script type="text/javascript">
function BigComputer(answer) {
this.the_answer = answer;
this.ask_question = function () {
alert(this.the_answer);
}
}
Function.prototype.bind = function (obj) {
var method = this;
var temp = function () {
return method.apply(obj, arguments);
};
return temp;
}
function addhandler() {
var deep_thought = new BigComputer(42);
var the_button = document.getElementById('thebutton');
the_button.onclick = deep_thought.ask_question.bind(deep_thought);
}
window.onload = addhandler;
</script>
</head>
<body>
<button id='thebutton'>
Click me!</button>
</body>
</html>