如果把程式比成一群演員,「資料型別」就是每個角色的性格與台詞。你不先弄清楚,舞台上就會混亂:字串裝成數字、陣列被當物件、NaN 竟然不等於自己、null 與 undefined 一直互相背鍋。
這篇想做你的排練指導:先介紹 JS 的所有型別與各自的「戲路」,再教你用對方法辨識(什麼時候用 typeof、怎麼判斷陣列、日期、正規表達式),接著帶你處理實務中的硬梆梆問題:表單轉型、API 邊界清洗、金額避免浮點誤差、深拷貝與不可變更新、Map/Set 的出場時機。
文末還整理出常見雷點清單,像「== 帶來的隱式轉型」、「parseInt 要記得 radix」、「structuredClone 的適用時機」等。希望你讀完後,能更懂每個值的個性,讓它們在你的程式裡各就各位、各司其職。希望本篇文章能幫助到需要的您。
目錄
{tocify} $title={目錄}
JavaScript 的資料型別有哪些?
原始型別(Primitive Types)
原始型別是不可變值(immutable)、以值傳遞(pass-by-value),比較快、占記憶體小。
number:含整數與浮點數、特別值 NaN、Infinity、-Infinity
bigint:任意精度整數(以 n 結尾:123n)
string:字串,UTF-16 編碼
boolean:true / false
undefined:未賦值的狀態
null:刻意「空」的狀態(設計歷史造成 typeof null === "object")
symbol:唯一且不可變的識別符(常拿來當物件的非衝突鍵)
ES 規範上還有 undefined 與 null 皆屬於原始型別;symbol、bigint 為較新的原始型別。
物件型別(Object Types)
物件是可變值(mutable)、以參照傳遞(pass-by-reference)。一切複合結構(陣列、日期、正規表達式、Map/Set、函式)本質都屬物件。
Object:萬物之源(平面物件、原型鍊)
Array:有序集合,長度可變
Function:可被呼叫的物件(typeof fn === "function")
Date、RegExp、Map、Set、WeakMap、WeakSet、ArrayBuffer、TypedArray…等
小結:判斷「是不是物件」只能得到一個大方向,要精準判斷(陣列?日期?)需更細的檢查方法。
為何要區分「原始型別」與「物件型別」
1. 賦值/傳遞語意不同
原始型別:拷貝「值」。
物件型別:拷貝「參照」。兩個變數指向同一個實體,改 A 也會影響 B。
2. 不可變 vs 可變
原始型別不可變:str[0] = 'X'不會改變原字串。
物件可變:obj.a = 1 直接改到原物件。
3. 比較方式不同
原始型別比的是值(===)。
物件比的是參照(同個記憶體位址才會 ===)。
4. 效能與記憶體管理
原始型別通常更輕量;物件需要 GC(垃圾回收)處理生命週期。
常用型別判斷方法(由粗到細)
A. typeof:快速判斷原始型別與函式
typeof 42 // "number"
typeof 42n // "bigint"
typeof "hi" // "string"
typeof true // "boolean"
typeof undefined // "undefined"
typeof Symbol("id") // "symbol"
typeof function () {} // "function"
typeof {} // "object"
typeof [] // "object" ← 陣列也是物件
typeof null // "object" ← 歷史包袱,雷點之一
適合: 快速判斷原始型別、或確認「是不是函式」。
不適合: 分辨物件子型別(Array/Date/RegExp…)。
B. Array.isArray(value):辨識陣列的首選
Array.isArray([]) // true
Array.isArray({ length: 1 }) // false
C. instanceof:判斷「是否為某建構函式實例」
[] instanceof Array // true
new Date() instanceof Date // true
(() => {}) instanceof Function // true
限制: iframes / 多重 realm 可能失準;以及對原始型別無效(42 instanceof Number 為 false,除非是 new Number(42) 這種「包裝物件」—不建議使用)。
D. Object.prototype.toString.call(value):最精準的通用解
const tag = Object.prototype.toString.call;
tag([]) // "[object Array]"
tag(new Date()) // "[object Date]"
tag(/re/) // "[object RegExp]"
tag(null) // "[object Null]"
tag(undefined) // "[object Undefined]"
tag(42) // "[object Number]"
tag(42n) // "[object BigInt]"
結論: 真要寫「型別工具箱」,這個方法最穩。
E. 快速對照表(常見值 vs 檢查方法)
陣列:Array.isArray(v) ✅
函式:typeof v === "function" ✅
null:v === null ✅
日期:Object.prototype.toString.call(v) === "[object Date]" ✅
NaN:Number.isNaN(v)(而不是 isNaN)✅
注意:
1. 以上的 v 只是「變數(variable)」或「值(value)」的縮寫的意思。
2. 你可以把它換成任何有意義的名字:value、input、data、maybeDate…都行。
3. 表格裡每一條,其實都在示範「拿某個待檢查的值丟進判斷式」,看看它屬不屬於某種型別。
各型別重點、實務示例與易踩坑位
1. Number(含 NaN / Infinity)
重點:
JS 只有一種數字型別(IEEE 754 雙精度浮點),包含整數與小數。
NaN 代表「不是數字的數字結果」,NaN !== NaN。
Number.isNaN 與全域 isNaN 行為不同(後者會先嘗試轉型)。
示例:
0.1 + 0.2 === 0.3 // false 浮點誤差
Number.isNaN("foo") // false(不會強轉)
isNaN("foo") // true(因為先轉成數字 -> NaN)
Number.isFinite(42) // true
避雷:
金融/金額請用整數(分、厘)+ BigInt 或 decimal library 來避免浮點誤差。
判斷 NaN 請用 Number.isNaN;需要安全有限值判斷用 Number.isFinite。
2. BigInt
重點:
解決大整數精度問題:9007199254740993n 這種超過 Number.MAX_SAFE_INTEGER 的整數。
不可與 Number 直接混算;需要顯式轉型。
示例:
10n + 20n // 30n
Number(10n) + 5 // 15
// 10n + 5 // TypeError: Cannot mix BigInt and other types
避雷:
JSON 目前不支援 BigInt,序列化需自訂。
3. String
重點:
不可變。任何「修改」其實都是回傳新字串。
常見需求:安全轉數字、模板字串。
示例:
const s = "123";
Number(s) // 123
parseInt(s, 10) // 123
`${user.firstName} ${user.lastName}`
避雷:
parseInt("08") 請務必帶 radix:parseInt("08", 10)。
大量串接建議用陣列 + join 或模板字串,易讀又省錯。
4. Boolean
重點:真值/假值(truthy/falsy)規則要熟:
false, 0, -0, 0n, "", null, undefined, NaN 為 falsy,其餘為 truthy(包含 "0", "false", [], {})。
避雷:
想判斷「有沒有內容」時,請區分 "" 與 "0"、[] 與 {} 的語意;別用過度寬鬆的判斷。
5. Undefined vs Null
重點:
undefined:變數宣告了但沒給值、或物件上沒有該屬性。
null:開發者主動賦予的「空」。
示例:
let x;
x // undefined
const obj = { a: null };
obj.b // undefined
obj.a === null // true
避雷:
讀深層屬性請用 可選鏈 ?. + 空值合併 ??:
const city = user?.address?.city ?? "未填寫";
6. Symbol
重點:
每個 Symbol() 都是唯一的,可當作物件鍵,避免鍵名衝突。
const k = Symbol("id");
const o = { [k]: 123 };
o[k] // 123
避雷:
JSON.stringify 會忽略 symbol 鍵;需自訂序列化。
7. Object / Array / Function / Date / RegExp / Map / Set
重點與示例:
// 物件:鍵值對
const user = { id: 1, name: "A" };
// 陣列:有序集合
const arr = [10, 20];
Array.isArray(arr) // true
// 函式:一級公民
function add(a, b) { return a + b; }
const mul = (a, b) => a * b;
typeof add === "function" // true
// 日期
const d = new Date("2025-10-12T00:00:00Z");
Object.prototype.toString.call(d) // "[object Date]"
// 正規表達式
const re = /ab+c/i;
re.test("ABBC") // true
// Map / Set:鍵可非字串、保留插入順序
const m = new Map([["k1", 1]]);
m.get("k1") // 1
const s = new Set([1,2,2]);
s.size // 2
避雷:
陣列 vs 物件用途別混:索引型資料用 Array,字典/表用 Object 或 Map。
需要「鍵不是字串」或需要可預期的迭代順序,選 Map。需要唯一集合選 Set。
Date 解析字串容易踩時區與瀏覽器差異;建議統一用 ISO 8601(UTC)或導入日期函式庫。
深拷貝別用 JSON.parse(JSON.stringify(obj)) 濫用,會丟失 Date/Map/Set/undefined/symbol 等;用結構化拷貝 structuredClone(現代瀏覽器/Node 支援)或專門工具。
型別確認與轉換:實務操作流程(步驟+範例)
流程 1:先判斷是否「可呼叫」或「可迭代」
1. typeof v === "function" → 函式
2. v != null && typeof v[Symbol.iterator] === "function" → 可迭代(陣列、字串、Map、Set…)
流程 2:原始 vs 物件的第一層判斷
v === null → null
Array.isArray(v) → 陣列
typeof v in ("string","number","bigint","boolean","undefined","symbol","function") → 原始/函式
其餘 → 物件分支,使用 Object.prototype.toString.call(v) 精確判斷
流程 3:安全數字處理
function toNumberSafe(x) {
const n = Number(x);
return Number.isFinite(n) ? n : null;
}
解析整數:Number.parseInt(str, 10)
解析浮點:Number.parseFloat(str)
檢查安全整數:Number.isSafeInteger(n)
流程 4:字串化與序列化
通用顯示:String(v)(會呼叫 toString 或 valueOf)
JSON 傳輸:JSON.stringify(obj) / JSON.parse(json)
自訂序列化:JSON.stringify(obj, replacer, space)
流程 5:深拷貝與不可變更新
// 現代環境
const clone = structuredClone(obj);
// 不可變更新(淺)
const updated = { ...obj, name: "New" };
const arr2 = [...arr, 99];
比較與等值:==、===、Object.is 的正確用法
一律優先 ===(嚴格相等,不做隱式轉型)
== 容易引發隱式轉型陷阱:
0 == "" // true
0 == "0" // true
null == undefined // true
Object.is 用於處理 NaN 與 -0/+0 的特殊情況:
Object.is(NaN, NaN) // true
Object.is(0, -0) // false
實戰場景:常用型別模式
1. 表單輸入 → 安全數字
function parseAmount(input) {
const n = Number(input.trim());
if (!Number.isFinite(n) || n < 0) return null;
return Math.round(n * 100); // 轉成「分」避免小數誤差
}
2. API 回傳可能缺欄位 → 可選鏈與空值合併
const city = payload?.user?.address?.city ?? "N/A";
3. 快速分類不同型別的處理器
function handle(value) {
switch (Object.prototype.toString.call(value)) {
case "[object String]": return value.trim();
case "[object Number]": return Number.isFinite(value) ? value : 0;
case "[object Array]": return value.filter(Boolean);
case "[object Object]": return { ...value };
default: return value;
}
}
4. 用 Map 管理快取(鍵可不是字串)
const cache = new Map();
function memo(keyObj, compute) {
if (cache.has(keyObj)) return cache.get(keyObj);
const v = compute();
cache.set(keyObj, v);
return v;
}
5. 安全判斷「陣列非空」
const isNonEmptyArray = (v) => Array.isArray(v) && v.length > 0;
常見錯誤與雷點
1. typeof null === "object"
現象:以為 null 是物件。
做法:判斷 null 請用 v === null。
2. 陣列判斷錯誤(用 typeof 得到 "object")
做法:用 Array.isArray(v)。
3. isNaN 與 Number.isNaN 混用
isNaN("foo") === true(先轉型)
Number.isNaN("foo") === false(不轉型)
做法:判斷 NaN 一律用 Number.isNaN。
4. 浮點數誤差(0.1 + 0.2 !== 0.3)
做法:金額用整數+縮放、或使用 BigInt/十進位套件。
5. 物件比較用 === 以為比內容
({a:1}) === ({a:1}) // false
做法:比內容請序列化或寫深比較函式。
6. JSON 深拷貝萬能論
會丟失 Date/Map/Set/undefined/symbol/函式。
做法:用 structuredClone 或專門工具。
7. parseInt 不給 radix
parseInt("08") 在舊環境可能被視為八進位。
做法:parseInt(str, 10)。
8. BigInt 與 Number 直接混算
會拋錯。
做法:顯式轉換後再計算。
9. == 帶來隱式轉型坑
做法:預設使用 === 與 !==。
10. 日期字串與時區混亂
做法:統一 ISO 8601 / UTC,或使用可靠日期工具,前端顯示再做本地化。
11. 把函式當一般物件存;或忘了判斷可呼叫性
做法:typeof v === "function" 再呼叫;或用 TypeScript 加上型別註記。
12. 把空陣列/空物件當 falsy
[] 與 {} 在 JS 是 truthy。
做法:判斷長度或鍵數:Array.isArray(v) && v.length > 0、Object.keys(o).length > 0。
從零開始:型別檢查與防呆的「專案預設」做法
1. 建立一個 types.js 小工具庫
// types.js
const tag = (v) => Object.prototype.toString.call(v);
export const isNil = (v) => v == null; // null or undefined
export const isStr = (v) => typeof v === "string";
export const isNum = (v) => typeof v === "number" && Number.isFinite(v);
export const isBigInt = (v) => typeof v === "bigint";
export const isBool = (v) => typeof v === "boolean";
export const isSym = (v) => typeof v === "symbol";
export const isFn = (v) => typeof v === "function";
export const isArr = Array.isArray;
export const isDate = (v) => tag(v) === "[object Date]";
export const isRegExp = (v) => tag(v) === "[object RegExp]";
export const isObj = (v) => tag(v) === "[object Object]";
export const isMap = (v) => tag(v) === "[object Map]";
export const isSet = (v) => tag(v) === "[object Set]";
// 安全轉型
export const toNumSafe = (x) => {
const n = Number(x);
return Number.isFinite(n) ? n : null;
};
2. 表單/接口邊界全面收斂型別
輸入一律字串化 → 再依需求轉型。
API 解析層做一次型別清洗(validation + transform),內部程式不再承擔模糊型別。
3. ESlint/TypeScript 加強保障(可選)
規則:禁止 ==、禁止 new Number/String/Boolean 包裝物件、禁止未處理的 any。
TypeScript:在編譯期把大部分錯誤擋掉,runtime 再加少量守門人。
問題集
Q1:我要判斷「空字串或只包含空白」怎麼寫?
const isBlank = (s) => typeof s === "string" && s.trim().length === 0;
Q2:怎麼判斷一個值「可被迭代」?
const isIterable = (v) => v != null && typeof v[Symbol.iterator] === "function";
Q3:如何判斷「純物件(Plain Object)」?
const isPlainObject = (v) =>
Object.prototype.toString.call(v) === "[object Object]" &&
(Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null);
Q4:為什麼 typeof new Number(1) === "object"?
因為它是包裝物件。請避免用 new Number/String/Boolean,會帶來混淆與效能負擔。
Q5:如何安全地合併物件?
const merged = { ...a, ...b }; // 淺合併
// 深合併請用專門工具(如 lodash.merge)或自行遞迴
範例集:從壞到好(常見坑位改寫)
範例 A:比較數字(壞)
if (price == "0") { /* ... */ } // 隱式轉型,容易誤判
應改(好)
if (Number(price) === 0) { /* ... */ }
範例 B:判斷陣列(壞)
if (typeof list === "array") { /* ... */ } // 永遠不會是 "array"
應改(好)
if (Array.isArray(list)) { /* ... */ }
範例 C:處理 NaN(壞)
if (value !== NaN) { /* ... */ } // 永遠為 true
應改(好)
if (!Number.isNaN(value)) { /* ... */ }
範例 D:深拷貝(壞)
const deep = JSON.parse(JSON.stringify(obj)); // 丟失 Date/Map/Set...
應改(好)
const deep = structuredClone(obj);
範例 E:金額相加(壞)
const sum = 0.1 + 0.2; // 0.30000000000000004
應改(好)
const sumCents = Math.round(0.1 * 100) + Math.round(0.2 * 100);
const sum = sumCents / 100; // 0.3
