深入理解 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
執行順序分析
- 執行同步程式碼:輸出 “a”
- setTimeout 加入宏觀任務佇列
- Promise.then 加入微觀任務佇列
- 執行同步程式碼:輸出 “e”
- 當前宏觀任務結束,執行微觀任務佇列
- 輸出 “b”,新增 .then 到微觀任務佇列
- 輸出 “c”
- 微觀任務佇列清空,執行下一個宏觀任務
- 輸出 “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.`);
}
}分析非同步執行順序的方法#
- 分析有多少個宏觀任務
- 在每個宏觀任務中,分析有多少個微觀任務
- 根據呼叫次序,確定宏觀任務中的微觀任務執行次序
- 根據宏觀任務的觸發規則和呼叫次序,確定宏觀任務的執行次序
- 確定整個順序
總結#
| 概念 | 要點 |
|---|---|
| 類型系統 | 7 種類型,注意浮點數精度問題 |
| 執行上下文 | 包含詞法環境、變數環境等 |
| 閉包 | 函式 + 執行環境 |
| 任務佇列 | 微觀任務優先於宏觀任務 |
| async/await | Promise 的語法糖,更優雅的非同步處理 |