你不知道的Javascript動態做用域

前言

最近被一道面試題給難住了,其實就是說不清楚爲何是這個答案,有時候可能屏幕前的你,也會有這個疑惑,因此打算來補一補基礎-做用域。javascript

爲了更好的理解它,能夠看看往期的知識點:java

JavaScript執行上下文-執行棧git

先上題目:github

var Fn = function () {
        console.log(Fn);
    }
    Fn();

    var obj = {
        fn2 : function () {
            console.log(fn2);
        }
    }
    obj.fn2();
複製代碼

個人答案認爲兩個都是打印Function,其實基礎紮實的小夥伴估計明白我錯哪了。面試

話很少說,開始咱們的正題吧🤭安全

什麼是做用域

  1. 任何語言都有做用域的概念,那有些語言做用域是動態的,有些語言做用域是靜態的,我我的理解JavaScript做用域是靜態的,爲何這麼說,下面我會說明白的。
  2. 做用域能夠理解成:定義了一組明確的規則,它定義如何在某些位置存儲變量,以及如何在稍後找到這些變量。

那麼,就有人問了,做用域規則在哪裏,如何被設置呢?性能優化

官方給出解釋:點這裏閉包

那麼我在這裏就不咬文嚼字了,那麼咱們要探究的就是靜態的問題了🤭函數

靜態做用域與動態做用域

由於 JavaScript 採用的是詞法做用域,函數的做用域在函數定義的時候就決定了。工具

而與詞法做用域相對的是動態做用域,函數的做用域是在函數調用的時候才決定的。

讓咱們認真看個例子就能明白之間的區別:

var x = 10;
    function fn() {
        console.log(x);
    }
    fn()
    function show(fun) {
        var x = 20;
        fun()
    }
    show(fn);
複製代碼

假設JavaScript採用靜態做用域,讓咱們分析下執行過程:

執行fn函數,先從fn函數內部查找是否有局部變量x,若是沒有,就根據書寫的位置,查找上面一層的代碼,也就是 value 等於 1,因此結果會打印 1。

假設JavaScript採用靜態做用域,讓咱們分析下執行過程:

執行 fn函數,依然是從 fn 函數內部查找是否有局部變量 x。若是沒有,就從調用函數的做用域,也就是 show函數內部查找 x變量,因此結果會打印 2。

實際JavaScript打印的結果就是1,從結果上說明JavaScript是靜態做用域。

爲了更好的理解,經過畫一張簡單圖來理解靜態做用域:

這樣子就很好理解這個關係了,Fn函數在本身的做用域中找變量x,根據變量查找規則,若是沒有的話,會去上一級的做用域查找,也就是全局做用域,看是否存在變量x,有的話就取這個值,沒有的話就返回undefined。

**一旦找到第一個匹配,做用域查詢就中止了。**相同的標識符名稱能夠在嵌套做用域的多個層中被指定,這稱爲「遮蔽(shadowing)」(內部的標識符「遮蔽」了外部的標識符)。

上述這個查詢的過程,叫作做用域查詢,它老是從當前被執行的最內側的做用域開始,向外/向上不斷查找,直到第一個匹配才中止。

做用域分類

全局做用域

在代碼任何地方都能訪問到的對象擁有全局做用域,更深刻的瞭解能夠結合全局執行上下文。好比: JavaScript的全局對象 函數 變量都能在全局訪問到。

3種情形會擁有全局做用域

最外層函數以及最外層定義的變量屬於全局做用域
var demo = 1;            //全局變量
        let fn = () => {
            alert(demo)
            let inner = () => alert(demo); 
        }
        fn();  //1 
        inner()  //ReferenceError
複製代碼
在任何位置不使用var聲明的變量屬於全局做用域
var demo = 1;            //全局變量
        let fn = () => {
            demo1 = '未使用var定義'
            alert(demo)
            let inner = () => alert(demo1);
        }
        fn();  //1 
        console.log(window.demo1);   //未使用var定義
複製代碼
全部window對象的屬性屬於全局做用域

局部做用域/函數做用域

和全局做用域相反,函數做用域通常只在函數的代碼片斷內可訪問到,外部不能進行變量訪問。在函數內部定義的變量存在於函數做用域中,其生命週期隨着函數的執行結束而結束。例如:

let name = '李四'
        let  getName = () => {
            var name = '張三';
            alert(name); //張三
        }
        console.log(name); //李四
複製代碼

塊級做用域

在ES6中提出塊級做用域概念,它的用途就是:變量的聲明應該距離使用的地方越近越好。並最大限度的本地化。避免污染。

塊做用域由 { } 包括,let const能夠造成塊級做用域,也就是俗稱的暫時性死區。具體的在這裏就不詳細的介紹了,感興趣的能夠了解下以前的文章-JavaScript執行上下文-執行棧 這裏面講了爲何let const 會存在暫時性死區,原理是什麼?

動態做用域

與詞法做用域不一樣於在定義時肯定,動態做用域在執行時肯定,其生存週期到代碼片斷執行爲止。動態變量存在於動態做用域中,任何給定的綁定的值,在肯定調用其函數以前,都是不可知的。

從某種程度上來講,這會修改做用域,(也就是欺騙)詞法做用域。在你的代碼中建議不要使用它們,這是由於在某些方面: 欺騙詞法做用域會致使更低下的性能。

eval

JavaScript中的eval(..)函數接收一個字符串做爲參數值,並將這個字符串的內容看做是好像它已經被實際編寫在程序的那個位置上。

eval(..)被執行的後續代碼行中,引擎 將不會「知道」或「關心」前面的代碼是被動態翻譯的,並且所以修改了詞法做用域環境。引擎 將會像它一直作的那樣,簡單地進行詞法做用域查詢。

考慮下面代碼:

var b = 2;
        function demo(str, a) {
            eval(str);           // 欺騙詞法做用域
            console.log(a, b);
        }
        demo("var b = 12;", 1); // 1, 12
複製代碼

eval(..)調用的位置上,字符串"var b = 12"被看做是一直就存在第2行的代碼。由於這個代碼恰巧聲明瞭一個新的變量b,它就修改了現存的demo(..)的詞法做用域。事實上,就像上面提到的那樣,這個代碼實際上在demo(..)內部建立了變量b,它遮蔽了聲明在外部(全局)做用域中的b

console.log(..)調用發生時,它會在demo(..)的做用域中找到ab,並且毫不會找到外部的b。這樣,咱們就打印出"1, 12"而不是通常狀況下的"1, 2"。

假設:eval(..)執行的代碼字符串包含一個或多個聲明(變量或函數)的話,這個動做就會修改這個eval(..)所在的詞法做用域。技術上講,eval(..)能夠經過種種技巧(超出了咱們這裏的討論範圍)被「間接」調用,而使它在全局做用域的上下文中執行,如此修改全局做用域。但不論那種狀況,eval(..)均可以在運行時修改一個編寫時的詞法做用域。

注意:eval(..)被用於一個操做它本身的詞法做用域的strict模式程序時,在eval(..)內部作出的聲明不會實際上修改包圍它的做用域。

var b = 2;
        function demo(str, a) {
 'use strict'
            eval(str);      // 欺騙詞法做用域不生效
            console.log(a, b);
        }
        demo("var b = 12;", 1); // 1, 2
複製代碼

在JavaScript中還有其餘的工具擁有與eval(..)很是相似的效果。setTimeout(..)setInterval(..)能夠 爲它們各自的第一個參數值接收一個字符串,其內容將會被eval爲一個動態生成的函數的代碼。這種老舊的,遺產行爲早就被廢棄了。別這麼作!

new Function(..)函數構造器相似地爲它的 最後 一個參數值接收一個代碼字符串,來把它轉換爲一個動態生成的函數(前面的參數值,若是有的話,將做爲新函數的命名參數)。這種函數構造器語法要比eval(..)稍稍安全一些,但在你的代碼中它仍然應當被避免。

在你的代碼中動態生成代碼的用例少的難以想象,由於在性能上的倒退使得這種能力幾乎老是得不償失。

with

MDN最新規範不建議使用,因此接下來咱們瞭解下with語句就行。

with語句接收一個對象,這個對象有0個或多個屬性,並 將這個對象視爲好像它是一個徹底隔離的詞法做用域,所以這個對象的屬性被視爲在這個「做用域」中詞法定義的標識符。

function foo(obj) {
    with (obj) {
        a = 2;
    }
}

var o1 = {
    a: 3
};

var o2 = {
    b: 3
};

foo( o1 );
console.log( o1.a ); // 2

foo( o2 );
console.log( o2.a ); // undefined
console.log( a ); // 2 -- 哦,全局做用域被泄漏了!
複製代碼

在這個代碼示例中,建立了兩個對象o1o2。一個有a屬性,而另外一個沒有。foo(..)函數接收一個對象引用obj做爲參數值,並在這個引用上調用with (obj) {..}。在with塊兒內部,咱們製造了一個變量a的看似是普通詞法引用的東西,並將值2賦予它。

當咱們傳入o1時,賦值a = 2找到屬性o1.a並賦予它值2,正如在後續的console.log(o1.a)語句反應的那樣。然而,當咱們傳入o2,由於它沒有a屬性,沒有這樣的屬性被建立,因此o2.a仍是undefined

可是以後咱們注意到一個特別的反作用,賦值a = 2建立了一個全局變量a。這怎麼可能?

注意: 儘管一個with塊兒將一個對象視爲一個詞法做用域,可是在with塊兒內部的一個普通var聲明將不會歸於這個with塊兒的做用域,而是歸於包含它的函數做用域。

with語句其實是從你傳遞給它的對象中憑空製造了一個 全新的詞法做用域

以這種方式理解的話,當咱們傳入o1with語句聲明的「做用域」就是o1,並且這個「做用域」擁有一個對應於o1.a屬性的「標識符」。但當咱們使用o2做爲「做用域」時,它裏面沒有這樣的a「標識符」,因而就會出現undefined

「做用域」o2中沒有,foo(..)的做用域中也沒有,甚至連全局做做用域中都沒有找到標識符a,因此當a = 2被執行時,其結果就是自動全局變量被建立(由於咱們沒有在strict模式下)。

with在運行時將一個對象和它的屬性轉換爲一個帶有「標識符」的「做用域」,這個奇怪想法有些燒腦。可是對於咱們看到的結果來講,這是我能給出的最清晰的解釋。

動態做用域性能

經過在運行時修改,或建立新的詞法做用域,eval(..)with均可以欺騙編寫時定義的詞法做用域。

JavaScript 引擎 在編譯階段期行許多性能優化工做。其中的一些優化原理都歸結爲實質上在進行詞法分析時能夠靜態地分析代碼,並提早決定全部的變量和函數聲明都在什麼位置,這樣在執行期間就能夠少花些力氣來解析標識符。

但若是 引擎 在代碼中找到一個eval(..)with,它實質上就不得不 假定 本身知道的全部的標識符的位置多是不合法的,由於它不可能在詞法分析時就知道你將會向eval(..)傳遞什麼樣的代碼來修改詞法做用域,或者你可能會向with傳遞的對象有什麼樣的內容來建立一個新的將被查詢的詞法做用域。

換句話說,悲觀地看,若是eval(..)with出現,那麼它 作的幾乎全部的優化都會變得沒有意義,因此它就會簡單地根本不作任何優化。

你的代碼幾乎確定會趨於運行的更慢,只由於你在代碼的任何地方引入了一個了eval(..)with。不管 引擎 將在努力限制這些悲觀臆測的反作用上表現得多麼聰明,都沒有任何辦法能夠繞過這個事實:沒有優化,代碼就運行的更慢。

結論

  • 做用域是一組規則,它決定了一個變量(標識符)在哪裏和如何被查找。
  • 做用域是由編寫時函數被聲明的位置的決策定義的,並非說函數在哪裏執行,哪裏就開始生成做用域,這點理解很重要,這也時區分靜態做用域和動態做用域區別的一個方法。
  • 查找一個變量時,都從當前執行中的 做用域 開始,若是有須要(也就是,它們在這裏沒能找到它們要找的東西),它們會在嵌套的 做用域 中一路向上,一次一個做用域(層)地查找這個標識符,直到它們到達全局做用域(頂層)並中止,既可能找到也可能沒找到。
  • eval(…) 和 with 均可以 '欺騙' 詞法做用域,前者能夠經過對一個擁有一個或多個聲明的「代碼」字符串進行求值,來(在運行時)修改現存的詞法做用域。後者實質上是經過將一個對象引用看做一個「做用域」,並將這個對象的屬性看做做用域中的標識符,(一樣,也是在運行時)建立一個全新的詞法做用域。
  • 以上兩種機制的缺點也很明顯,它們壓制了引擎在做用域查詢上進行編譯期優化的能力,由於引擎不得不悲觀的假定這樣子的優化不合理,這兩種機制會使代碼運行的更慢!!! 建議不使用它們

參考

你不懂JS:做用域與閉包

JS做用域

官方中文版原文連接-推薦看這個

做用域氣泡

JavaScript深刻之詞法做用域和動態做用域

相關文章
相關標籤/搜索