# Day 42-44：遞迴重構；TDD 流程；Caesar Cipher；Array.fill vs Array.from；分組計數；Map vs Object；解構賦值；JSDoc 型別；JSFuck

By [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake) · 2026-01-22

---

**整理日期:** 2026/01/22

* * *

一、遞迴函式程式碼重構：追求簡潔與可讀性
--------------------

### 原始問題：過度複雜的物件組裝

當遞迴函式需要回傳多個值時，新手常會將所有資料包裝成物件，並額外建立一個包裝函式 。這會讓程式碼變得複雜且難以維護： [jsdevspace.substack](https://jsdevspace.substack.com/p/how-to-master-recursion-in-javascript)

    // ❌ 避免：巢狀函式與過度包裝
    export function calculateFormula(n) {
      // 應避免在 function 內部定義 function（巢狀函式）
      const result = recursiveUnit(n);
      // 考慮在主程式層級組裝字串，而非在遞迴內部
      return `${result.formula} = ${result.sum}`;
    }
    
    function recursiveUnit(cur) {
      if (cur === 4) {
        const value = 2 * 4;
        return {
          formula: `2*4`,
          sum: value,
        };
      }
      const prev = recursiveUnit(cur - 2);
      const a = cur - 2;
      const b = cur;
      const value = a * b;
      return {
        formula: `${prev.formula} + ${a}*${b}`,
        sum: prev.sum + value,
      };
    }
    

### 重構建議

**關鍵原則：** 讓遞迴函式專注於單一職責，字串組裝交給外層處理 。 [javascript.plainenglish](https://javascript.plainenglish.io/javascript-recursive-function-the-ultimate-tutorial-77892de67b65)

#### Function 建構式替代 eval

如果需要動態執行字串運算，可使用 `new Function()` 建構式，它比 `eval()` 更安全，因為無法存取呼叫端的作用域 ： [stackoverflow](https://stackoverflow.com/questions/55074927/eval-vs-function-constructor)

    // ✅ 較安全的動態執行方式
    const calculate = new Function('a', 'b', 'return a * b');
    console.log(calculate(3, 4)); // 12
    

`Function` 建構式只能存取全域作用域與傳入的參數，避免了 `eval` 可能存取外層變數的安全風險 。 [dfkaye.github](https://dfkaye.github.io/2014/03/14/javascript-eval-and-function-constructor/)

#### 單一職責原則

遞迴函式應該只負責計算，不要混雜格式化邏輯 ： [javascript.plainenglish](https://javascript.plainenglish.io/javascript-recursive-function-the-ultimate-tutorial-77892de67b65)

    // ✅ 推薦：職責分離
    function calculateSum(n) {
      if (n === 4) return 2 * 4;
      return (n - 2) * n + calculateSum(n - 2);
    }
    
    // 格式化由外層處理
    function formatResult(n) {
      const sum = calculateSum(n);
      return `計算結果 = ${sum}`;
    }
    

* * *

二、TDD 思考流程：先定義 I/O，再開始實作
------------------------

### 核心概念：黑盒子設計思維

**所有程式碼都應該用相同的思考流程**：看到需求時，先定義清楚輸入（Input）與輸出（Output），實作不通再回頭調整計畫 。 [moldstud](https://moldstud.com/articles/p-best-practices-for-implementing-tdd-in-javascript-a-guide-for-developers)

TDD 強制先寫測試的本質，就是訓練「實作前先想清楚」的思維模式 。 [moldstud](https://moldstud.com/articles/p-best-practices-for-implementing-tdd-in-javascript-a-guide-for-developers)

### 關於程式碼重用的思考

**何時該抽出共用函式？**

回到自然語言判斷「這是一件獨立的事」，而非單純看到重複程式碼就抽出來 。 [moldstud](https://moldstud.com/articles/p-best-practices-for-implementing-tdd-in-javascript-a-guide-for-developers)

*   ✅ **正確思維：** 「我要取奇數索引值的元素」是一件獨立且完整的事，後續被重複使用只是結果
    
*   ❌ **錯誤思維：** 看到兩處程式碼一樣就立刻抽成函式
    

程式碼的表達方式要考慮閱讀者感受，所以對「單獨一件事」的定義很重要。

* * *

三、Caesar Cipher 加密演算法實作
-----------------------

### 問題描述

實作 Caesar Cipher 加密函式，將輸入字串的英文字母（a-z, A-Z）依指定位移量平移，其他字元保持原樣 。 [caesar-cipher](https://caesar-cipher.com/caesar-cipher-javascript)

**需求：**

*   支援小寫、大寫字母
    
*   處理字母包回（wrap around），如 `xyz` → `abc`
    
*   非字母字元（數字、標點、空白）原樣回傳
    

### 解題思路

1.  產生小寫字母陣列 `lowerAlphabet`（a-z）與大寫字母陣列 `upperAlphabet`（A-Z）
    
2.  對每個字元判斷：
    
    *   若是小寫 → 位移加密並處理包回
        
    *   若是大寫 → 位移加密並處理包回
        
    *   其他字元 → 原樣回傳
        
3.  使用 `Array.prototype.map` 處理字元陣列，最後用 `join('')` 組回字串 [caesar-cipher](https://caesar-cipher.com/caesar-cipher-javascript)
    

### JavaScript API 選用理由

*   `Array.from`：產生字母陣列，搭配 `String.fromCharCode` 建立 Unicode 字元 [delicious-insights](https://delicious-insights.com/en/posts/js-protip-array-from-fill/)
    
*   `split('')`：將字串拆成字元陣列
    
*   `map`：逐字元處理加密邏輯 [developer.mozilla](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/map)
    
*   `indexOf`：判斷字元是否為英文字母並取得索引
    
*   `join('')`：組回加密後字串
    

### 實作程式碼

    export function caesarCipher(publicString, shift = 3) {
      // 產生 A-Z
      const upperAlphabet = Array.from({ length: 26 }, (_, i) =>
        String.fromCharCode(65 + i),
      );
      
      // 產生 a-z
      const lowerAlphabet = Array.from({ length: 26 }, (_, i) =>
        String.fromCharCode(97 + i),
      );
      
      const privateString = publicString
        .split('')
        .map((char) => {
          if (lowerAlphabet.indexOf(char) !== -1) {
            const originIndex = lowerAlphabet.indexOf(char);
            const shiftIndex = (originIndex + shift) % 26;
            return lowerAlphabet[shiftIndex];
          } else if (upperAlphabet.indexOf(char) !== -1) {
            const originIndex = upperAlphabet.indexOf(char);
            const shiftIndex = (originIndex + shift) % 26;
            return upperAlphabet[shiftIndex];
          }
          return char;
        })
        .join('');
    
      return privateString;
    }
    

### 學習重點

1.  **陣列操作：** 熟悉 `map`、`indexOf` 與字串處理方法
    
2.  **TDD 設計：** 覆蓋正常、邊界、例外情境，確保程式碼健壯性 [moldstud](https://moldstud.com/articles/p-best-practices-for-implementing-tdd-in-javascript-a-guide-for-developers)
    
3.  **早期回傳（Guard Clause）：** 讓程式更易讀、易維護
    
4.  **分類處理：** 支援多種輸入時，先分類再處理，避免複雜的條件判斷
    

* * *

四、Array.fill() vs Array.from() 的選擇時機
------------------------------------

### 核心概念：原始值 vs 物件參考

#### 原始值／純值（Primitive） [developer.mozilla](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Guide/Data_structures)

`string`、`number`、`boolean`、`null`、`undefined`、`symbol`、`bigint`

*   賦值時**複製值本身**
    
*   每個變數獨立，互不影響
    

#### 物件（Object）

`array`、`object`、`function`

*   賦值時**複製參考（記憶體位址）**
    
*   多個變數可能指向同一物件，修改會互相影響 [developer.mozilla](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Guide/Data_structures)
    

### `fill()` 的陷阱

`fill(value)` 只對 `value` **求值一次**，然後把同一個值放進每個位置 。 [developer.mozilla](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/fill)

    // ✅ 原始值：安全
    const nums = Array(3).fill(0);
    nums[0] = 99;
    console.log(nums); // [99, 0, 0] ← 互不影響
    
    // ❌ 物件：共享參考
    const arrs = Array(3).fill([]);
    arrs[0].push("A");
    console.log(arrs); // [["A"], ["A"], ["A"]] ← 全部都被改了！
    

### `Array.from()` 的安全性

callback 在**每次迭代時執行**，每次產生新的獨立物件 。 [delicious-insights](https://delicious-insights.com/en/posts/js-protip-array-from-fill/)

    // ✅ 每個元素是獨立的新陣列
    const arrs = Array.from({ length: 3 }, () => []);
    arrs[0].push("A");
    console.log(arrs); // [["A"], [], []] ← 只有第一個被改
    

### 判斷原則

    要填入的值是什麼？
    ├─ 原始值 → Array(n).fill(value)
    │   - 簡潔、效能較好
    │   - 語意：「建立 n 個相同值」
    │
    └─ 物件 → Array.from({ length: n }, () => 新物件)
        - 每次迭代產生獨立實例
        - 語意：「根據邏輯產生 n 個元素」
    

### 額外情況：每個元素的值依賴 index

即使是原始值，若需要根據 index 計算，也要用 `Array.from` ： [delicious-insights](https://delicious-insights.com/en/posts/js-protip-array-from-fill/)

    // 產生 [0, 1, 2, 3, 4]
    Array.from({ length: 5 }, (_, i) => i)
    
    // 產生 [0, 3, 6, 9, 12]（每人分配 3 個元素的起始位置）
    Array.from({ length: 5 }, (_, i) => i * 3)
    

### 快速查表

情境

建議寫法

範例

n 個相同原始值

`Array(n).fill(value)`

`Array(5).fill(0)`

n 個獨立空陣列

`Array.from` + callback

`Array.from({ length: 5 }, () => [])`

n 個獨立空物件

`Array.from` + callback

`Array.from({ length: 5 }, () => ({}))`

根據 index 計算值

`Array.from` + callback

`Array.from({ length: 5 }, (_, i) => i * 2)`

* * *

五、分組計數演算法設計
-----------

### 累加器模式（Accumulator Pattern）

遍歷資料時用「累加到容器」的方式統計資料，這是最常見的計數模式：

    // 經典結構
    const counts = {};
    items.forEach((item) => {
      const key = generateKey(item);
      if (counts[key]) {
        counts[key] += 1;
      } else {
        counts[key] = 1;
      }
    });
    

### Key 生成策略（Composite Key）

當需要把「多欄位」或「陣列」轉成唯一的字串 key：

    // 多欄位組合
    const key = `${item.name}-${item.spec}`;
    
    // 陣列排序後組合（順序無關的組合）
    const key = dancer.slice().sort().join('-');
    

**為什麼需要這樣？**  
因為 JS 物件的 key 只能是字串，Map 的 key 雖然可以是任意值，但陣列比較是 reference，不是 value 。所以都選擇「序列化成字串」來當 key。 [developer.mozilla](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Map)

* * *

六、Map vs Object 完整對照
--------------------

### API 語法對照表

操作

Object

Map

初始化

`const obj = {}`

`const map = new Map()`

設定值

`obj[key] = value`

`map.set(key, value)`

取值

`obj[key]`

`map.get(key)`

檢查存在

`key in obj`

`map.has(key)`

刪除

`delete obj[key]`

`map.delete(key)`

大小

`Object.keys(obj).length`

`map.size`

取所有 values

`Object.values(obj)`

`[...map.values()]`

遍歷

`Object.entries(obj).forEach(...)`

`map.forEach((value, key) => ...)`

### 常見陷阱

陷阱

說明

`map[key] = value`

❌ 不會存進 Map，變成物件屬性

`key in obj`

會檢查**原型鏈**（如 `'toString' in obj` 是 `true`）

`map.has(key)`

只檢查你自己設的 key，更純粹 [developer.mozilla](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Map)

    // Object 原型鏈干擾
    const obj = {};
    'toString' in obj;       // true（原型鏈上的）
    
    // Map 乾淨
    const map = new Map();
    map.has('toString');     // false
    

### 效能比較

根據實際測試，Map 在多數情況下有更好的效能表現 ： [zhenghao](https://www.zhenghao.io/posts/object-vs-map)

**記憶體使用：**

*   Map 比 Object 少 20-50% 記憶體使用 [zhenghao](https://www.zhenghao.io/posts/object-vs-map)
    
*   Map 不儲存屬性描述符（writable/enumerable/configurable）
    

**操作速度：**

*   字串 key：Map 在插入、刪除、迭代都較快 [zhenghao](https://www.zhenghao.io/posts/object-vs-map)
    
*   數字 key：Object 在小範圍整數可能較快，但 Map 更穩定
    
*   大量資料（>100000）：Map 優勢明顯 [zhenghao](https://www.zhenghao.io/posts/object-vs-map)
    

### 選擇時機

情境

建議

value 是**純數字/字串**

✅ Map 語意清晰

value 是**複雜物件**

兩者差異不大，選熟悉的

key 是**動態/使用者輸入**

✅ Map 較安全（避免 `__proto__` 衝突）

需要**轉 JSON**

✅ Object（`JSON.stringify(map)` 會失敗）

需要**頻繁增刪** key

✅ Map 效能較好 [zhenghao](https://www.zhenghao.io/posts/object-vs-map)

需要**保持插入順序**

✅ Map（語意更明確） [developer.mozilla](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Map)

* * *

七、解構賦值（Destructuring Assignment）
--------------------------------

### 一句話定義

從陣列或物件中「拆出」值，直接賦給變數，省去一個個取值的步驟。

### 兩種解構方式

類型

匹配方式

語法

**陣列解構**

靠「位置」

`const [a, b] = arr`

**物件解構**

靠「屬性名稱」

`const { name, age } = obj`

### 陣列解構：位置對應

    // 傳統寫法
    const pair = ['紅-藍', 3];
    const key = pair[0];
    const value = pair [jsdevspace.substack](https://jsdevspace.substack.com/p/how-to-master-recursion-in-javascript);
    
    // 解構寫法
    const [key, value] = ['紅-藍', 3];
    // key = '紅-藍'（位置 0）
    // value = 3（位置 1）
    

### 物件解構：名稱對應（順序無關）

    // 傳統寫法
    const dancer = { name: '小明', color: '紅' };
    const name = dancer.name;
    const color = dancer.color;
    
    // 解構寫法（順序可以不同）
    const { color, name } = { name: '小明', color: '紅' };
    // name = '小明', color = '紅'
    

### 函式參數中解構

最常用的情境，直接在參數位置拆值：

    // 陣列解構
    const pairs = [['a', 1], ['b', 2]];
    pairs.forEach(([key, value]) => {
      console.log(key, value);  // 'a' 1, 'b' 2
    });
    
    // 物件解構
    const people = [{ name: '小明', age: 20 }, { name: '小華', age: 25 }];
    people.forEach(({ name, age }) => {
      console.log(name, age);  // '小明' 20, '小華' 25
    });
    

### 實際應用：Map 轉陣列

    const map = new Map();
    map.set('紅-藍', 3);
    map.set('綠-黃', 2);
    
    // map.entries() 展開後是 [['紅-藍', 3], ['綠-黃', 2]]
    // 用陣列解構把每個 [key, value] 拆開，轉成物件
    const result = [...map.entries()].map(([key, value]) => ({ key, value }));
    // [{ key: '紅-藍', value: 3 }, { key: '綠-黃', value: 2 }]
    

* * *

八、JSDoc 型別定義：漸進式導入型別安全
----------------------

### 引入型別的好處

1.  **提示**（開發中）：IDE 自動完成與參數提示 [stefanjudis](https://www.stefanjudis.com/today-i-learned/vs-code-supports-jsdoc-powered-type-checking/)
    
2.  **文件**（開發後）：註解即文件，易於維護
    
3.  **檢查與拋錯**（運行中）：TypeScript 編譯時檢查
    

### JSDoc：用註解完成定義與得到提示

JSDoc 的優勢在於導入成本相對低，因為寫在註解，程式運作不會壞掉，可以只寫其中一部分也沒關係 。 [stefanjudis](https://www.stefanjudis.com/today-i-learned/vs-code-supports-jsdoc-powered-type-checking/)

### 基礎型別定義語法

使用 `@typedef` 定義自訂型別：

    /**
     * @typedef {object} User
     * @property {string} name - 使用者名稱
     * @property {string} email - 使用者信箱
     * @property {number=} age - 可選的年齡
     */
    
    /** @type {User} */
    const user = {
      name: "Alex",
      email: "alex@example.com"
    };
    

### 配合 Copilot 生成 JSDoc

1.  寫好函式或變數
    
2.  在上方輸入 `/**` 按 Enter
    
3.  Copilot 自動生成對應的 JSDoc 註解
    
4.  已經可以在開發過程得到足夠的正確型別提示 [stefanjudis](https://www.stefanjudis.com/today-i-learned/vs-code-supports-jsdoc-powered-type-checking/)
    

### VS Code 型別檢查設定

**方式 1：單檔啟用**

    // @ts-check
    const x = 123;
    x = "hello"; // ❌ Type 'string' is not assignable to type 'number'
    

**方式 2：專案啟用（jsconfig.json）** [stefanjudis](https://www.stefanjudis.com/today-i-learned/vs-code-supports-jsdoc-powered-type-checking/)

    {
      "compilerOptions": {
        "checkJs": true,
        "strict": true
      },
      "exclude": ["node_modules", "**/node_modules/*"]
    }
    

**方式 3：全域設定（settings.json）** [stefanjudis](https://www.stefanjudis.com/today-i-learned/vs-code-supports-jsdoc-powered-type-checking/)

    {
      "javascript.implicitProjectConfig.checkJs": true
    }
    

### 進階：用單獨檔案管理型別定義

**Schemas.js（型別定義檔）：**

    /**
     * @typedef {object} User
     * @property {string} name - 使用者名稱
     * @property {string} gender - 使用者性別
     */
    
    /**
     * @typedef {object} Product
     * @property {string} id - 產品編號
     * @property {number} price - 產品價格
     */
    
    export const Types = {}
    

**main.js（使用型別）：**

    import { Types } from './Schemas.js';
    
    /** @type {import('./Schemas.js').User} */
    const user = {
      name: "Alex",
      gender: "male"
    };
    
    // VS Code 會提示型別錯誤
    user.age = 25; // ❌ Property 'age' does not exist
    

### 使用 ES Module 確保型別一致性

以 ES module `export`/`import` 在開發過程確保宣告的變數符合事先定義的型別 ： [stefanjudis](https://www.stefanjudis.com/today-i-learned/vs-code-supports-jsdoc-powered-type-checking/)

    // types/schemas.js
    /** @typedef {object} Config */
    export const ConfigType = {};
    
    // utils.js
    /** @param {import('./types/schemas.js').Config} config */
    export function init(config) {
      // 型別安全的操作
    }
    

這樣即使不用 TypeScript，也能在純 JavaScript 專案中獲得型別提示與錯誤檢查，開發速度與穩定性兼顧 。 [stefanjudis](https://www.stefanjudis.com/today-i-learned/vs-code-supports-jsdoc-powered-type-checking/)

* * *

九、JavaScript 型別系統冷知識：JSFuck
---------------------------

### 什麼是 JSFuck？

JSFuck 是一種**極限編程風格**，只用 **6 個字元** `[]()!+` 就能寫出任何 JavaScript 程式。

    // 正常寫法
    alert(1)
    
    // JSFuck 寫法（部分）
    [][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]...][(...)...](...)()
    

### 為什麼可行？

核心原理：JavaScript 的**動態弱型別 + 隱性型別轉換** [developer.mozilla](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Guide/Data_structures)

1.  任何程式都能轉成字串
    
2.  字串可以用 `Function()` 建構子執行
    
3.  六個符號透過型別轉換能產生所有需要的字元
    

    // 原理示範
    Function('alert(1)')()  // 等同 alert(1)
    

### 基礎型別轉換規則

#### 產生布林值

    ![]        // false - 空陣列轉布林後取反
    !![]       // true  - 雙重否定
    

#### 產生數字

    +[]        // 0     - 空陣列轉數字
    +!+[]      // 1     - false 轉數字後取反再轉數字
    !+[]+!+[]  // 2     - 兩個 1 相加
    

#### 產生特殊值

    [][[]]     // undefined - 空陣列存取不存在的屬性
    +[![]]     // NaN       - false 轉數字失敗
    

### 字串來源與可用字母

表達式

結果字串

可取字母

`[]+{}`

`"[object Object]"`

b c e j O o t

`false+[]`

`"false"`

a e f l s

`true+[]`

`"true"`

e r t u

`[][[]]+[]`

`"undefined"`

d e f i n u

`+[![]]+[]`

`"NaN"`

a N

### 與 JS 型別系統的連結

JSFuck 完美展示了以下概念 ： [developer.mozilla](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Guide/Data_structures)

1.  **隱性強制轉型（Implicit Coercion）**：`+` 運算子自動判斷要做字串連接或數字相加
    
2.  **動態型別**：同一變數可以是陣列、數字、字串，執行時才決定
    
3.  **弱型別**：自動轉換型別，不需手動宣告
    
4.  **原型鏈**：透過 `constructor` 存取建構函式
    
5.  **Function 即物件**：函式也是物件，有屬性可存取
    

**核心領悟：** JSFuck 不是實用工具，而是 JavaScript **動態弱型別特性**的極致藝術展示。它提醒我們：理解型別轉換規則很重要，否則程式行為會超出預期。

* * *

延伸閱讀
----

### MDN 官方文件

*   [JavaScript 資料型別與資料結構](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Guide/Data_structures) [developer.mozilla](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Guide/Data_structures)
    
*   [Array.prototype.map](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/map) [developer.mozilla](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/map)
    
*   [Array.prototype.fill](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/fill) [developer.mozilla](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/fill)
    
*   [Map](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Map) [developer.mozilla](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Map)
    

### 最佳實踐

*   [JavaScript Recursion Best Practices](https://jsdevspace.substack.com/p/how-to-master-recursion-in-javascript) [jsdevspace.substack](https://jsdevspace.substack.com/p/how-to-master-recursion-in-javascript)
    
*   [TDD in JavaScript Best Practices](https://moldstud.com/articles/p-best-practices-for-implementing-tdd-in-javascript-a-guide-for-developers) [moldstud](https://moldstud.com/articles/p-best-practices-for-implementing-tdd-in-javascript-a-guide-for-developers)
    
*   [Array.from() vs Array.fill()](https://delicious-insights.com/en/posts/js-protip-array-from-fill/) [delicious-insights](https://delicious-insights.com/en/posts/js-protip-array-from-fill/)
    
*   [Map vs Object Performance](https://www.zhenghao.io/posts/object-vs-map) [zhenghao](https://www.zhenghao.io/posts/object-vs-map)
    
*   [VS Code JSDoc Type Checking](https://www.stefanjudis.com/today-i-learned/vs-code-supports-jsdoc-powered-type-checking/) [stefanjudis](https://www.stefanjudis.com/today-i-learned/vs-code-supports-jsdoc-powered-type-checking/)
    

### ECMAScript 規範

*   [ECMAScript 2026 Language Specification](https://tc39.es/ecma262/) [tc39](https://tc39.es/ecma262/)
    
*   [Type Conversion](https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html) [tc39](https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html)

---

*Originally published on [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake/day42-44)*
