理javascript中的三座大山:作用域、原型、異步

this指向引用、原型鏈之類。這些模糊的概念,隱藏着javascript微妙之處;真正搞懂這些知識,對於理解javascript之門語言有很大的幫。

javascript中的三座大山:作用域、原型、異步。

兩個概念的知識點和個人的理解;關於異步的概念,單獨講起來也是很長的一個篇幅,可以嘗試從回調函數——jquery中的異步處理——ES6中的Promise——Generator——ES7中的async/await。

本文從:作用域和閉包+this和對象原型,這兩部分入手。那作者又是如何展開的呢?

任何編程語言最基本的功能都離不開存儲和訪問變量。要想存儲和訪問變量那就離不開作用域。作用域說白了就是一套規則,一套存儲變量,並可以訪問變量的規則。作用域類型可以分爲:詞法作用域、函數作用域以及塊作用域。涉及到詞法作用域就不能不提閉包這個非常有用而又神祕的概念。當某個函數可以記住並訪問所在的詞法作用域,且在當前詞法作用域之外執行時就產生了閉包。當你能真正理解了閉包之後,你慢慢就可以理解並實現模塊機制。想必模塊思想不用多說,不管你是用vue還是react這樣的開發框架,模塊化的思想已經深入人心。模塊化思想具有清晰,簡潔,可複用的優點。 javascript中的作用域就是詞法作用域,javascript中的詞法作用域是一種靜態的概念,既在代碼的書寫階段就已經確定(如果不使用eval、with這類改變詞法作用域的方法);

同時javascript中還存在某種很像動態作用域的機制,它就是this。this是一個很有趣的概念,它的動態綁定的特性決定了它的指向是一個比較難以理解的概念。根據調用位置的不同,this會綁定到不同的對象。說到對象,就引出了javascript中的一個精髓所在,原型鏈、原型繼承。這是一個容易混淆的概念,經常會跟面向對象的類設計模式混淆。說到底javascript中的針對的是對象,對象之間的關聯是委託關係。這部分內容經常跟模擬實現的類模式混在一起,並且很多語法糖和使用方法都在造成一種javascript類模式的錯覺。不管是模擬實現的類的設計模式,還是對象關聯的委託設計模式,其本質都是基於對象原型prototype實現的。至於使用哪種設計模式,仁者見仁,智者見智。

作用域和閉包

作用域是什麼

作用域是一套規則,用來存儲和訪問變量。任何編程語言都不開作用域,正是作用域這種存儲和訪問變量的能力將狀態帶給了程序,賦予了編程語言可以實現豐富功能的能力。

講到作用域就不得不提兩個重要角色:引擎和編譯器。引擎從頭到尾負責整個javascript程序的編譯和執行過程。編譯器負責詞法分析、語法分析、代碼生成等髒活累活。

javascript是一種編譯型的語言,但它不是提前編譯的,它的編譯發生在在代碼執行前的幾微秒。傳統的編譯過程分爲三個階段

詞法分析——詞法分析階段主要將程序代碼拆分成一個個的詞法單元。

比如將var a = 2;拆分爲var、a、=、2、;

語法分析——語法分析階段主要將詞法單元按照程序中的嵌套邏輯轉換成“抽象語法樹”。

代碼生成——代碼生成就是將抽象語法樹裝換爲可執行代碼的過程。

javascript的編譯過程比上面稍微複雜點,會在語法分析和代碼生成階段進行性能優化;一邊編譯優化、一邊執行。

當一段程序被編譯器編譯完生成可執行代碼,然後引擎執行它時,會對其中的變量進行查詢,這個查詢過程在作用域協助下,會從當前作用域開始,冒泡向上查找,找到即停止;如果沒有找到,會一層層的嵌套進行,直到全局作用域爲止。

這個過程中變量查詢主要分爲:LHS查詢(賦值操作的目標)和RHS查詢(賦值操作的源頭);如果全局作用域中也沒找到,會根據查詢類型不同拋出不同錯誤。LHS查詢在嚴格模式下拋出ReferenceError,非嚴格模式下隱式的創建全局變量;RHS拋出ReferenceError錯誤。

詞法作用域

javascript中的絕大部分作用域都是詞法作用。詞法作用域是在詞法階段定義的作用域,所以在書寫代碼階段已經確定,是個靜態的概念,它跟後面的this動態的概念形成對比。

有兩種方法可以修改詞法作用域:eval和with。

eval——通過代碼中某處插入新的代碼並運行,從而在代碼執行階段改變所在的詞法作用域。

function foo(str,a) {

    eval(str);

    console.log(a,b);

}

foo("var b = 2;", 1); //1,2

with——常被用作重複引用一個對象中多個屬性的快捷方式。接受一個對象作爲參數,從而將一個對象處理爲詞法作用,創建一個全新的詞法作用域。

function foo(obj) {

    with(obj){

        a = 2;

    }

}

var obj1 = {a: 3};

var obj2 = {b: 3};

foo(obj1);

console.log(obj1.a); //2

foo(obj2);

console.log(obj2.a); //undefined

console.log(a); //2

不提倡使用這兩種方法,會導致性能下降,使得代碼執行變慢。因爲引擎在優化代碼的時候依賴於詞法進行靜態分析,而它們會在代碼執行階段改變詞法作用域,導致在編譯階段引擎無法對代碼進行優化,使得性能下降。同時嚴格模式下with的行爲也會被禁止。綜上,不建議使用eval和with這兩種方法。

函數作用域和塊作用域

前一章說到了作用域,那麼javascript中不僅有函數作用域還存在塊作用域。

【函數作用域】函數作用域中屬於這個函數的全部變量都可以在整個函數範圍內使用及複用。

除了正常的聲明一個函數然後定義函數外,我們還可以使用函數來包裹一個代碼塊,從而實現將代碼塊中的變量隱藏起來實現最小暴露原則;即只暴露那些必要的變量或是函數,從而規避一些命名衝突。

這裏有兩個典型的做法:

第一種就是全局空間命名,用一個複雜的名字來定義某個對象,然後將要暴露出來的變量作爲該對象的屬性,規避變量名的衝突;

第二種就是模塊管理,強制所有標識符都不能注入共享的作用域中去。

但是利用函數包裹代碼塊的做法本來就會帶來兩個問題:

函數的聲明本身就是對所在作用域的一種污染;

函數需要調用運行纔行。

於是我們就想到了使用立即執行的匿名函數表達式。匿名函數相比於具名函數存在三個缺點:

難以調試,追蹤棧中不顯示有意義的名字;

難以調用,沒有名字無法直接調用;

難以理解,沒有可讀性的名字。

所以一般不建議使用匿名函數,建議使用具名的函數以替代匿名函數。

關於立即執行函數表達式:

可以被當做函數調用傳遞參數進去;

可以用來倒置代碼的運行順序。

具體是否使用函數作用域,可以根據具體場景來定。

【塊作用域】使用函數的時候,我們可以通過函數作用域來規避一些潛在的衝突。但是當我們使用代碼塊的時候,就要時刻注意在作用範圍之外可能存在的變量衝突。典型的:

for(var i=0; i < 10; i++) {

    console.log(i);

}

這裏的i會被定義到全局,導致讓人意想不到的衝突。

爲了解決這樣的問題,引入塊作用域可以很好的解決這樣的問題。

js中本身不存在塊作用域,我們可以使用一下四種方式模擬實現塊作用域。

with:在傳入的對象中創建了塊作用域;

try/with:catch分句中可以創建塊作用域。可以用來模擬實現ES6之前的環境作爲塊作用域的替代方案;

let:可以在任意代碼塊中隱式的創建或是劫持塊作用域;

const:同樣可以用來創造塊作用域;

不管是函數作用域還是塊作用域,任何聲明在某個作用域內的變量,都將附屬於這個作用域。

提升

上一部分說到,任何聲明在某個作用域的變量都將附屬於這個作用域。

但是變量在作用域中聲明的位置與作用域存在微妙的聯繫。不管是變量賦值還是函數定義,所有聲明都會提升到各自作用域最頂部。

我們編寫的程序會經過編譯器進行編譯,然後由引擎執行。

這裏的提升發生在編譯器的編譯階段,也就是說變量、函數聲明都會進行提升,但是發生在執行階段的變量賦值、函數執行不會。

提升會限制各自所在的作用域中進行。

函數的提升優先於變量提升

同時針對於函數提升有三個注意點:

函數聲明可以提升,但是函數表達式不提升,具名的函數表達式的標識符也不會提升。

同名的函數聲明,後面的覆蓋前面的。

函數聲明的提升,不受邏輯判斷的控制。

作用域閉包

談完了作用域,那我們就不得不提基於作用域的一個特別重要的概念:閉包。

函數可以記住並訪問所在的詞法作用域時,就產生了閉包。它是基於作用域這個概念的。很典型的一個列子:

function foo(){

    var a = 2;

    function test(){

        console.log(a);

    }

    return test;

}

var func = foo();

func(); //2 ---這裏就是閉包的效果——引用了foo中詞法作用域中的變量

當某個函數可以記住並訪問所在的詞法作用域,那麼就可以在其他地方使用這個閉包;並且這個被記住的詞法作用域不會被銷燬,可以一直被引用。

同時閉包是javascript中一個很重要的概念,很多異步操作中都會使用到。比如:定時器、事件監聽器、ajax請求等。只要使用了回調函數,就一定存在閉包。

一個很常見的關於閉包的誤解經常發生在循環中。比如:

for(var i=0; i < 6; i++) {

    setTimeout(function(){

        console.log(i);

    },0);

}

這裏只會輸出6個6。【Tip:這裏定時器會等到循環執行完纔會執行內面的內容】

爲什麼會這樣呢?

這裏我們使用了閉包+塊代碼,其中塊代碼的作用域是全局的,所以當執行完循環之後運行setTimeout中閉包之後,其中引用的i就是全局公共區域中的i,也就是6。所以最終輸出6個6.

那麼如何達到輸出1到6的效果呢?

我們可以通過作用域+閉包,解決循環中存在的問題。

這裏作用域可以通過函數實現or塊作用域實現。

函數作用域:

for(var i=0; i < 6; i++) {

    (function(j) {

        setTimeout(function(){

            console.log(j);

        },0)

    })(i);

}

塊作用域:

for(let i=0; i < 6; i++) {

    setTimeout(function(){

        console.log(i);

    },0)

}

談到閉包,就不提一個很新的概念——模塊模式。

模塊模式其實就是藉助了閉包的思想。要實現一個模塊模式需要具備兩個必要條件:

外部包裹函數+函數至少被調用一次返回實例。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章