附錄

原型繼承

除了 nullundefined 以外,每種原始資料類型都有「原型」,也就是對應的物件包裝函式,提供使用值的方法。在原始上叫用方法或屬性查詢時,JavaScript 會包裝原始元素,並呼叫該方法,或改為對包裝函式物件執行屬性查詢。

舉例來說,字串常值沒有專屬的方法,但您可以利用對應的 String 物件包裝函式,對該字串呼叫 .toUpperCase() 方法:

"this is a string literal".toUpperCase();
> THIS IS A STRING LITERAL

這稱為「原型繼承」,也就是繼承值對應建構函式的屬性和方法。

Number.prototype
> Number { 0 }
>  constructor: function Number()
>  toExponential: function toExponential()
>  toFixed: function toFixed()
>  toLocaleString: function toLocaleString()
>  toPrecision: function toPrecision()
>  toString: function toString()
>  valueOf: function valueOf()
>  <prototype>: Object { … }

您可以使用這些建構函式建立基本物件,而不只是根據其值定義物件。舉例來說,使用 String 建構函式會建立字串物件,而非字串常值:這個物件不僅包含我們的字串值,同時也包含建構函式的所有繼承屬性和方法。

const myString = new String( "I'm a string." );

myString;
> String { "I'm a string." }

typeof myString;
> "object"

myString.valueOf();
> "I'm a string."

在大部分情況下,結果物件的行為與用來定義物件的值相同。例如,即使使用 new Number 建構函式定義數字值,產生的物件會包含 Number 原型的所有方法和屬性,但您可以在這些物件上使用數學運算子,就像處理數字常值一樣:

const numberOne = new Number(1);
const numberTwo = new Number(2);

numberOne;
> Number { 1 }

typeof numberOne;
> "object"

numberTwo;
> Number { 2 }

typeof numberTwo;
> "object"

numberOne + numberTwo;
> 3

您極少需要使用這些建構函式,因為 JavaScript 內建的原型繼承關係就毫無助益。使用建構函式建立基元,也可能導致未預期的結果,因為結果是物件,而非簡單的常值:

let stringLiteral = "String literal."

typeof stringLiteral;
> "string"

let stringObject = new String( "String object." );

stringObject
> "object"

這可能會使使用嚴格的比較運算子變得更加複雜:

const myStringLiteral = "My string";
const myStringObject = new String( "My string" );

myStringLiteral === "My string";
> true

myStringObject === "My string";
> false

自動插入分號 (ASI)

剖析指令碼時,JavaScript 翻譯器會使用名為自動分號插入 (ASI) 的功能,嘗試修正省略分號的執行個體。如果 JavaScript 剖析器遇到不允許的符記,只要符合下列一或多個條件,系統就會在該符記前加上分號,修正潛在的語法錯誤:

  • 以換行符號分隔上一個符記。
  • 符記為 }
  • 前一個符記是 ),插入的分號就是 do...while 陳述式的結束分號。

詳情請參閱 ASI 規則

例如,在下列陳述式後方省略分號並不會導致語法錯誤,這是因為 ASI:

const myVariable = 2
myVariable + 3
> 5

不過,ASI 不能在同一行列出多個陳述式。如果您要在同一行寫入多個陳述式,請務必以分號分隔:

const myVariable = 2 myVariable + 3
> Uncaught SyntaxError: unexpected token: identifier

const myVariable = 2; myVariable + 3;
> 5

ASI 是嘗試修正錯誤,而非 JavaScript 內建的語法靈活性。請視情況使用分號,以免依賴分號產生正確的程式碼。

嚴格模式

規範 JavaScript 編寫方式的標準,遠遠超越了語言初期設計所考慮的因素。每次變更 JavaScript 預期行為時,都必須避免讓舊版網站發生錯誤。

ES5 透過採用「嚴格模式」的方式,為整個指令碼或個別函式選擇採用更嚴格的語言規則組合,而不會破壞現有 JavaScript 語意方面的部分長期問題。如要啟用嚴格模式,請在指令碼或函式的第一行使用字串常值 "use strict",後面加上半形分號:

"use strict";
function myFunction() {
  "use strict";
}

嚴格模式可防止某些「不安全的」操作或已淘汰的功能,系統會擲回明確錯誤以取代常見的「靜音」錯誤,並禁止使用可能與未來語言功能衝突的語法。舉例來說,在宣告變數時,無論其包含的背景資訊為何,開發人員都更有可能在宣告變數時,誤認變數範圍的全域範圍,方法是省略 var 關鍵字:

(function() {
  mySloppyGlobal = true;
}());

mySloppyGlobal;
> true

現代 JavaScript 執行階段無法修正這項行為,卻不會破壞任何依賴該功能的網站,無論是錯誤或故意運作。新型 JavaScript 的做法是讓開發人員為新工作選擇採用嚴格模式,而在不會違反舊版實作項目的情況下,只在新的語言功能環境中啟用嚴格模式。

(function() {
    "use strict";
    mySloppyGlobal = true;
}());
> Uncaught ReferenceError: assignment to undeclared variable mySloppyGlobal

您必須將 "use strict" 寫入為字串常值範本常值 (use strict) 將無法運作。請務必在指定結構定義中的任何可執行程式碼之前加入 "use strict"。否則翻譯器會忽略該設定。

(function() {
    "use strict";
    let myVariable = "String.";
    console.log( myVariable );
    sloppyGlobal = true;
}());
> "String."
> Uncaught ReferenceError: assignment to undeclared variable sloppyGlobal

(function() {
    let myVariable = "String.";
    "use strict";
    console.log( myVariable );
    sloppyGlobal = true;
}());
> "String." // Because there was code prior to "use strict", this variable still pollutes the global scope

按照參照,依值

任何變數,包括物件屬性、函式參數,以及陣列設定對應中的元素,都可以包含原始值或參照值

將原始值指派給另一個變數時,JavaScript 引擎會建立該值的複本,並將其指派給變數。

當您將物件 (類別例項、陣列及函式) 指派給變數時,變數會包含物件在記憶體中儲存位置的參照,而不會建立新的物件副本。因此,變更變數參照的物件會變更參照的物件,而不只是該變數包含的值。舉例來說,如要使用包含物件參照的變數初始化新變數,請使用新變數為該物件新增屬性,這樣屬性及其值就會新增至原始物件:

const myObject = {};
const myObjectReference = myObject;

myObjectReference.myProperty = true;

myObject;
> Object { myProperty: true }

請注意,這不僅是要修改物件,還要執行嚴格的比較作業,因為物件之間的嚴格相等性需要兩個變數參照相同的物件來評估為 true。這些物件無法參照不同的物件,即使這些物件的結構完全相同也一樣:

const myObject = {};
const myReferencedObject = myObject;
const myNewObject = {};

myObject === myNewObject;
> false

myObject === myReferencedObject;
> true

記憶體分配

JavaScript 使用自動記憶體管理,意即在開發過程中不需要明確分配或取消記憶體。雖然 JavaScript 引擎的記憶體管理方法細節不在本單元的討論範圍內,但是瞭解記憶體的分配方式可提供實用的背景資訊,以便使用參照值。

記憶體中有兩個「區域」:「堆疊」和「堆積」。堆疊會儲存靜態資料 (原始值和物件的參照),因為系統會在指令碼執行前,分配儲存這項資料所需的固定空間量。堆積儲存物件,需要動態配置的空間,因為其大小可能會在執行期間變更。記憶體由名為「垃圾收集」的程序釋放,該程序會從記憶體中移除沒有參照的物件。

主要執行緒

JavaScript 是一種基本的單一執行緒語言,搭配「同步」執行模型,代表一次只能執行一項工作。此依序執行結構定義稱為「主執行緒」

主要執行緒由其他瀏覽器工作共用,例如剖析 HTML、轉譯及重新轉譯部分頁面、執行 CSS 動畫,以及處理從簡單 (例如醒目顯示文字) 到複雜 (例如與表單元素互動) 的使用者互動。瀏覽器供應商已找到方法,可將主執行緒執行的工作最佳化,但較複雜的指令碼仍可使用過多主執行緒的資源,並影響整體頁面效能。

部分工作可在名為「網路工作站」背景執行緒中執行,但有下列限制:

  • 工作站執行緒只能對獨立的 JavaScript 檔案執行操作。
  • 嚴重降低或無法存取瀏覽器視窗和 UI。
  • 用於與主執行緒通訊的方式會受到限制。

這些限制使這些限制非常適合用於可能佔用主執行緒的聚焦、資源密集型工作。

呼叫堆疊

用於管理「執行結構定義」(主動執行的程式碼) 的資料結構,是稱為「呼叫堆疊」的清單 (通常僅簡稱「堆疊」)。首次執行指令碼時,JavaScript 解譯器會建立「全域執行內容」,並將其推送至呼叫堆疊,而該全域環境中的陳述式會從上到下逐一執行。當解譯器在執行全域結構定義時遇到函式呼叫時,會將該呼叫的「函式執行內容」推送到堆疊頂端、暫停全域執行內容,並執行函式執行內容。

每次呼叫函式時,該呼叫的函式執行內容會推送至堆疊頂端,位於目前執行作業的正上方。呼叫堆疊是以「先進先出」為基礎運作,也就是說,最近期的函式呼叫 (在堆疊中位位最高) 將執行並持續執行,直到解析為止。函式執行完畢後,直譯器會從呼叫堆疊中移除,包含該函式呼叫的執行結構定義會再次成為堆疊中最高的項目並繼續執行。

這些執行情境會擷取其執行作業所需的任何值。這些結構也會根據父項內容,在函式範圍內建立可用的變數和函式,並在函式的內容中判斷及設定 this 關鍵字的值。

事件迴圈和回呼佇列

這種依序執行的執行作業是指包含回呼函式的非同步工作 (例如從伺服器擷取資料、回應使用者互動,或是等待以 setTimeoutsetInterval 設定的計時器) 會封鎖主執行緒,直到工作完成為止,或者在回呼函式執行內容新增至堆疊時意外中斷目前的執行環境。為解決這個問題,JavaScript 會使用以事件驅動的「並行模型」管理非同步工作,模型由「事件迴圈」和「回呼佇列」(有時稱為「訊息佇列」) 組成。

在主執行緒上執行非同步工作時,回呼函式的執行內容會放在回呼佇列中,而非呼叫堆疊上方。事件迴圈是一種模式,有時稱為「反應器」,會持續輪詢呼叫堆疊和回呼佇列的狀態。如果回呼佇列中存在工作,且事件迴圈判定呼叫堆疊為空白,則回呼佇列的工作會一次推送至堆疊,執行一次。