iT邦幫忙

0

[JS] You Don't Know JavaScript [Scope & Closures] - Around the Global Scope ?

  • 分享至 

  • twitterImage
  •  

前言

我們在The Scope Chain不斷地提到全域作用域,可能會疑問為什麼最外層的作用域是全域作用域?而為什麼對於JS來說會是最重要的?僅避免使用全域範圍就足夠了嗎?

對於JS來說,全域是一個很複雜的環節,他非常的實用但細節也非常多,本章節將會對這些問題做一些解釋。


Why Global Scope?

對於我們在開發一個專案的時候,我們並不會把所有的程式都寫在同一個文件中,那麼JS是通過什麼方式讓不同文件中的程式在運行的時候上下組合再一起的??

對於瀏覽器來說主要有三種方式:

  1. 如果使用的是ES Modules,那麼這些文件會被JS單獨加載,然後使用import將需要的module引入到需要的環境中,單獨的module通過這個方法互相引入與合作且不需要共享作用域。
  2. 如果在建構的時期將程式捆綁在一起,則通常會將所有文件連接在一起然後再交給JS引擎和瀏覽器,然後他們都只處理連接在一起的這個大文件,雖然所有的程式都在一個文件中,但每個部分都需要有能夠讓其他部分呼叫的名字與功能;在某些設置中,文件中內容都包裝在一個封閉的作用域中(wrapper function..),每個功能都可以透過共享的作用域來讓其他功能呼叫。
(function wrappingOuterScope(){
    var moduleOne = (function one(){
        // ..
    })();

    var moduleTwo = (function two(){
        // ..

        function callModuleOne() {
            moduleOne.someMethod();
        }

        // ..
    })();
})();

以上面的例子來說,由於moduleOnemoduleTwo都存在於wrappingOuterScope作用域中,所以可以所以可以互相訪問,而wrappingOuterScope只是一個function而不是一個真正的全域作用域,但是他儲存了所有的功能,所以他可以稱為是全域作用域的替身。
3. 如果都不是上面的兩種方式,那麼全域作用域就是聯繫不同功能的唯一方法。

var moduleOne = (function one(){
    // ..
})();
var moduleTwo = (function two(){
    // ..

    function callModuleOne() {
        moduleOne.someMethod();
    }

    // ..
})();

由於沒有共同的function scope,所以將moduleOnemoduleTwo宣告在全域中,而事實上JS是分別對兩個文件進行加載

// module1.js
var moduleOne = (function one(){
    // ..
})();

// module2.js
var moduleTwo = (function two(){
    // ..

    function callModuleOne() {
        moduleOne.someMethod();
    }

    // ..
})();

這兩個檔案在瀏覽器中分別被加載,每個文件中的頂部變量宣告都會成為全域變量,而全域作用域是這兩個文件溝通的唯一橋樑,所以對JS引擎來說這他們都是獨立的程式。

全域作用域還包括:

  • JS exposes its built-ins:
    • primitives: undefined,null,Infinity,NaN...
    • natives: Date(),Object(),String()...
    • global functions: eval(),parseInt()...
    • namesoaces: Math.Atinucs.JSON
    • friends of JS: Intl, WebAssembly
  • The environment hosting the JS engine exposes its own built-ins:
    • console
    • DOM(window,document...)
    • timer(setTimeout(...)...)
    • web platform APIs(history,navigatot...)

Where Exactly in this Gloval Scope

雖然說全域作用域就是文件的最外層,也就是說他不存在於任function或block中,但他的定義也不是這麼間單的,不同的JS環境對全域作用域的定義與處理方式都不一樣,如果沒有分辨的能力可能會有不能預期的錯誤出現。

Browser "Window"

var studentName = "Kyle";

function hello() {
    console.log(`Hello, ${ studentName }!`);
}
hello(); //Hello, Kyle!

我們可以使用<script>標籤加入到瀏覽器的環境中,他會動態的建立一個DOM元素,上面的例子中我們將stundntNamehello(...)都宣告在全域作用中,這意味著我們可以在全域的物件(在瀏覽器中全域的物件是window)中找到與他們同名的屬性。

var studentName = "Kyle";

function hello() {
    console.log(`Hello, ${ window.studentName }!`);
}
window.hello(); // Hello, kyle!

Globals Shadowing Globals

我們有在The Scope Chain介紹到什麼是shadowing,內部作用域的宣告可以覆蓋外部作用域的宣告,讓其無法訪問到外部作用域的同名變量。

而對於全域作用域來說,全域物件的屬性會被在全域宣告的變量shadowing。

window.something = 42; // property of object in global

let something = "Kyle"; // declarate blobal variable

console.log(something); // Kyle -> shadowing
console.log(window.something); //42

上面的程式碼中,對於全域的物件宣告一個someting的屬性,但是也使用let宣告一個同名的變量,這樣的結果是something的詞法宣告會shadowing全域物件的屬性。

DOM Globals

對於DOM來說有一個特別的現象,當你處於瀏覽器的環境下,若你有一個DOM元素它具有id Attribute,那麼他就會自動創建一個引用他的全域變量。

<ul id="my-todo-list">
   <li id="first">Write a book</li>
   ..
</ul>

對於上面的html會轉換為

first; 
// <li id="first">..</li>

window["my-todo-list"]; 
// <ul id="my-todo-list">..</ul>

如果id的值對於lexcal來說是合法的那麼這個id的值便會被建立,若不是則會在全域物件中建立(window[...]),雖然這種將所有id的值都建立為全域是舊版瀏覽器的行為,但是為了迎合一些舊版的網站只能暫時將他們保留,作為開發者能做的就是盡量不要去使用這些被自動創建出來的全域變量。

what's in a(window) Name?

var name = 42;
console.log(name, typeof name); // "42", string

window.name他是在瀏覽器中定義的全域,他是全域物件中的一個屬性(property),我們使用var宣告了name,但是他卻沒有shadowing全域物件的porperty,這代表著當全域物件中已經有這個名字的property,那麼使用var宣告一個一樣名字的全域變數時var的宣告會被忽略,這與我們上面看得的使用let宣告的結果並不一樣。

但是奇怪的是我們將這個全域的numbername console出來後發現他雖然也是42但是他的資料結構變為string,這是因為對於window物件來說,要取得這個name屬性他所使用的是getting/setting方法,這個方法要求他的值必須是字串。

Developer Tools Console/REPL

對於開發者工具來說,雖然他們也確實的再處理JS程式,但是為了給使用者良好的使用體驗所以會額外做一些處理,在某些情況下開發者工具並不會處理JS程序的所有步驟,這個可能會造成程式與開發者工具之間的差異,舉例來說,
開發者工具在對於一些JS的錯誤相對放寬,所以當輸入一個程式到開發者工具中,有可能不會報錯。

其中的差異有以下幾種:

  • 全域作用域行為
  • Hoisting
  • 在全域作用域中使用塊狀作用域宣告關鍵字(let/const)

所以雖然使用console/REPL在確實有在全域作用域中處理程序,但這並不是很準確,雖然這些開發者工具會在一定程度上模仿全域作用域的行為,但是他僅僅是模仿並不是嚴格遵守,開發者工具優先考量開發者使用的便利性,所以行為會與真正的JS規範有一定的差距。

ES Modules(ESM)

在ES6中引入了對模塊的概念(ES Modules),他能夠修改文件中的全域作用域是他最為明顯的影響之一。

var studentName = "Kyle";

function hello() {
    console.log(`Hello, ${ studentName }!`);
}

hello();  // Hello, Kyle!
export hello;

若是使用ES Modules來加載那麼他的運行方式和一般程式運行的方式一樣,但是從應用程式的角度來說卻是不一樣的,儘管在模塊的頂層宣告,但是在全域作用域中stydentNamehello(...)任然不是全域變數,他們是屬於模塊作用域中(模塊中的全域)的。

所以說他並不是被加入到全域物件中,但是這並不代表他不能夠在全域中被訪問到,只是不能通過在module中的全域來建立真正意義上的全域變數。

module中的全域作用域是真正全域作用域的下一級,這代表全域作用域中的宣告可以提供給module使用。

ESM的出現就是為了讓開發者減少對全域作用域的依賴,在全域作用域中可以import需要的module,這樣可以減少對於全域或全域物件的用法。

Node

Node會處理他加載的每一個.js文件包括啟動Node的主要module,而這樣的效果是Node程式的頂層並不是全域作用域。

var studentName = "Kyle";

function hello() {
    console.log(`Hello, ${ studentName }!`);
}

hello(); // Hello, Kyle!

在處理之前,Node會將程式碼包裝近一個function中,所以varfunction會在一個function中被宣告而不是視為在全域作用中宣告,就像下面的程式碼。

function Module(module,require,__dirname,...) {
    var studentName = "Kyle";

    function hello() {
        console.log(`Hello, ${ studentName }!`);
    }

    hello(); // Hello, Kyle!
    module.exports.hello = hello; //export variables
}

Node實際上會運行你調用的module,所以上面的程式碼中可以了解到為什麼studentNamehello(...)不是全域宣告而是module範圍的宣告。

Node定義了一些如require(...)的全域變量,但他們實際上並不是真正定義在全域作用域中,他們只是被注入到每一個module中,所以要在Node中定義實際的全域變量需要將他加入到一個Node自動提供的globals屬性中,當然他也不是真正的globals,他只是實際全域作用域的一個reference,類似於瀏覽器中的window


Global This

在ES2020中,JS終於定義了對全域物件標準化的引用flobalThis,所以理論上可以使用globalThis代替上面介紹的所有方法。

參考文獻:
You Don't Know JavaScript 2nd


圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言