深入理解 JavaScript 的執行時機制。

本章重點掌握型別系統、執行環境、閉包,以及非同步程式設計的本質。

型別系統#

JavaScript 定義了 7 種語言型別:

型別說明
Undefined未定義
Null空值
Boolean布林值
String字串
Number數字
Symbol符號(ES6)
Object物件

Undefined vs Null#

// undefined 是變數,可能被竄改(在區域作用域中)
// 建議使用 void 0 取得安全的 undefined 值
let x = void 0;

// null 是關鍵字,表示「定義了但為空」
let y = null;

程式撰寫規範建議用 void 0 取代 undefined,因為 undefined 在區域作用域中可能被重新賦值。

String 型別#

// String 的最大長度是 2^53 - 1
// 注意:這是 UTF-16 編碼單元的長度,不是字元數

// 字串是不可變的
let str = "hello";
str[0] = "H"; // 無效,str 仍然是 "hello"

Number 型別#

// 特殊值
Infinity - // 正無窮
  Infinity; // 負無窮
NaN; // Not a Number

// +0 和 -0
1 / 0; // Infinity
1 / -0; // -Infinity

// 浮點數精度問題
0.1 + 0.2 === 0.3; // false!

浮點數比較的正確方式

// 使用 Number.EPSILON 作為誤差範圍
Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON; // true

Symbol 型別#

// 建立 Symbol
const sym = Symbol("description");

// Symbol 是唯一的
Symbol("a") === Symbol("a"); // false

// 用作物件的 key
const obj = {
  [Symbol.iterator]: function* () {
    yield 1;
    yield 2;
  },
};

for (const v of obj) {
  console.log(v); // 1, 2
}

型別轉換#

裝箱轉換(Boxing)#

基本型別到對應物件的轉換:

// . 運算子會自動裝箱
"hello".charAt(0); // "h"

// 等價於
new String("hello").charAt(0);

// 手動裝箱
const strObj = Object("hello");
strObj instanceof String; // true

拆箱轉換(Unboxing)#

物件到基本型別的轉換:

const obj = {
  valueOf() {
    console.log("valueOf");
    return 1;
  },
  toString() {
    console.log("toString");
    return "2";
  },
};

// 數字運算優先呼叫 valueOf
obj * 2; // "valueOf", 結果: 2

// 字串轉換優先呼叫 toString
String(obj); // "toString", 結果: "2"

// 可以用 Symbol.toPrimitive 覆蓋預設行為
obj[Symbol.toPrimitive] = () => "custom";
obj + ""; // "custom"

執行環境(Execution Context)#

ES2018 執行環境結構#

執行環境
├── lexical environment(詞法環境)
│   └── 取得變數和 this 時使用
├── variable environment(變數環境)
│   └── 宣告變數時使用
├── code evaluation state
│   └── 恢復程式碼執行位置
├── Function / ScriptOrModule
├── Realm
│   └── 基礎函式庫與內建物件實體
└── Generator(僅產生器環境)

var 與 let 的差異#

// var 的作用域是函式級,會「穿透」區塊
function test() {
  for (var i = 0; i < 3; i++) {}
  console.log(i); // 3
}

// let 引入了區塊作用域
function test2() {
  for (let j = 0; j < 3; j++) {}
  console.log(j); // ReferenceError
}

let 會在以下語句中產生區塊作用域:for、if、switch、try/catch/finally

IIFE(立即執行函式表達式)#

// 傳統寫法(需要括號)
(function () {
  var a = 1;
})();

// 推薦寫法(使用 void)
void (function () {
  var a = 1;
})();

閉包#

閉包的本質

閉包是一個綁定了執行環境的函式。它攜帶了執行所需的環境,包含:

  • 環境部分:函式的詞法環境
  • 識別字清單:函式中用到的未宣告變數
  • 表達式部分:函式本體
function createCounter() {
  let count = 0; // 被閉包捕獲的變數
  return function () {
    return ++count;
  };
}

const counter = createCounter();
counter(); // 1
counter(); // 2
counter(); // 3

JavaScript 的函式完全符合閉包的定義 —— 每個函式都是閉包(Closure)。

Macrotask 與 Microtask#

任務類型#

任務類型發起者範例
Macrotask宿主環境(瀏覽器/Node)setTimeout, setInterval, I/O
MicrotaskJavaScript 引擎Promise.then, MutationObserver

執行順序#

Macrotask 1
├── 同步程式碼
├── Microtask 1
├── Microtask 2
└── ...
Macrotask 2
├── 同步程式碼
├── Microtask 1
└── ...

Microtask 一定在當前的 macrotask 結束前清空,優先於下一個 macrotask。

執行順序範例#

console.log("a");

setTimeout(() => console.log("d"), 0);

Promise.resolve()
  .then(() => console.log("b"))
  .then(() => console.log("c"));

console.log("e");

// 輸出順序:a, e, b, c, d
執行順序分析
  1. 執行同步程式碼:輸出 “a”
  2. setTimeout 加入 macrotask 佇列
  3. Promise.then 加入 microtask 佇列
  4. 執行同步程式碼:輸出 “e”
  5. 當前 macrotask 結束,執行 microtask 佇列
  6. 輸出 “b”,新增 .then 到 microtask 佇列
  7. 輸出 “c”
  8. Microtask 佇列清空,執行下一個 macrotask
  9. 輸出 “d”

Promise#

基本用法#

function sleep(duration) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, duration);
  });
}

sleep(1000).then(() => {
  console.log("1 秒後執行");
});

Promise 鏈#

fetch("/api/user")
  .then((response) => response.json())
  .then((user) => fetch(`/api/posts/${user.id}`))
  .then((response) => response.json())
  .then((posts) => console.log(posts))
  .catch((error) => console.error(error));

async/await#

async/await 是 Promise 的語法糖,讓非同步程式碼看起來像同步程式碼。

基本用法#

async function fetchUser() {
  try {
    const response = await fetch("/api/user");
    const user = await response.json();
    return user;
  } catch (error) {
    console.error("Failed to fetch user:", error);
    throw error;
  }
}

並行執行#

// 序列執行(較慢)
async function serial() {
  const a = await fetchA();
  const b = await fetchB();
  return [a, b];
}

// 並行執行(較快)
async function parallel() {
  const [a, b] = await Promise.all([fetchA(), fetchB()]);
  return [a, b];
}

實戰:紅綠燈控制#

function sleep(duration) {
  return new Promise((resolve) => setTimeout(resolve, duration));
}

async function changeColor(color, duration) {
  document.getElementById("light").style.background = color;
  await sleep(duration);
}

async function trafficLight() {
  while (true) {
    await changeColor("green", 3000);
    await changeColor("yellow", 1000);
    await changeColor("red", 2000);
  }
}

trafficLight();

ES6+ 特性精選#

解構賦值#

// 陣列解構
const [a, b, ...rest] = [1, 2, 3, 4, 5];

// 物件解構
const { name, age = 18 } = user;

// 函式參數解構
function greet({ name, greeting = "Hello" }) {
  console.log(`${greeting}, ${name}!`);
}

展開運算子#

// 陣列展開
const arr = [...arr1, ...arr2];

// 物件展開
const obj = { ...obj1, ...obj2, extra: true };

// 函式呼叫
Math.max(...numbers);

可選鏈與空值合併#

// 可選鏈 ?.
const name = user?.profile?.name;
const first = arr?.[0];
const result = obj?.method?.();

// 空值合併 ??
const value = input ?? defaultValue;
// 只有 null 或 undefined 時才使用預設值

Class 語法#

class Animal {
  #privateField = 0; // 私有欄位

  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} makes a sound.`);
  }

  static create(name) {
    return new Animal(name);
  }
}

class Dog extends Animal {
  speak() {
    console.log(`${this.name} barks.`);
  }
}

分析非同步執行順序的方法#

  1. 分析有多少個 macrotask
  2. 在每個 macrotask 中,分析有多少個 microtask
  3. 根據呼叫順序,確定 macrotask 中的 microtask 執行順序
  4. 根據 macrotask 的觸發規則和呼叫順序,確定 macrotask 的執行順序
  5. 確定整體順序

總結#

概念要點
型別系統7 種型別,注意浮點數精度問題
執行環境包含詞法環境、變數環境等
閉包函式 + 執行環境
任務佇列Microtask 優先於 macrotask
async/awaitPromise 的語法糖,更優雅的非同步處理