認(rèn)識(shí)javascript中的作用域和上下文

2018-06-16 18:35 更新

javascript中的作用域(scope)和上下文(context)是這門語(yǔ)言的獨(dú)到之處,這部分歸功于他們帶來(lái)的靈活性。每個(gè)函數(shù)有不同的變量上下文和作用域。這些概念是javascript中一些強(qiáng)大的設(shè)計(jì)模式的后盾。然而這也給開發(fā)人員帶來(lái)很大困惑。下面全面揭示了javascript中的上下文和作用域的不同,以及各種設(shè)計(jì)模式如何使用他們。

上下文 vs 作用域

首先需要澄清的問(wèn)題是上下文和作用域是不同的概念。多年來(lái)我注意到許多開發(fā)者經(jīng)常將這兩個(gè)術(shù)語(yǔ)混淆,錯(cuò)誤的將一個(gè)描述為另一個(gè)。平心而論,這些術(shù)語(yǔ)變得非?;靵y不堪。

每個(gè)函數(shù)調(diào)用都有與之相關(guān)的作用域和上下文。從根本上說(shuō),范圍是基于函數(shù)(function-based)而上下文是基于對(duì)象(object-based)。換句話說(shuō),作用域是和每次函數(shù)調(diào)用時(shí)變量的訪問(wèn)有關(guān),并且每次調(diào)用都是獨(dú)立的。上下文總是關(guān)鍵字 this 的值,是調(diào)用當(dāng)前可執(zhí)行代碼的對(duì)象的引用。

變量作用域

變量能夠被定義在局部或者全局作用域,這導(dǎo)致運(yùn)行時(shí)變量的訪問(wèn)來(lái)自不同的作用域。全局變量需被聲明在函數(shù)體外,在整個(gè)運(yùn)行過(guò)程中都存在,能在任何作用域中訪問(wèn)和修改。局部變量?jī)H在函數(shù)體內(nèi)定義,并且每次函數(shù)調(diào)用都有不同的作用域。這主題是僅在調(diào)用中的賦值,求值和對(duì)值的操作,不能訪問(wèn)作用域之外的值。

目前javascript不支持塊級(jí)作用域,塊級(jí)作用域指在if語(yǔ)句,switch語(yǔ)句,循環(huán)語(yǔ)句等語(yǔ)句塊中定義變量,這意味著變量不能在語(yǔ)句塊之外被訪問(wèn)。當(dāng)前任何在語(yǔ)句塊中定義的變量都能在語(yǔ)句塊之外訪問(wèn)。然而,這種情況很快會(huì)得到改變,let 關(guān)鍵字已經(jīng)正式添加到ES6規(guī)范。用它來(lái)代替var關(guān)鍵字可以將局部變量聲明為塊級(jí)作用域。

“this” 上下文

上下文通常是取決于一個(gè)函數(shù)如何被調(diào)用。當(dāng)函數(shù)作為對(duì)象的方法被調(diào)用時(shí),this 被設(shè)置為調(diào)用方法的對(duì)象:

var object = {
    foo: function(){
        alert(this === object); 
    }
};

object.foo(); // true

同樣的原理適用于當(dāng)調(diào)用一個(gè)函數(shù)時(shí)通過(guò)new的操作符創(chuàng)建一個(gè)對(duì)象的實(shí)例。當(dāng)以這種方式調(diào)用時(shí),this 的值將被設(shè)置為新創(chuàng)建的實(shí)例:

function foo(){
    alert(this);
}

foo() // window
new foo() // foo

當(dāng)調(diào)用一個(gè)未綁定函數(shù),this 將被默認(rèn)設(shè)置為 全局上下文(global context) 或window對(duì)象(如果在瀏覽器中)。然而如果函數(shù)在嚴(yán)格模式下被執(zhí)行(“use strict”),this的值將被默認(rèn)設(shè)置為undefined。

執(zhí)行上下文和作用域鏈

javascript是一個(gè)單線程語(yǔ)言,這意味著在瀏覽器中同時(shí)只能做一件事情。當(dāng)javascript解釋器初始執(zhí)行代碼,它首先默認(rèn)竟如全局上下文。每次調(diào)用一個(gè)函數(shù)將會(huì)創(chuàng)建一個(gè)新的執(zhí)行上下文。

這里經(jīng)常發(fā)生混淆,這術(shù)語(yǔ)”執(zhí)行上下文(execution context)“在這里的所要表達(dá)的意思是作用域,不是前面討論的上下文。這是槽糕的命名,然而這術(shù)語(yǔ)ECMAScript規(guī)范所定義的,無(wú)奈的遵守吧。

每次新創(chuàng)建一個(gè)執(zhí)行上下文,會(huì)被添加到作用域鏈的頂部,又是也成為執(zhí)行或調(diào)用棧。瀏覽器總是運(yùn)行在位于作用域鏈頂部當(dāng)前執(zhí)行上下文。一旦完成,它(當(dāng)前執(zhí)行上下文)將從棧頂被移除并且將控制權(quán)歸還給之前的執(zhí)行上下文。例如:

function first(){
    second();
    function second(){
        third();
        function third(){
            fourth();
            function fourth(){
                // do something
            }
        }
    }   
}
first();

運(yùn)行前面的代碼將會(huì)導(dǎo)致嵌套的函數(shù)被從上倒下執(zhí)行直到 fourth 函數(shù),此時(shí)作用域鏈從上到下為: fourth, third, second, first, global。fourth 函數(shù)能夠訪問(wèn)全局變量和任何在first,second和third函數(shù)中定義的變量,就如同訪問(wèn)自己的變量一樣。一旦fourth函數(shù)執(zhí)行完成,fourth暈高興上下文將被從作用域鏈頂端移除并且執(zhí)行將返回到thrid函數(shù)。這一過(guò)程持續(xù)進(jìn)行直到所有代碼已完成執(zhí)行。

不同執(zhí)行上下文之間的變量命名沖突通過(guò)攀爬作用域鏈解決,從局部直到全局。這意味著具有相同名稱的局部變量在作用域鏈中有更高的優(yōu)先級(jí)。

簡(jiǎn)單的說(shuō),每次你試圖訪問(wèn)函數(shù)執(zhí)行上下文中的變量時(shí),查找進(jìn)程總是從自己的變量對(duì)象開始。如果在自己的變量對(duì)象中沒發(fā)現(xiàn)要查找的變量,繼續(xù)搜索作用域鏈。它將攀爬作用域鏈檢查每一個(gè)執(zhí)行上下文的變量對(duì)象去尋找和變量名稱匹配的值。

閉包

當(dāng)一個(gè)嵌套的函數(shù)在定義(作用域)的外面被訪問(wèn),以至它可以在外部函數(shù)返回后被執(zhí)行,此時(shí)一個(gè)閉包形成。它(閉包)維護(hù)(在內(nèi)部函數(shù)中)對(duì)外部函數(shù)中局部變量,arguments和函數(shù)聲明的訪問(wèn)。封裝允許我們從外部作用域中隱藏和保護(hù)執(zhí)行上下文,而暴露公共接口,通過(guò)接口進(jìn)一步操作。一個(gè)簡(jiǎn)單的例子看起來(lái)如下:

function foo(){
    var local = 'private variable';
    return function bar(){
        return local;
    }
}

var getLocalVariable = foo();
getLocalVariable() // private variable

其中最流行的閉包類型是廣為人知的模塊模式。它允許你模擬公共的,私有的和特權(quán)成員:

var Module = (function(){
    var privateProperty = 'foo';

    function privateMethod(args){
        //do something
    }

    return {

        publicProperty: "",

        publicMethod: function(args){
            //do something
        },

        privilegedMethod: function(args){
            privateMethod(args);
        }
    }
})();

模塊實(shí)際上有些類似于單例,在末尾添加一對(duì)括號(hào),當(dāng)解釋器解釋完后立即執(zhí)行(立即執(zhí)行函數(shù))。閉包執(zhí)行上下位的外部唯一可用的成員是返回對(duì)象中公用的方法和屬性(例如Module.publicMethod)。然而,所有的私有屬性和方法在整個(gè)程序的生命周期中都將存在,由于(閉包)使執(zhí)行上下文收到保護(hù),和變量的交互要通過(guò)公用的方法。

另一種類型的閉包叫做立即調(diào)用函數(shù)表達(dá)式(immediately-invoked function expression IIFE),無(wú)非是一個(gè)在window上下文中的自調(diào)用匿名函數(shù)(self-invoked anonymous function)。

function(window){

    var a = 'foo', b = 'bar';

    function private(){
        // do something
    }

    window.Module = {

        public: function(){
            // do something 
        }
    };

})(this);

對(duì)保護(hù)全局命名空間,這種表達(dá)式非常有用,所有在函數(shù)體內(nèi)聲明的變量都是局部變量,并通過(guò)閉包在整個(gè)運(yùn)行環(huán)境保持存在。這種封裝源代碼的方式對(duì)程序和框架都是非常流行的,通常暴露單一全局接口與外界交互。

Call 和 Apply

這兩個(gè)簡(jiǎn)單的方法,內(nèi)建在所有的函數(shù)中,允許在自定義上下文中執(zhí)行函數(shù)。call 函數(shù)需要參數(shù)列表而 apply 函數(shù)允許你傳遞參數(shù)為數(shù)組:

function user(first, last, age){
    // do something 
}
user.call(window, 'John', 'Doe', 30);
user.apply(window, ['John', 'Doe', 30]);

執(zhí)行的結(jié)果是相同的,user 函數(shù)在window上下文上被調(diào)用,并提供了相同的三個(gè)參數(shù)。

ECMAScript 5 (ES5)引入了Function.prototype.bind方法來(lái)控制上下文,它返回一個(gè)新函數(shù),這函數(shù)(的上下文)被永久綁定到bind方法的第一個(gè)參數(shù),無(wú)論函數(shù)被如何調(diào)用。它通過(guò)閉包修正函數(shù)的上下文,下面是為不支持的瀏覽器提供的方案:

if(!('bind' in Function.prototype)){
    Function.prototype.bind = function(){
        var fn = this, context = arguments[0], args = Array.prototype.slice.call(arguments, 1);
        return function(){
            return fn.apply(context, args);
        }
    }
}

它常用在上下文丟失:面向?qū)ο蠛褪录幚怼_@點(diǎn)有必要的因?yàn)?節(jié)點(diǎn)的addEventListener 方法總保持函數(shù)執(zhí)行的上下文為事件處理被綁定的節(jié)點(diǎn),這點(diǎn)很重要。然而如果你使用高級(jí)面向?qū)ο蠹夹g(shù)并且需要維護(hù)回調(diào)函數(shù)的上下文是方法的實(shí)例,你必須手動(dòng)調(diào)整上下文。這就是bind 帶來(lái)的方便:

function MyClass(){
    this.element = document.createElement('div');
    this.element.addEventListener('click', this.onClick.bind(this), false);
}

MyClass.prototype.onClick = function(e){
    // do something
};

當(dāng)回顧bind函數(shù)的源代碼,你可能注意到下面這一行相對(duì)簡(jiǎn)單的代碼,調(diào)用Array的一個(gè)方法:

Array.prototype.slice.call(arguments, 1);

有趣的是,這里需要注意的是arguments對(duì)象實(shí)際上并不是一個(gè)數(shù)組,然而它經(jīng)常被描述為類數(shù)組(array-like)對(duì)象,很向 nodelist(document.getElementsByTagName()方法返回的結(jié)果)。他們包含lenght屬性,值能夠被索引,但他們?nèi)匀徊皇菙?shù)組,由于他們不支持原生的數(shù)組方法,比如slice和push。然而,由于他們有和數(shù)組類似的行為,數(shù)組的方法能被調(diào)用和劫持。如果你想這樣,在類數(shù)組的上下文中執(zhí)行數(shù)組方法,可參照上面的例子。

這種調(diào)用其他對(duì)象方法的技術(shù)也被應(yīng)用到面向?qū)ο笾?,?dāng)在javascript中模仿經(jīng)典繼承(類繼承):

MyClass.prototype.init = function(){
    // call the superclass init method in the context of the "MyClass" instance
    MySuperClass.prototype.init.apply(this, arguments);
}

通過(guò)在子類(MyClass)的實(shí)例中調(diào)用超類(MySuperClass)的方法,我們能重現(xiàn)這種強(qiáng)大的設(shè)計(jì)模式。

結(jié)論

在你開始學(xué)習(xí)高級(jí)設(shè)計(jì)模式之前理解這些概念是非常重要的,由于作用域和上下文在現(xiàn)代javascript中扮演重要的和根本的角色。無(wú)論我們談?wù)撻]包,面向?qū)ο?,和繼承或各種原生實(shí)現(xiàn),上下文和作用域都扮演重要角色。如果你的目標(biāo)是掌握javascript語(yǔ)言并深入了解它的組成,作用域和上下文應(yīng)該是你的起點(diǎn)。

譯者補(bǔ)充

作者實(shí)現(xiàn)的bind函數(shù)是不完全的,調(diào)用bind返回的函數(shù)時(shí)不能傳遞參數(shù),下面的代碼修復(fù)了這個(gè)問(wèn)題:

if(!(‘bind’ in Function.prototype)){
    Function.prototype.bind = function(){
        var fn = this, context = arguments[0], args =            Array.prototype.slice.call(arguments, 1);
        return function(){
            return fn.apply(context, args.concat(arguments));//fixed
        }
    }
}

注:本文為翻譯文章,原文為 Understanding Scope and Context in JavaScript。

原文網(wǎng)址:http://yanhaijing.com/javascript/2013/08/30/understanding-scope-and-context-in-javascript/

歡迎訂閱我的微信公眾帳號(hào),只推送原創(chuàng)文字。掃碼或搜索:顏海鏡

打賞其他方式

本文對(duì)你有幫助?那就賞杯咖啡吧

微信掃一掃轉(zhuǎn)賬 支付寶掃一掃轉(zhuǎn)賬
以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)