九色国产,午夜在线视频,新黄色网址,九九色综合,天天做夜夜做久久做狠狠,天天躁夜夜躁狠狠躁2021a,久久不卡一区二区三区

打開APP
userphoto
未登錄

開通VIP,暢享免費電子書等14項超值服

開通VIP
理解 JavaScript 閉包

目錄

簡介

返回目錄

Closure
所謂“閉包”,指的是一個擁有許多變量和綁定了這些變量的環(huán)境的表達式(通常是一個函數(shù)),因而這些變量也是該表達式的一部分。

閉包是 ECMAScript (JavaScript)最強大的特性之一,但用好閉包的前提是必須理解閉包。閉包的創(chuàng)建相對容易,人們甚至會在不經(jīng)意間創(chuàng)建閉包,但這些無意創(chuàng)建的閉包卻存在潛在的危害,尤其是在比較常見的瀏覽器環(huán)境下。如果想要揚長避短地使用閉包這一特性,則必須了解它們的工作機制。而閉包工作機制的實現(xiàn)很大程度上有賴于標識符(或者說對象屬性)解析過程中作用域的角色。

關(guān)于閉包,最簡單的描述就是 ECMAScript 允許使用內(nèi)部函數(shù)--即函數(shù)定義和函數(shù)表達式位于另一個函數(shù)的函數(shù)體內(nèi)。而且,這些內(nèi)部函數(shù)可以訪問它們所在的外部函數(shù)中聲明的所有局部變量、參數(shù)和聲明的其他內(nèi)部函數(shù)。當其中一個這樣的內(nèi)部函數(shù)在包含它們的外部函數(shù)之外被調(diào)用時,就會形成閉包。也就是說,內(nèi)部函數(shù)會在外部函數(shù)返回后被執(zhí)行。而當這個內(nèi)部函數(shù)執(zhí)行時,它仍然必需訪問其外部函數(shù)的局部變量、參數(shù)以及其他內(nèi)部函數(shù)。這些局部變量、參數(shù)和函數(shù)聲明(最初時)的值是外部函數(shù)返回時的值,但也會受到內(nèi)部函數(shù)的影響。

遺憾的是,要適當?shù)乩斫忾]包就必須理解閉包背后運行的機制,以及許多相關(guān)的技術(shù)細節(jié)。雖然本文的前半部分并沒有涉及 ECMA 262 規(guī)范指定的某些算法,但仍然有許多無法回避或簡化的內(nèi)容。對于個別熟悉對象屬性名解析的人來說,可以跳過相關(guān)的內(nèi)容,但是除非你對閉包也非常熟悉,否則最好是不要跳下面幾節(jié)。

對象屬性名解析

返回目錄

ECMAScript 認可兩類對象:原生(Native)對象和宿主(Host)對象,其中宿主對象包含一個被稱為內(nèi)置對象的原生對象的子類(ECMA 262 3rd Ed Section 4.3)。原生對象屬于語言,而宿主對象由環(huán)境提供,比如說可能是文檔對象、DOM 等類似的對象。

原生對象具有松散和動態(tài)的命名屬性(對于某些實現(xiàn)的內(nèi)置對象子類別而言,動態(tài)性是受限的--但這不是太大的問題)。對象的命名屬性用于保存值,該值可以是指向另一個對象(Objects)的引用(在這個意義上說,函數(shù)也是對象),也可以是一些基本的數(shù)據(jù)類型,比如:String、Number、Boolean、Null 或 Undefined。其中比較特殊的是 Undefined 類型,因為可以給對象的屬性指定一個 Undefined 類型的值,而不會刪除對象的相應(yīng)屬性。而且,該屬性只是保存著 undefined 值。 

下面簡要介紹一下如何設(shè)置和讀取對象的屬性值,并最大程度地體現(xiàn)相應(yīng)的內(nèi)部細節(jié)。

值的賦予

返回目錄

對象的命名屬性可以通過為該命名屬性賦值來創(chuàng)建,或重新賦值。即,對于:

var objectRef = new Object(); //創(chuàng)建一個普通的 JavaScript 對象。

可以通過下面語句來創(chuàng)建名為 “testNumber” 的屬性:

objectRef.testNumber = 5;
/* - 或- */
objectRef[”testNumber”] = 5;

在賦值之前,對象中沒有“testNumber” 屬性,但在賦值后,則創(chuàng)建一個屬性。之后的任何賦值語句都不需要再創(chuàng)建這個屬性,而只會重新設(shè)置它的值:

objectRef.testNumber = 8;
/* - or:- */
objectRef[”testNumber”] = 8;

稍后我們會介紹,Javascript 對象都有原型(prototypes)屬性,而這些原型本身也是對象,因而也可以帶有命名的屬性。但是,原型對象命名屬性的作用并不體現(xiàn)在賦值階段。同樣,在將值賦給其命名屬性時,如果對象沒有該屬性則會創(chuàng)建該命名屬性,否則會重設(shè)該屬性的值。

值的讀取

返回目錄

當讀取對象的屬性值時,原型對象的作用便體現(xiàn)出來。如果對象的原型中包含屬性訪問器(property accessor)所使用的屬性名,那么該屬性的值就會返回:

/* 為命名屬性賦值。如果在賦值前對象沒有相應(yīng)的屬性,那么賦值后就會得到一個:*/
objectRef.testNumber = 8;

/* 從屬性中讀取值 */
var val = objectRef.testNumber;

/* 現(xiàn)在, - val - 中保存著剛賦給對象命名屬性的值 8*/

而且,由于所有對象都有原型,而原型本身也是對象,所以原型也可能有原型,這樣就構(gòu)成了所謂的原型鏈。原型鏈終止于鏈中原型為 null 的對象。Object 構(gòu)造函數(shù)的默認原型就有一個 null 原型,因此:

var objectRef = new Object(); //創(chuàng)建一個普通的 JavaScript 對象。

創(chuàng)建了一個原型為 Object.prototype 的對象,而該原型自身則擁有一個值為 null 的原型。也就是說, objectRef 的原型鏈中只包含一個對象-- Object.prototype。但對于下面的代碼而言:

/* 創(chuàng)建 - MyObject1 - 類型對象的函數(shù)*/
function MyObject1(formalParameter){
/* 給創(chuàng)建的對象添加一個名為 - testNumber - 的屬性
并將傳遞給構(gòu)造函數(shù)的第一個參數(shù)指定為該屬性的值:*/
this.testNumber = formalParameter;
}
/* 創(chuàng)建 - MyObject2 - 類型對象的函數(shù)*/
function MyObject2(formalParameter){
/* 給創(chuàng)建的對象添加一個名為 - testString - 的屬性
并將傳遞給構(gòu)造函數(shù)的第一個參數(shù)指定為該屬性的值:*/
this.testString = formalParameter;
}

/* 接下來的操作用 MyObject1 類的實例替換了所有與 MyObject2 類的實例相關(guān)聯(lián)的原型。而且,為 MyObject1 構(gòu)造函數(shù)傳遞了參數(shù) - 8 - ,因而其 - testNumber - 屬性被賦予該值:*/
MyObject2.prototype = new MyObject1( 8 );

/* 最后,將一個字符串作為構(gòu)造函數(shù)的第一個參數(shù),創(chuàng)建一個 - MyObject2 - 的實例,并將指向該對象的引用賦給變量 - objectRef - :*/
var objectRef = new MyObject2( “String_Value” );

被變量 objectRef 所引用的 MyObject2 的實例擁有一個原型鏈。該鏈中的第一個對象是在創(chuàng)建后被指定給 MyObject2 構(gòu)造函數(shù)的 prototype 屬性的 MyObject1 的一個實例。MyObject1 的實例也有一個原型,即與 Object.prototype 所引用的對象對應(yīng)的默認的 Object 對象的原型。最后, Object.prototype 有一個值為 null 的原型,因此這條原型鏈到此結(jié)束。

當某個屬性訪問器嘗試讀取由 objectRef 所引用的對象的屬性值時,整個原型鏈都會被搜索。在下面這種簡單的情況下:

var val = objectRef.testString;

因為 objectRef 所引用的 MyObject2 的實例有一個名為“testString”的屬性,因此被設(shè)置為“String_Value”的該屬性的值被賦給了變量 val。但是:

var val = objectRef.testNumber;

則不能從 MyObject2 實例自身中讀取到相應(yīng)的命名屬性值,因為該實例沒有這個屬性。然而,變量 val 的值仍然被設(shè)置為 8,而不是未定義--這是因為在該實例中查找相應(yīng)的命名屬性失敗后,解釋程序會繼續(xù)檢查其原型對象。而該實例的原型對象是 MyObject1 的實例,這個實例有一個名為“testNumber”的屬性并且值為 8,所以這個屬性訪問器最后會取得值 8。而且,雖然 MyObject1MyObject2 都沒有定義 toString 方法,但是當屬性訪問器通過 objectRef 讀取 toString 屬性的值時:

var val = objectRef.toString;

變量 val 也會被賦予一個函數(shù)的引用。這個函數(shù)就是在 Object.prototypetoString 屬性中所保存的函數(shù)。之所以會返回這個函數(shù),是因為發(fā)生了搜索 objectRef 原型鏈的過程。當在作為對象的 objectRef 中發(fā)現(xiàn)沒有“toString”屬性存在時,會搜索其原型對象,而當原型對象中不存在該屬性時,則會繼續(xù)搜索原型的原型。而原型鏈中最終的原型是 Object.prototype,這個對象確實有一個 toString 方法,因此該方法的引用被返回。

最后:

var val = objectRef.madeUpProperty;

返回 undefined,因為在搜索原型鏈的過程中,直至 Object.prototype 的原型--null,都沒有找到任何對象有名為“madeUpPeoperty”的屬性,因此最終返回 undefined。

不論是在對象或?qū)ο蟮脑椭校x取命名屬性值的時候只返回首先找到的屬性值。而當為對象的命名屬性賦值時,如果對象自身不存在該屬性則創(chuàng)建相應(yīng)的屬性。

這意味著,如果執(zhí)行像 objectRef.testNumber = 3 這樣一條賦值語句,那么這個 MyObject2 的實例自身也會創(chuàng)建一個名為“testNumber”的屬性,而之后任何讀取該命名屬性的嘗試都將獲得相同的新值。這時候,屬性訪問器不會再進一步搜索原型鏈,但 MyObject1 實例值為 8 的“testNumber”屬性并沒有被修改。給 objectRef 對象的賦值只是遮擋了其原型鏈中相應(yīng)的屬性。

注意:ECMAScript 為 Object 類型定義了一個內(nèi)部 [[prototype]] 屬性。這個屬性不能通過腳本直接訪問,但在屬性訪問器解析過程中,則需要用到這個內(nèi)部 [[prototype]] 屬性所引用的對象鏈--即原型鏈??梢酝ㄟ^一個公共的 prototype 屬性,來對與內(nèi)部的 [[prototype]] 屬性對應(yīng)的原型對象進行賦值或定義。這兩者之間的關(guān)系在 ECMA 262(3rd edition)中有詳細描述,但超出了本文要討論的范疇。

標識符解析、執(zhí)行環(huán)境和作用域鏈

執(zhí)行環(huán)境

返回目錄

執(zhí)行環(huán)境是 ECMAScript 規(guī)范(ECMA 262 第 3 版)用于定義 ECMAScript 實現(xiàn)必要行為的一個抽象的概念。對如何實現(xiàn)執(zhí)行環(huán)境,規(guī)范沒有作規(guī)定。但由于執(zhí)行環(huán)境中包含引用規(guī)范所定義結(jié)構(gòu)的相關(guān)屬性,因此執(zhí)行環(huán)境中應(yīng)該保有(甚至實現(xiàn))帶有屬性的對象--即使屬性不是公共屬性。

所有 JavaScript 代碼都是在一個執(zhí)行環(huán)境中被執(zhí)行的。全局代碼(作為內(nèi)置的JS 文件執(zhí)行的代碼,或者 HTML 頁面加載的代碼)是在我稱之為“全局執(zhí)行環(huán)境”的執(zhí)行環(huán)境中執(zhí)行的,而對函數(shù)的每次調(diào)用(
有可能是作為構(gòu)造函數(shù))同樣有關(guān)聯(lián)的執(zhí)行環(huán)境。通過 eval 函數(shù)執(zhí)行的代碼也有截然不同的執(zhí)行環(huán)境,但因為 JavaScript 程序員在正常情況下一般不會使用 eval,所以這里不作討論。有關(guān)執(zhí)行環(huán)境的詳細說明請參閱 ECMA 262(3rd edition)第 10.2 節(jié)。

當調(diào)用一個 JavaScript 函數(shù)時,該函數(shù)就會進入相應(yīng)的執(zhí)行環(huán)境。如果又調(diào)用了另外一個函數(shù)(或者遞歸地調(diào)用同一個函數(shù)),則又會創(chuàng)建一個新的執(zhí)行環(huán)境,并且在函數(shù)調(diào)用期間執(zhí)行過程都處于該環(huán)境中。當調(diào)用的函數(shù)返回后,執(zhí)行過程會返回原始執(zhí)行環(huán)境。因而,運行中的 JavaScript 代碼就構(gòu)成了一個執(zhí)行環(huán)境棧。

在創(chuàng)建執(zhí)行環(huán)境的過程中,會按照定義的先后順序完成一系列操作。首先,在一個函數(shù)的執(zhí)行環(huán)境中,會創(chuàng)建一個“活動”對象?;顒訉ο笫且?guī)范中規(guī)定的另外一種機制。之所以稱之為對象,是因為它擁有可訪問的命名屬性,但是它又不像正常對象那樣具有原型(至少沒有預(yù)定義的原型),而且不能通過 JavaScript 代碼直接引用活動對象。

為函數(shù)調(diào)用創(chuàng)建執(zhí)行環(huán)境的下一步是創(chuàng)建一個 arguments 對象,這是一個類似數(shù)組的對象,它以整數(shù)索引的數(shù)組成員一一對應(yīng)地保存著調(diào)用函數(shù)時所傳遞的參數(shù)。這個對象也有 lengthcallee 屬性(這兩個屬性與我們討論的內(nèi)容無關(guān),詳見規(guī)范)。然后,會為活動對象創(chuàng)建一個名為“arguments”的屬性,該屬性引用前面創(chuàng)建的 arguments對象。

接著,為執(zhí)行環(huán)境分配作用域。作用域由對象列表(鏈)組成。每個函數(shù)對象都有一個內(nèi)部的 [[scope]] 屬性(該屬性我們稍后會詳細介紹),這個屬性也由對象列表(鏈)組成。指定給一個函數(shù)調(diào)用執(zhí)行環(huán)境的作用域,由該函數(shù)對象的 [[scope]] 屬性所引用的對象列表(鏈)組成,同時,活動對象被添加到該對象列表的頂部(鏈的前端)。

之后會發(fā)生由 ECMA 262 中所謂“可變”對象完成的“變量實例化”的過程。只不過此時使用活動對象作為可變對象(這里很重要,請注意:它們是同一個對象)。此時會將函數(shù)的形式參數(shù)創(chuàng)建為可變對象的命名屬性,如果調(diào)用函數(shù)時傳遞的參數(shù)與形式參數(shù)一致,則將相應(yīng)參數(shù)的值賦給這些命名屬性(否則,會給命名屬性賦 undefined 值)。對于定義的內(nèi)部函數(shù),會以其聲明時所用名稱為可變對象創(chuàng)建同名屬性,而相應(yīng)的內(nèi)部函數(shù)則被創(chuàng)建為函數(shù)對象并指定給該屬性。變量實例化的最后一步是將在函數(shù)內(nèi)部聲明的所有局部變量創(chuàng)建為可變對象的命名屬性。

根據(jù)聲明的局部變量創(chuàng)建的可變對象的屬性在變量實例化過程中會被賦予 undefined 值。在執(zhí)行函數(shù)體內(nèi)的代碼、并計算相應(yīng)的賦值表達式之前不會對局部變量執(zhí)行真正的實例化。

事實上,擁有 arguments 屬性的活動對象和擁有與函數(shù)局部變量對應(yīng)的命名屬性的可變對象是同一個對象。因此,可以將標識符 arguments 作為函數(shù)的局部變量來看待。

最后,要為使用 this 關(guān)鍵字而賦值。如果所賦的值引用一個對象,那么前綴以 this 關(guān)鍵字的屬性訪問器就是引用該對象的屬性。如果所賦(內(nèi)部)值是 null,那么 this 關(guān)鍵字則引用全局對象。

創(chuàng)建全局執(zhí)行環(huán)境的過程會稍有不同,因為它沒有參數(shù),所以不需要通過定義的活動對象來引用這些參數(shù)。但全局執(zhí)行環(huán)境也需要一個作用域,而它的作用域鏈實際上只由一個對象--全局對象--組成。全局執(zhí)行環(huán)境也會有變量實例化的過程,它的內(nèi)部函數(shù)就是涉及大部分 JavaScript 代碼的、常規(guī)的頂級函數(shù)聲明。而且,在變量實例化過程中全局對象就是可變對象,這就是為什么全局性聲明的函數(shù)是全局對象屬性的原因。全局性聲明的變量同樣如此。

全局執(zhí)行環(huán)境也會使用 this 對象來引用全局對象。

作用域鏈與 [[scope]]

返回目錄

調(diào)用函數(shù)時創(chuàng)建的執(zhí)行環(huán)境會包含一個作用域鏈,這個作用域鏈是通過將該執(zhí)行環(huán)境的活動(可變)對象添加到保存于所調(diào)用函數(shù)對象的 [[scope]] 屬性中的作用域鏈前端而構(gòu)成的。所以,理解函數(shù)對象內(nèi)部的 [[scope]] 屬性的定義過程至關(guān)重要。

在 ECMAScript 中,函數(shù)也是對象。函數(shù)對象在變量實例化過程中會根據(jù)函數(shù)聲明來創(chuàng)建,或者是在計算函數(shù)表達式或調(diào)用 Function 構(gòu)造函數(shù)時創(chuàng)建。

通過調(diào)用 Function 構(gòu)造函數(shù)創(chuàng)建的函數(shù)對象,其內(nèi)部的 [[scope]] 屬性引用的作用域鏈中始終只包含全局對象。

通過函數(shù)聲明或函數(shù)表達式創(chuàng)建的函數(shù)對象,其內(nèi)部的 [[scope]] 屬性引用的則是創(chuàng)建它們的執(zhí)行環(huán)境的作用域鏈。

在最簡單的情況下,比如聲明如下全局函數(shù):-

function exampleFunction(formalParameter){
// 函數(shù)體內(nèi)的代碼
}

- 當為創(chuàng)建全局執(zhí)行環(huán)境而進行變量實例化時,會根據(jù)上面的函數(shù)聲明創(chuàng)建相應(yīng)的函數(shù)對象。因為全局執(zhí)行環(huán)境的作用域鏈中只包含全局對象,所以它就給自己創(chuàng)建的、并以名為“exampleFunction”的屬性引用的這個函數(shù)對象的內(nèi)部 [[scope]] 屬性,賦予了只包含全局對象的作用域鏈。

當在全局環(huán)境中計算函數(shù)表達式時,也會發(fā)生類似的指定作用域鏈的過程:-

var exampleFuncRef = function(){
// 函數(shù)體代碼
}

在這種情況下,不同的是在全局執(zhí)行環(huán)境的變量實例化過程中,會先為全局對象創(chuàng)建一個命名屬性。而在計算賦值語句之前,暫時不會創(chuàng)建函數(shù)對象,也不會將該函數(shù)對象的引用指定給全局對象的命名屬性。但是,最終還是會在全局執(zhí)行環(huán)境中創(chuàng)建這個函數(shù)對象(當計算函數(shù)表達式時。譯者注),而為這個創(chuàng)建的函數(shù)對象的 [[scope]] 屬性指定的作用域鏈中仍然只包含全局對象。內(nèi)部的函數(shù)聲明或表達式會導(dǎo)致在包含它們的外部函數(shù)的執(zhí)行環(huán)境中創(chuàng)建相應(yīng)的函數(shù)對象,因此這些函數(shù)對象的作用域鏈會稍微復(fù)雜一些。在下面的代碼中,先定義了一個帶有內(nèi)部函數(shù)聲明的外部函數(shù),然后調(diào)用外部函數(shù):

/* 創(chuàng)建全局變量 - y - 它引用一個對象:- */var y = {x:5}; // 帶有一個屬性 - x - 的對象直接量function exampleFuncWith(){var z;/* 將全局對象 - y - 引用的對象添加到作用域鏈的前端:- */with(y){/* 對函數(shù)表達式求值,以創(chuàng)建函數(shù)對象并將該函數(shù)對象的引用指定給局部變量 - z - :- */z = function(){… // 內(nèi)部函數(shù)表達式中的代碼;}}…}/* 執(zhí)行 - exampleFuncWith - 函數(shù):- */

exampleFuncWith();在調(diào)用 exampleFuncWith 函數(shù)創(chuàng)建的執(zhí)行環(huán)境中包含一個由其活動對象后跟全局對象構(gòu)成的作用域鏈。而在執(zhí)行 with 語句時,又會把全局變量 y 引用的對象添加到這個作用域鏈的前端。在對其中的函數(shù)表達式求值的過程中,所創(chuàng)建函數(shù)對象的 [[scope]] 屬性與創(chuàng)建它的執(zhí)行環(huán)境的作用域保持一致--即,該屬性會引用一個由對象 y 后跟調(diào)用外部函數(shù)時所創(chuàng)建執(zhí)行環(huán)境的活動對象,后跟全局對象的作用域鏈。

當與 with 語句相關(guān)的語句塊執(zhí)行結(jié)束時,執(zhí)行環(huán)境的作用域得以恢復(fù)(y 會被移除),但是已經(jīng)創(chuàng)建的函數(shù)對象(z。譯者注)的 [[scope]] 屬性所引用的作用域鏈中位于最前面的仍然是對象 y。

例 3:包裝相關(guān)的功能

返回目錄

閉包可以用于創(chuàng)建額外的作用域,通過該作用域可以將相關(guān)的和具有依賴性的代碼組織起來,以便將意外交互的風險降到最低。假設(shè)有一個用于構(gòu)建字符串的函數(shù),為了避免重復(fù)性的連接操作(和創(chuàng)建眾多的中間字符串),我們的愿望是使用一個數(shù)組按順序來存儲字符串的各個部分,然后再使用 Array.prototype.join 方法(以空字符串作為其參數(shù))輸出結(jié)果。這個數(shù)組將作為輸出的緩沖器,但是將數(shù)組作為函數(shù)的局部變量又會導(dǎo)致在每次調(diào)用函數(shù)時都重新創(chuàng)建一個新數(shù)組,這在每次調(diào)用函數(shù)時只重新指定數(shù)組中的可變內(nèi)容的情況下并不是必要的。

一種解決方案是將這個數(shù)組聲明為全局變量,這樣就可以重用這個數(shù)組,而不必每次都建立新數(shù)組。但這個方案的結(jié)果是,除了引用函數(shù)的全局變量會使用這個緩沖數(shù)組外,還會多出一個全局屬性引用數(shù)組自身。如此不僅使代碼變得不容易管理,而且,如果要在其他地方使用這個數(shù)組時,開發(fā)者必須要再次定義函數(shù)和數(shù)組。這樣一來,也使得代碼不容易與其他代碼整合,因為此時不僅要保證所使用的函數(shù)名在全局命名空間中是唯一的,而且還要保證函數(shù)所依賴的數(shù)組在全局命名空間中也必須是唯一的。

而通過閉包可以使作為緩沖器的數(shù)組與依賴它的函數(shù)關(guān)聯(lián)起來(優(yōu)雅地打包),同時也能夠維持在全局命名空間外指定的緩沖數(shù)組的屬性名,免除了名稱沖突和意外交互的危險。

其中的關(guān)鍵技巧在于通過執(zhí)行一個單行(in-line)函數(shù)表達式創(chuàng)建一個額外的執(zhí)行環(huán)境,而將該函數(shù)表達式返回的內(nèi)部函數(shù)作為在外部代碼中使用的函數(shù)。此時,緩沖數(shù)組被定義為函數(shù)表達式的一個局部變量。這個函數(shù)表達式只需執(zhí)行一次,而數(shù)組也只需創(chuàng)建一次,就可以供依賴它的函數(shù)重復(fù)使用。

下面的代碼定義了一個函數(shù),這個函數(shù)用于返回一個 HTML 字符串,其中大部分內(nèi)容都是常量,但這些常量字符序列中需要穿插一些可變的信息,而可變的信息由調(diào)用函數(shù)時傳遞的參數(shù)提供。

通過執(zhí)行單行函數(shù)表達式返回一個內(nèi)部函數(shù),并將返回的函數(shù)賦給一個全局變量,因此這個函數(shù)也可以稱為全局函數(shù)。而緩沖數(shù)組被定義為外部函數(shù)表達式的一個局部變量。它不會暴露在全局命名空間中,而且無論什么時候調(diào)用依賴它的函數(shù)都不需要重新創(chuàng)建這個數(shù)組。

/* 聲明一個全局變量 - getImgInPositionedDivHtml -并將一次調(diào)用一個外部函數(shù)表達式返回的內(nèi)部函數(shù)賦給它。這個內(nèi)部函數(shù)會返回一個用于表示絕對定位的 DIV 元素包圍著一個 IMG 元素 的 HTML 字符串,這樣一來,所有可變的屬性值都由調(diào)用該函數(shù)時的參數(shù)提供:*/var getImgInPositionedDivHtml = (function(){/* 外部函數(shù)表達式的局部變量 - buffAr - 保存著緩沖數(shù)組。這個數(shù)組只會被創(chuàng)建一次,生成的數(shù)組實例對內(nèi)部函數(shù)而言永遠是可用的因此,可供每次調(diào)用這個內(nèi)部函數(shù)時使用。其中的空字符串用作數(shù)據(jù)占位符,相應(yīng)的數(shù)據(jù)將由內(nèi)部函數(shù)插入到這個數(shù)組中:*/var buffAr = [‘<div id=”‘,”, //index 1, DIV ID 屬性‘” style=”position:absolute;top:’,”, //index 3, DIV 頂部位置‘px;left:’,”, //index 5, DIV 左端位置‘px;width:’,”, //index 7, DIV 寬度‘px;height:’,”, //index 9, DIV 高度‘px;overflow:hidden;”><img src=”‘,”, //index 11, IMG URL‘” width=”‘,”, //index 13, IMG 寬度‘” height=”‘,”, //index 15, IMG 高度‘” alt=”‘,”, //index 17, IMG alt 文本內(nèi)容‘”></div>’];/* 返回作為對函數(shù)表達式求值后結(jié)果的內(nèi)部函數(shù)對象。這個內(nèi)部函數(shù)就是每次調(diào)用執(zhí)行的函數(shù)- getImgInPositionedDivHtml( … ) -*/return (function(url, id, width, height, top, left, altText){/* 將不同的參數(shù)插入到緩沖數(shù)組相應(yīng)的位置:*/buffAr[1] = id;buffAr[3] = top;buffAr[5] = left;buffAr[13] = (buffAr[7] = width);buffAr[15] = (buffAr[9] = height);buffAr[11] = url;buffAr[17] = altText;/* 返回通過使用空字符串(相當于將數(shù)組元素連接起來)連接數(shù)組每個元素后形成的字符串:*/return buffAr.join(”);}); //:內(nèi)部函數(shù)表達式結(jié)束。})();/*^^- :單行外部函數(shù)表達式。*/

如果一個函數(shù)依賴于另一(或多)個其他函數(shù),而其他函數(shù)又沒有必要被其他代碼直接調(diào)用,那么可以運用相同的技術(shù)來包裝這些函數(shù),而通過一個公開暴露的函數(shù)來調(diào)用它們。這樣,就將一個復(fù)雜的多函數(shù)處理過程封裝成了一個具有移植性的代碼單元。

其他例子

有關(guān)閉包的一個可能是最廣為人知的應(yīng)用是 Douglas Crockford’s technique for the emulation of private instance variables in ECMAScript objects。這種應(yīng)用方式可以擴展到各種嵌套包含的可訪問性(或可見性)的作用域結(jié)構(gòu),包括 the emulation of private static members for ECMAScript objects。

閉包可能的用途是無限的,可能理解其工作原理才是把握如何使用它的最好指南。

意外的閉包

返回目錄

在創(chuàng)建可訪問的內(nèi)部函數(shù)的函數(shù)體之外解析該內(nèi)部函數(shù)就會構(gòu)成閉包。這表明閉包很容易創(chuàng)建,但這樣一來可能會導(dǎo)致一種結(jié)果,即沒有認識到閉包是一種語言特性的 JavaScript 作者,會按照內(nèi)部函數(shù)能完成多種任務(wù)的想法來使用內(nèi)部函數(shù)。但他們對使用內(nèi)部函數(shù)的結(jié)果并不明了,而且根本意識不到創(chuàng)建了閉包,或者那樣做意味著什么。

正如下一節(jié)談到 IE 中內(nèi)存泄漏問題時所提及的,意外創(chuàng)建的閉包可能導(dǎo)致嚴重的負面效應(yīng),而且也會影響到代碼的性能。問題不在于閉包本身,如果能夠真正做到謹慎地使用它們,反而會有助于創(chuàng)建高效的代碼。換句話說,使用內(nèi)部函數(shù)會影響到效率。

使用內(nèi)部函數(shù)最常見的一種情況就是將其作為 DOM 元素的事件處理器。例如,下面的代碼用于向一個鏈接元素添加 onclick 事件處理器:

/* 定義一個全局變量,通過下面的函數(shù)將它的值作為查詢字符串的一部分添加到鏈接的 - href - 中:*/var quantaty = 5;/* 當給這個函數(shù)傳遞一個鏈接(作為函數(shù)中的參數(shù) - linkRef -)時,會將一個 onclick 事件處理器指定給該鏈接,該事件處理器將全局變量 - quantaty - 的值作為字符串添加到鏈接的 - href -屬性中,然后返回 true 使該鏈接在單擊后定位到由 - href -屬性包含的查詢字符串指定的資源:*/function addGlobalQueryOnClick(linkRef){/* 如果可以將參數(shù) - linkRef - 通過類型轉(zhuǎn)換為 ture(說明它引用了一個對象):*/if(linkRef){/* 對一個函數(shù)表達式求值,并將對該函數(shù)對象的引用指定給這個鏈接元素的 onclick 事件處理器:*/linkRef.onclick = function(){/* 這個內(nèi)部函數(shù)表達式將查詢字符串添加到附加事件處理器的元素的 - href - 屬性中:*/this.href += (’?quantaty=’+escape(quantaty));return true;};}}

無論什么時候調(diào)用 addGlobalQueryOnClick 函數(shù),都會創(chuàng)建一個新的內(nèi)部函數(shù)(通過賦值構(gòu)成了閉包)。從效率的角度上看,如果只是調(diào)用一兩次 addGlobalQueryOnClick 函數(shù)并沒有什么大的妨礙,但如果頻繁使用該函數(shù),就會導(dǎo)致創(chuàng)建許多截然不同的函數(shù)對象(每對內(nèi)部函數(shù)表達式求一次值,就會產(chǎn)生一個新的函數(shù)對象)。

上面例子中的代碼沒有關(guān)注內(nèi)部函數(shù)在創(chuàng)建它的函數(shù)外部可以訪問(或者說構(gòu)成了閉包)這一事實。實際上,同樣的效果可以通過另一種方式來完成。即單獨地定義一個用于事件處理器的函數(shù),然后將該函數(shù)的引用指定給元素的事件處理屬性。這樣,只需創(chuàng)建一個函數(shù)對象,而所有使用相同事件處理器的元素都可以共享對這個函數(shù)的引用:

/* 定義一個全局變量,通過下面的函數(shù)將它的值作為查詢字符串的一部分添加到鏈接的 - href - 中:*/var quantaty = 5;/* 當把一個鏈接(作為函數(shù)中的參數(shù) - linkRef -)傳遞給這個函數(shù)時,會給這個鏈接添加一個 onclick 事件處理器,該事件處理器會將全局變量 - quantaty - 的值作為查詢字符串的一部分添加到鏈接的 - href - 中,然后返回 true,以便單擊鏈接時定位到由作為 - href - 屬性值的查詢字符串所指定的資源:*/function addGlobalQueryOnClick(linkRef){/* 如果 - linkRef - 參數(shù)能夠通過類型轉(zhuǎn)換為 true(說明它引用了一個對象):*/if(linkRef){/* 將一個對全局函數(shù)的引用指定給這個鏈接的事件處理屬性,使函數(shù)成為鏈接元素的事件處理器:*/linkRef.onclick = forAddQueryOnClick;}}/* 聲明一個全局函數(shù),作為鏈接元素的事件處理器,這個函數(shù)將一個全局變量的值作為要添加事件處理器的鏈接元素的 - href - 值的一部分:*/function forAddQueryOnClick(){this.href += (’?quantaty=’+escape(quantaty));return true;}

在上面例子的第一個版本中,內(nèi)部函數(shù)并沒有作為閉包發(fā)揮應(yīng)有的作用。在那種情況下,反而是不使用閉包更有效率,因為不用重復(fù)創(chuàng)建許多本質(zhì)上相同的函數(shù)對象。

類似地考量同樣適用于對象的構(gòu)造函數(shù)。與下面代碼中的構(gòu)造函數(shù)框架類似的代碼并不罕見:

function ExampleConst(param){/* 通過對函數(shù)表達式求值創(chuàng)建對象的方法,并將求值所得的函數(shù)對象的引用賦給要創(chuàng)建對象的屬性:*/this.method1 = function(){… // 方法體。};this.method2 = function(){… // 方法體。};this.method3 = function(){… // 方法體。};/* 把構(gòu)造函數(shù)的參數(shù)賦給對象的一個屬性:*/this.publicProp = param;}

每當通過 new ExampleConst(n) 使用這個構(gòu)造函數(shù)創(chuàng)建一個對象時,都會創(chuàng)建一組新的、作為對象方法的函數(shù)對象。因此,創(chuàng)建的對象實例越多,相應(yīng)的函數(shù)對象也就越多。

Douglas Crockford 提出的模仿 JavaScript 對象私有成員的技術(shù),就利用了將對內(nèi)部函數(shù)的引用指定給在構(gòu)造函數(shù)中構(gòu)造對象的公共屬性而形成的閉包。如果對象的方法沒有利用在構(gòu)造函數(shù)中形成的閉包,那么在實例化每個對象時創(chuàng)建的多個函數(shù)對象,會使實例化過程變慢,而且將有更多的資源被占用,以滿足創(chuàng)建更多函數(shù)對象的需要。

這那種情況下,只創(chuàng)建一次函數(shù)對象,并把它們指定給構(gòu)造函數(shù) prototype 的相應(yīng)屬性顯然更有效率。這樣一來,它們就能被構(gòu)造函數(shù)創(chuàng)建的所有對象共享了:

function ExampleConst(param){/* 將構(gòu)造函數(shù)的參數(shù)賦給對象的一個屬性:*/this.publicProp = param;}/* 通過對函數(shù)表達式求值,并將結(jié)果函數(shù)對象的引用指定給構(gòu)造函數(shù)原型的相應(yīng)屬性來創(chuàng)建對象的方法:*/ExampleConst.prototype.method1 = function(){… // 方法體。};ExampleConst.prototype.method2 = function(){… // 方法體。};ExampleConst.prototype.method3 = function(){… // 方法體。};

Internet Explorer 的內(nèi)存泄漏問題

返回目錄

Internet Explorer Web 瀏覽器(在 IE 4 到 IE 6 中核實)的垃圾收集系統(tǒng)中存在一個問題,即如果 ECMAScript 和某些宿主對象構(gòu)成了 “循環(huán)引用”,那么這些對象將不會被當作垃圾收集。此時所謂的宿主對象指的是任何 DOM 節(jié)點(包括 document 對象及其后代元素)和 ActiveX 對象。如果在一個循環(huán)引用中包含了一或多個這樣的對象,那么這些對象直到瀏覽器關(guān)閉都不會被釋放,而它們所占用的內(nèi)存同樣在瀏覽器關(guān)閉之前都不會交回系統(tǒng)重用。

當兩個或多個對象以首尾相連的方式相互引用時,就構(gòu)成了循環(huán)引用。比如對象 1 的一個屬性引用了對象 2 ,對象 2 的一個屬性引用了對象 3,而對象 3 的一個屬性又引用了對象 1。對于純粹的 ECMAScript 對象而言,只要沒有其他對象引用對象 1、2、3,也就是說它們只是相互之間的引用,那么仍然會被垃圾收集系統(tǒng)識別并處理。但是,在 Internet Explorer 中,如果循環(huán)引用中的任何對象是 DOM 節(jié)點或者 ActiveX 對象,垃圾收集系統(tǒng)則不會發(fā)現(xiàn)它們之間的循環(huán)關(guān)系與系統(tǒng)中的其他對象是隔離的并釋放它們。最終它們將被保留在內(nèi)存中,直到瀏覽器關(guān)閉。

閉包非常容易構(gòu)成循環(huán)引用。如果一個構(gòu)成閉包的函數(shù)對象被指定給,比如一個 DOM 節(jié)點的事件處理器,而對該節(jié)點的引用又被指定給函數(shù)對象作用域中的一個活動(或可變)對象,那么就存在一個循環(huán)引用。DOM_Node.onevent ->function_object.[[scope]] ->scope_chain ->Activation_object.nodeRef ->DOM_Node。形成這樣一個循環(huán)引用是輕而易舉的,而且稍微瀏覽一下包含類似循環(huán)引用代碼的網(wǎng)站(通常會出現(xiàn)在網(wǎng)站的每個頁面中),就會消耗大量(甚至全部)系統(tǒng)內(nèi)存。

多加注意可以避免形成循環(huán)引用,而在無法避免時,也可以使用補償?shù)姆椒?,比如使?IE 的 onunload 事件來來清空(null)事件處理函數(shù)的引用。時刻意識到這個問題并理解閉包的工作機制是在 IE 中避免此類問題的關(guān)鍵。

本站僅提供存儲服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊舉報。
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
JavaScript秘密花園
JS 語言核心(JavaScript權(quán)威指南第六版)(閱讀筆記)
函數(shù)(Functions)
高性能Javascript:高效的數(shù)據(jù)訪問
前端常見20道高頻面試題深入解析
JavaScript執(zhí)行環(huán)境+變量對象+作用域鏈+閉包
更多類似文章 >>
生活服務(wù)
熱點新聞
分享 收藏 導(dǎo)長圖 關(guān)注 下載文章
綁定賬號成功
后續(xù)可登錄賬號暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點擊這里聯(lián)系客服!

聯(lián)系客服