深入理解 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"

執行上下文#

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 中的函式完全符合閉包的定義 —— 每個函式都是閉包。

宏觀任務與微觀任務#

任務類型#

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

執行順序#

宏觀任務 1
├── 同步程式碼
├── 微觀任務 1
├── 微觀任務 2
└── ...
宏觀任務 2
├── 同步程式碼
├── 微觀任務 1
└── ...

微觀任務始終在當前宏觀任務結束前完成,優先於下一個宏觀任務。

執行順序範例#

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 加入宏觀任務佇列
  3. Promise.then 加入微觀任務佇列
  4. 執行同步程式碼:輸出 “e”
  5. 當前宏觀任務結束,執行微觀任務佇列
  6. 輸出 “b”,新增 .then 到微觀任務佇列
  7. 輸出 “c”
  8. 微觀任務佇列清空,執行下一個宏觀任務
  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 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. 分析有多少個宏觀任務
  2. 在每個宏觀任務中,分析有多少個微觀任務
  3. 根據呼叫次序,確定宏觀任務中的微觀任務執行次序
  4. 根據宏觀任務的觸發規則和呼叫次序,確定宏觀任務的執行次序
  5. 確定整個順序

總結#

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