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_catlonely_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_objectsecond_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_objectsecond_objectmultiply。然后,为 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>

下载 Demo

posted @ 2013-10-06 01:33  船长&CAP  阅读(1045)  评论(1编辑  收藏  举报
免费流量统计软件