附錄

原型繼承

除了 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 剖析器遇到不允許的符記,就會嘗試在該符記前加上分號,以修正潛在的語法錯誤,前提是下列一或多個條件為真:

  • 該符號與前一個符號之間以換行符號分隔。
  • 這個符記為 }
  • 前一個符記是 ),插入的半形分號則是 dowhile 陳述式的結束半形分號。

詳情請參閱 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 動畫,以及處理使用者互動,從簡單的 (例如醒目顯示文字) 到複雜的 (例如與表單元素互動) 都有。瀏覽器供應商已找到方法,可最佳化主要執行緒執行的工作,但較複雜的指令碼仍可能使用過多主要執行緒的資源,並影響整體網頁效能。

部分工作可在稱為 Web Workers 的背景執行緒中執行,但有以下限制:

  • 工作執行緒只能針對獨立的 JavaScript 檔案執行動作。
  • 他們無法存取瀏覽器視窗和 UI,或只能存取部分功能。
  • 這些子執行緒無法與主執行緒進行通訊。

由於這些限制,因此這些類別最適合用於專注且資源密集的工作,否則這些工作可能會佔用主要執行緒。

呼叫堆疊

用於管理「執行階段」(即正在積極執行的程式碼) 的資料結構,是稱為「呼叫堆疊」的清單 (通常稱為「堆疊」)。當指令碼首次執行時,JavaScript 轉譯器會建立「全域執行內容」,並將其推送至呼叫堆疊,全域內容中的陳述式會依序從上到下執行。當執行緒解譯器在執行全域背景資訊時遇到函式呼叫,就會將該呼叫的「函式執行背景資訊」推送至堆疊頂端,暫停全域執行背景資訊,並執行函式執行背景資訊。

每次呼叫函式時,該呼叫的函式執行內容會推送至堆疊頂端,位於目前執行內容的上方。呼叫堆疊是以「後進先出」的方式運作,也就是說,系統會執行堆疊中最高層的最近函式呼叫,並持續執行,直到該函式解析為止。函式完成後,轉譯器會將該函式從呼叫堆疊中移除,而包含該函式呼叫的執行情境會再次成為堆疊中最高的項目,並繼續執行。

這些執行階段會擷取執行所需的任何值。它們也會根據父項脈絡,在函式範圍內建立可用的變數和函式,並在函式脈絡中判斷並設定 this 關鍵字的值。

事件迴圈和回呼佇列

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

在主執行緒上執行非同步工作時,回呼函式的執行內容會放置在回呼佇列中,而不是放在呼叫堆疊的頂端。事件迴圈是一種模式,有時也稱為「反應器」,會持續輪詢呼叫堆疊和回呼佇列的狀態。如果回呼佇列中有工作,且事件迴圈判斷呼叫堆疊為空,回呼佇列中的各項工作就會逐一推送至堆疊,以便執行。