国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

先有蛋還是先有雞?JavaScript 作用域與閉包探析

elisa.yang / 449人閱讀

摘要:而閉包的神奇之處正是可以阻止事情的發生。拜所聲明的位置所賜,它擁有涵蓋內部作用域的閉包,使得該作用域能夠一直存活,以供在之后任何時間進行引用。依然持有對該作用域的引用,而這個引用就叫閉包。

引子

先看一個問題,下面兩個代碼片段會輸出什么?

// Snippet 1
a = 2;
var a;
console.log(a);

// Snippet 2
console.log(a);
var a = 2;

如果了解過 JavaScript 變量提升相關語法的話,答案是顯而易見的。本文作為《你不知道的 JavaScript》第一部分的閱讀筆記,順便來總結一下對作用域與閉包的理解。

一、先有蛋還是先有雞

上面問題的答案是:

-> 2

-> undefined

我們從編譯器的角度思考:

引擎會在解釋 JavaScript 代碼之前首先對其進行編譯(沒錯,JavaScript 也是要進行編譯的!),而編譯階段中的一部分工作就是找到所有聲明,并用合適的作用域將他們關聯起來,即 包括變量和函數在內的所有聲明都會在任何代碼被執行前首先被處理

當你看到 var a = 2;時可能會認為這是一個聲明,但 JavaScript 實際上會將其看成兩個聲明:var aa = 2,第一個定義聲明是在編譯階段進行的,第二個賦值聲明會被留在原地等待執行階段處理。

打個比方,這個過程就好像變量和函數聲明從它們的代碼中出現的位置被“移動”到了最上面,這個過程就叫做 提升

所以,編譯之后上面兩個代碼片段是這樣的:

// Snippet 1 編譯后
var a;
a = 2;
console.log(a);    // -> 2

// Snippet 2 編譯后
var a;
console.log(a);    // -> undefined
a = 2;

所以結論就是:先有蛋(聲明),后有雞(賦值)

二、編譯

實際上,JavaScript 也是一門編譯語言。與傳統編譯語言的過程一樣,程序中的一段源代碼在執行之前會經過是三個步驟,統稱為“編譯”:

分詞/詞法分析(Tokenizing/Lexing)

解析/語法分析(Parsing)

代碼生成

簡單來說,任何 JavaScript 代碼片段在執行前都要進行編譯(通常就在執行前)。

三、作用域

為了理解作用域,可以想象出有以下三種角色:

引擎:從頭到尾負責整個 JavaScript 程序的編譯及執行過程。

編譯器:引擎的好朋友之一,負責語法分析及代碼生成等臟活累活。

作用域:引擎的另一位好朋友,負責收集并維護所有聲明的標識符(變量)組成的一系列查詢,并實施一套非常嚴格的規則,確定當前執行的代碼對這些標識符的訪問權限。

var a = 2; 為例,過程如下:

首先遇到 var a,編譯器會詢問作用域是否已經有一個名為 a 的變量存在于同一個作用域的集合中。如果是,編譯器會忽略該聲明,繼續進行編譯;否則就會要求作用域在當前作用域的集合中聲明一個新的變量,并命名為 a.

然后,編譯器會為引擎生成運行時所需的代碼,這些代碼被用來處理 a=2 這個賦值操作。引擎運行時會首選詢問作用域,在當前的作用域集合中是否存在一個叫做 a 的變量。如果是,引擎就會使用這個變量;如果否,引擎就會繼續查找該變量(一層一層向上查找)。

最后,如果引擎最終找到了a變量,就會將 2 賦值給它,否則引擎就會舉手示意并拋出一個異常(ReferenceError)!

當一個塊或函數嵌套在另一個塊或函數中時,就發生了作用域的嵌套。遍歷嵌套作用域鏈的規則很簡單:引擎從當前的執行作用域開始查找變量,如果找不到,就向上一級查找。當抵達最外層的全局作用域時,無論找到還是沒找到,查找過程都會停止

四、函數聲明式 & 函數表達式

JavaScript 中創建函數有兩種方式:

// 函數聲明式 
function funcDeclaration() { 
    return "A function declaration"; 
} 

// 函數表達式 
var funcExpression = function () { 
    return "A function expression"; 
}

聲明式與表達式的差異:

類似于 var 聲明,函數聲明可以 提升 到其它代碼之前,但函數表達式不能,不過允許保留在本地變量范圍內;

函數表達式可以匿名,而函數聲明不可以。

怎么判斷是函數聲明式還是函數表達式?

一個最簡單的方法是看 function 關鍵字出現在聲明的位置,如果是在第一個詞,那么就是函數聲明式,否則就是函數表達式。

函數表達式比函數聲明式更加有用的地方:

是一個閉包

可以作為其他函數的參數

可以作為立即調用函數表達式(IIFE

可以作為回調函數

五、匿名函數 & 立即調用函數

“在任意代碼片段外部添加包裝函數,可以將內部的變量和函數定義“隱藏起來”,外部作用域就無法訪問包裝函數內部的任何內容。那么,能否更徹底一些?如果必須聲明一個有具體名字的函數,這個名字本身就會“污染”所在作用域;其次,必須顯式通過函數名調用這個函數才能運行其中的代碼。如果函數不需要函數名(或者至少函數名可以不污染所在作用域),并且能夠自動運行,這就完美了!”——論匿名函數和理解調用函數的誕生。

匿名函數表達式最熟悉的場景就是回調函數:

setTimeout(function(){
    console.log("I waited 1 second!");
}, 1000);

匿名函數表達式書寫起來簡單快捷,很多庫和工具也傾向鼓勵使用這種風格的代碼。但是,它也有幾個缺點需要考慮:

匿名函數在棧追蹤中不會顯示出有意義的函數名,使得調試很困難。

如果沒有函數名,當函數需要引用自身時只能使用已經過期的 arguments.callee 引用,比如在遞歸中。另一個函數需要引用自身的例子,是在事件觸發后事件監聽器需要解綁自身。

匿名函數省略了對于代碼可讀性、可理解性很重要的函數名。一個描述性的名稱可以讓代碼不言自明。

所以,始終給函數表達式命名是一個最佳實踐:

setTimeout(function timeoutHandler(){
    console.log("I waited 1 second!");
});

由于函數被包含在一對()括號內部,因此成為了一個表達式,通過在末尾加上另外一個()括號就可以立即執行這個函數,比如:

(function foo(){
    // ...
})()

第一個()將函數變成了表達式,第二個()執行了這個函數。

它有個術語:IIFE,表示:立即執行函數表達式(Immediately Invoked Function Expression)

它有另外一個改進形式:

(function foo(){
    // ...
}())    

不同點就是把最后的括號挪進去了,實際上 這兩種形式在功能上是一致的,選擇哪個全憑個人喜好

至于 IIFE 的另一個非常普遍的進階用法是 把它們當做函數調用并傳遞參數進去

var a = 2;
(function foo(global){
    var a = 3;
    console.log(a);    // -> 3
    console.log(global.a);    // -> 2
})(window);    // 傳入window對象的引用
console.log(a);    // -> 2
六、再談提升

現在我們再來談一談提升。

// Snippet 3
foo();    // -> TypeError
bar();    // -> ReferenceError
var foo = function bar(){
    console.log(1);
};

為什么會輸出上面這兩個異常?我們可以從編譯器的角度把代碼看出這樣子:

var foo;    // 聲明提升
foo();      // 聲明但未定義為 undefined,然后這里進行了函數調用,所以返回 TypeError
bar();      // 無聲明拋出引用異常,所以返回 ReferenceError
foo = function bar(){
    console.log(1);    
};

然后再變化一下,同名的函數聲明和變量聲明在提升階段會怎么處理:

foo();    // 到底會輸出什么?
var foo;
function foo(){
    console.log(1);
}
foo = function(){
    console.log(2);
}

上面代碼會被引擎理解為如下形式:

function foo(){
    console.log(1);
}
foo();    // -> 1
foo = function(){
    console.log(2);
}

解釋:var foo 盡管出現在 function foo() 的聲明之前,但它是重復的聲明(因此被忽略了),因為函數聲明會被提升到普通變量之前。即:函數聲明和變量聲明都會被提升,但函數會首先被提升,然后才是變量(這也從側面說明了在 JavaScript 中“函數是一等公民”)。

再來:

foo();    // -> 3
function foo(){
    console.log(1);
}
var foo = function(){
    console.log(2);
}
function foo(){
    console.log(3);
}

解釋:盡管重復的 var 聲明會被忽略掉,但出現后面的函數聲明還是可以覆蓋前面的。

七、閉包

閉包是基于詞法作用域書寫代碼時所產生的自然結果,你甚至不需要為了利用它們而有意識地創建閉包。閉包的創建和使用在你的代碼中隨處可見。你缺少的是根據你自己的意愿來識別、擁抱和影響閉包的思維環境。

當函數可以記住并訪問所在的詞法作用域時,就產生了 閉包,即使函數是在當前詞法作用域之外執行。

function foo(){
    var a = 2;
    function bar(){
        console.log(a);
    }
    return bar;
}
var baz = foo();
baz();    // -> 2,閉包的效果!

以下是解釋說明:

函數 bar() 的詞法作用域能夠訪問 foo() 的內部作用域,然后我們將 bar() 函數本身當做一個值類型緊傳遞。在這個例子中,我們將 bar() 所引用的函數對象本身當做返回值。

foo()執行后,其返回值(也就是內部的 bar() 函數)賦值給變量 baz 并調用 baz(),實際上只是通過不同的標識符引用調用了內部的函數 bar()

bar() 顯示是可以被正常執行,但是在這個例子中,它在自己定義的詞法作用域以外的地方執行。

foo() 執行后,通常會期待 foo() 的整個內部作用域都被銷毀,因為我們知道引擎有垃圾回收器用來釋放不再使用的內存空間。由于看上去 foo() 的內容不會再被使用,所以很自然地會考慮對其進行回收。

而閉包的神奇之處正是可以阻止事情的發生。事實上,內部作用域依然存在,因此沒有被回收。誰在使用這個內部作用域?原來是 bar() 本身在使用。

bar() 所聲明的位置所賜,它擁有涵蓋 foo() 內部作用域的閉包,使得該作用域能夠一直存活,以供 bar() 在之后任何時間進行引用。

bar() 依然持有對該作用域的引用,而 這個引用就叫閉包

本質上,無論何時何地,如果將函數(訪問它們各自的詞法作用域)當做第一級的值類型并到處傳遞,你就會看到閉包在這些函數中的應用。在定時器、事件監聽器、Ajax 請求、跨窗口通信、Web Workers 或者任何其他的異步(或者同步)任務中,只要使用了回調函數,實際上就是在使用閉包

再補充一個示例:

function foo() {
    function bar() {
        console.log("1");
    }
    function baz() {
        console.log("2");
    }

    var yyy = {
        bar: bar,
        baz: baz
    }
    return yyy;
}

var kkk = foo();    // kkk通過foo獲得了yyy的引用,也就可以調用bar和baz
        
kkk.bar();    // -> 1
kkk.baz();    // -> 2
九、動態作用域

事實上,JavaScript 并不具有動態作用域,它只有 詞法作用域(雖然 this 機制某種程度上很像動態作用域)。詞法作用域和動態作用域的主要區別為:

詞法作用域是在寫代碼或者定義時確定的,而動態作用域是在運行時確定的;

詞法作用域關注函數在何處聲明,而動態作用域關注函數從何處調用。

像下面的代碼片段,如果是動態作用域輸出的就是3而不是2了:

function foo(){
    console.log(a);    // -> 2
}
function bar(){
    var a = 3;
    foo();
}
var a = 2;
bar();
十、參考

你不知道的 JavaScript(上卷)

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。

轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/88139.html

相關文章

  • 你不知道的提升 - 先有還是有蛋

    摘要:一先有雞還有先有蛋直覺上會認為代碼在執行時是由上到下一行一行執行的。不幸的是兩種猜測都是不對的。換句話說,我們的問題先有雞還是先有蛋的結論是先有蛋聲明后有雞賦值。 一、先有雞還有先有蛋? 直覺上會認為javascript代碼在執行時是由上到下一行一行執行的。但實際上這并不完全正確,有一種特殊情況會導致這個假設是錯誤的。 a = 2; var a; console.log(a); 大家...

    fish 評論0 收藏0
  • JavaScript 原型中的哲學思想

    摘要:而作為構造函數,需要有個屬性用來作為以該構造函數創造的實例的繼承。 歡迎來我的博客閱讀:「JavaScript 原型中的哲學思想」 記得當年初試前端的時候,學習JavaScript過程中,原型問題一直讓我疑惑許久,那時候捧著那本著名的紅皮書,看到有關原型的講解時,總是心存疑慮。 當在JavaScript世界中走過不少旅程之后,再次萌發起研究這部分知識的欲望,翻閱了不少書籍和資料,才搞懂...

    sugarmo 評論0 收藏0
  • 如何優雅的理解ECMAScript中的對象

    摘要:標準對象,語義由本規范定義的對象。這意味著雖然有,本質上依然是構造函數,并不能像那樣表演多繼承嵌套類等高難度動作。不過這里的并不是我們所說的數據類型,而是對象構造函數。 序 ECMAScript is an object-oriented programming language for performing computations and manipulating computat...

    why_rookie 評論0 收藏0
  • 原型鏈是什么?關于原型鏈中constructor、prototype及__proto__之間關系的認

    摘要:的隱式原型是母,母是由構造函數構造的,但函數的隱式原型又是。。。。可能是考慮到它也是由構造函數生成的吧,所以返回的值也是。 showImg(https://segmentfault.com/img/bVyLk0); 首先,我們暫且把object類型和function類型分開來,因為 function是一個特殊的對象類型,我們這里這是便于區分,把function類型單獨拿出來。順便一提,...

    kaka 評論0 收藏0
  • YouDontKnowJS 小黃書學習小結

    摘要:真正的理解閉包的原理與使用更加透徹綁定的四種規則機制你不知道的人稱小黃書,第一次看到這本書名就想到了一句話你懂得,翻閱后感覺到很驚艷,分析的很透徹,學習起來也很快,塊級作用域語句語句相當于比較麻煩而且用在對象上創建的塊作用域僅僅在聲明中有效 真正的理解閉包的原理與使用 更加透徹this綁定的四種規則機制 你不知道的JavaScript 人稱小黃書,第一次看到這本書名 就想到了一句話...

    Yuqi 評論0 收藏0

發表評論

0條評論

elisa.yang

|高級講師

TA的文章

閱讀更多
最新活動
閱讀需要支付1元查看
<