JavaScript 是动态类型语言:变量绑定没有固定类型,值有类型。语言值分为七种基本值和对象。
运算符和条件会按各自规则强制转换;"false" 是真值,Number("") 是 0,而 + 既可能相加也可能拼接。
在输入边界按字段语义显式解析并验证,默认值优先区分 null/undefined 与其他假值,比较时明确选择相等算法。
是什么,为什么存在
JavaScript 采用 动态类型(dynamic typing) :类型属于运行时的值,而不是变量名。一个 let 绑定可以先指向数字,之后再指向字符串;这不会改变原值的类型,而是让绑定指向另一个值。动态类型让同一套语法处理来自表单、JSON 和库调用的不同数据,但边界上的类型假设必须由代码写清楚。
语言中的值分为对象和七种 基本值(primitive value) :undefined、null、Boolean、Number、BigInt、String 与 Symbol。基本值本身不可变;对字符串做替换或转大小写会得到新字符串。对象可以拥有可变属性,数组和函数也属于对象,不是额外的语言类型。
赋值与传参传递的都是值。复制基本值会得到那个基本值;复制对象值会得到指向同一对象的引用,因此两个绑定可以观察同一批属性变化。把这个区别说成“基本值在栈上、对象在堆上”并不可靠,那是实现策略,不是 ECMAScript 语义。
const 约束绑定,不能让它改指向另一个值,却不会冻结对象。const order = { count: 1 } 仍允许修改 order.count。需要只读对象契约时,应通过 API 设计、复制或冻结表达,而不能从 const 推断深层不可变。
这些类型在三个地方最容易影响业务结果:检查外部输入、选择运算符语义,以及判断两个值是否相等。问题通常不是“JavaScript 没有类型”,而是代码把输入类型、转换规则或缺失值策略留成了隐含假设。
工作原理
七种基本类型与对象
undefined 通常表示尚未提供值,例如缺失属性或没有显式返回值的函数。null 是程序显式使用的空值。两者是否含义不同由接口契约决定,语言只规定它们是两个不同的基本值。
Boolean 只有 true 和 false。String 是 UTF-16 码元序列,Symbol 是具有唯一标识的基本值,也可作为属性键。Number 使用 IEEE 754 双精度浮点格式,同时表示普通整数、小数、Infinity 和 NaN。
BigInt 表示任意精度整数,字面量以 n 结尾。它与 Number 是不同的数值类型;大多数算术运算不能混用两者,因为运行时不能擅自决定应保留 BigInt 的精度,还是采用 Number 的范围与小数语义。比较或转换前,应先确定领域需要哪一种数值表示。
Object 是其余语言值的类型。普通对象、数组、日期、正则表达式、Map、Set 和函数都在这一类中。不同对象即使属性相同也有不同标识;多个引用只有在指向同一个对象时才通过严格相等比较。
typeof 是粗粒度检查
typeof 返回固定字符串,适合区分基本类型和检测可调用值,但它不是通用的对象分类器。下面的表格给出常见结果与更精确的检查方式。
| 值 | typeof 结果 | 需要更精确检查时 |
|---|---|---|
undefined | "undefined" | 通常无需补充 |
null | "object" | value === null |
| Boolean | "boolean" | 通常无需补充 |
| Number | "number" | Number.isFinite()、Number.isNaN() |
| BigInt | "bigint" | 通常无需补充 |
| String | "string" | 通常无需补充 |
| Symbol | "symbol" | 通常无需补充 |
| 函数 | "function" | 必要时再检查具体协议 |
| 数组及其他对象 | "object" | Array.isArray() 或领域检查 |
typeof null === "object" 是保留至今的兼容行为。数组也返回 "object",所以数组分支必须用 Array.isArray(value)。判断“非空对象”至少需要 value !== null && typeof value === "object",之后仍要校验所需属性,而不是把所有对象视为同一种记录。
转换取决于上下文
类型强制转换(type coercion) 是操作按需要自动转换值。条件与 ! 使用布尔转换;多数算术运算使用数值转换;模板插值和 String() 使用字符串转换。显式调用 Boolean()、Number() 或 String() 使用相关规则,但“显式”不表示结果一定符合业务语义。
布尔转换只有一组固定假值:false、0、-0、0n、""、null、undefined 和 NaN。其余语言值都是真值,包括 "0"、"false"、空数组和空对象。if (value) 适合询问“它的真值是什么”,不适合代替“字段是否存在”或“数组是否为空”。
&& 和 || 会检查操作数的真值,但返回某个原操作数,不会固定返回 Boolean。left && right 在 left 为假值时返回 left,否则返回 right;left || right 在 left 为真值时返回 left,否则返回 right。?? 只在左侧为 null 或 undefined 时取右侧,因此能保留有效的 0、false 和空字符串。
数值转换也有需要在边界上防守的结果。Number("") 和 Number(" ") 都是 0,Number(null) 也是 0,无效数字文本则得到 NaN。parseInt() 解析前缀,所以 parseInt("12px", 10) 得到 12;它适合明确允许后缀的语法解析,不适合验证“整个字段必须是十进制整数”。
运算符决定强制转换路径
减法、乘法、除法和取余先把操作数转换为数值。加法先把对象转换为基本值;若任一结果是 String,就执行拼接,否则执行数值加法。因此,1 + 2 + "3" 是 "33",而 "1" + 2 + 3 是 "123",因为表达式从左向右结合。
关系比较同样不能概括成“都转数字”。两个操作数转换为基本值后,如果两者都是字符串,就按字符串的 UTF-16 码元顺序比较;否则走数值比较。于是 "10" < "2" 为 true,而 Number("10") < Number("2") 为 false。
宽松相等 == 使用抽象相等比较,可能转换操作数,但不是简单地“把两边转成同一类型”。例如 null == undefined 为 true,两者与其他值的宽松相等比较通常为 false;Boolean 会先转换为 Number;对象与基本值比较时会先进行对象到基本值转换。除非接口有意使用 value == null 同时匹配两个空值,业务代码通常更适合使用严格相等并在边界显式转换。
相等不是一种算法
严格相等 === 不做类型转换。不同类型直接不相等;对象按标识比较;NaN 与自身不相等;+0 与 -0 相等。它是大多数分支判断的默认选择,但不能解决内容相同对象的结构比较。
Object.is() 使用 SameValue 语义:Object.is(NaN, NaN) 为 true,Object.is(+0, -0) 为 false。Array.prototype.includes()、Map 键和 Set 成员使用 SameValueZero(同值零相等) ,它把 NaN 视为等于自身,同时把两个零视为相等。选择集合或查找 API 时,这些边界差异会直接决定命中结果。
示例
下面四个示例依次检查类型、解析外部配置、观察运算符转换,并比较相等算法。所有输出都由本地 Node 24 执行对应文件得到。
检查语言类型
这个探针先用 typeof 做粗分类,再为 null 和数组补上专用判断。它没有尝试为每个内置对象制造一个“精确类型名”,因为业务代码通常需要验证能力或字段,而不是依赖可伪造的标签。
const samples = [
undefined,
null,
false,
42,
42n,
"42",
Symbol("id"),
[42],
{ value: 42 },
() => 42,
];
for (const value of samples) {
const kind = value === null
? "null"
: Array.isArray(value)
? "array"
: typeof value;
console.log(kind);
}undefined
null
boolean
number
bigint
string
symbol
array
object
function这里的 kind 是应用层分类,不是 ECMAScript 新增的类型系统。它有意回答“是否为 null、数组或其他 typeof 类别”,若函数只接受有限结构,还应继续检查自有属性及其值的类型。
在边界解析配置
环境变量、URL 参数和表单字段通常以字符串进入程序。这个解析器只接受契约列出的拼写,拒绝部分数字,并用 ?? 保留空标签。
function parseBoolean(value, field) {
if (value === true || value === "true") return true;
if (value === false || value === "false") return false;
throw new TypeError(`${field} must be true or false`);
}
function parseRetries(value) {
const text = String(value);
if (!/^(0|[1-9]\d*)$/.test(text)) {
throw new TypeError("retries must be a non-negative integer");
}
const retries = Number(text);
if (!Number.isSafeInteger(retries)) {
throw new RangeError("retries is outside the safe integer range");
}
return retries;
}
function parseOptions(raw) {
return {
retries: parseRetries(raw.retries),
verbose: parseBoolean(raw.verbose, "verbose"),
label: raw.label ?? "worker",
};
}
console.log(parseOptions({ retries: "0", verbose: "false", label: "" }));
console.log(parseOptions({ retries: 3, verbose: true }));{ retries: 0, verbose: false, label: '' }
{ retries: 3, verbose: true, label: 'worker' }Number.isSafeInteger() 确保结果是 安全整数(safe integer) ,而不是已经舍入的 Number。解析策略是接口的一部分:如果输入契约允许前后空白、正号或本地化数字,应在正则与测试中明确加入,而不是依赖转换函数猜测。
观察上下文转换
同一个字符串在不同运算符中会走不同路径。JSON.stringify() 让空字符串和字符串中的空格在输出里可见,避免终端显示掩盖结果。
const inputs = ["5", "", "false", 0, null];
for (const value of inputs) {
console.log(JSON.stringify({
input: value,
boolean: Boolean(value),
number: Number(value),
defaultWithOr: value || "fallback",
defaultWithNullish: value ?? "fallback",
}));
}
console.log("5" + 2);
console.log("5" - 2);
console.log(1 + 2 + "3");{"input":"5","boolean":true,"number":5,"defaultWithOr":"5","defaultWithNullish":"5"}
{"input":"","boolean":false,"number":0,"defaultWithOr":"fallback","defaultWithNullish":""}
{"input":"false","boolean":true,"number":null,"defaultWithOr":"false","defaultWithNullish":"false"}
{"input":0,"boolean":false,"number":0,"defaultWithOr":"fallback","defaultWithNullish":0}
{"input":null,"boolean":false,"number":0,"defaultWithOr":"fallback","defaultWithNullish":"fallback"}
52
3
33第三行的 number 显示为 null,不是因为 Number("false") 返回 null,而是因为它返回 NaN,且 JSON 数组或对象序列化会把非有限 Number 写成 null。调试转换时,应直接检查 Number.isNaN(),不要把序列化文本误当作内存中的原值。
对比相等与对象标识
这个例子把严格相等、SameValue 和 SameValueZero 放在一起。最后两行表明对象内容相同不代表对象标识相同。
console.log(NaN === NaN);
console.log(Object.is(NaN, NaN));
console.log([NaN].includes(NaN));
console.log(+0 === -0);
console.log(Object.is(+0, -0));
console.log(new Set([+0, -0]).size);
const first = { id: 7 };
const alias = first;
const copy = { id: 7 };
console.log(first === alias);
console.log(first === copy);false
true
true
true
false
1
true
false若业务需要按 id 判断记录是否代表同一实体,应直接比较经过验证的 id。通用深比较牵涉循环引用、原型、访问器、Symbol 键和领域规则,不应把 JSON.stringify(a) === JSON.stringify(b) 当成可靠替代品。
陷阱
把 typeof 当成完整分类器
修复方法: 先排除 null,数组用 Array.isArray()。随后按接口验证所需字段、属性所有权与值类型,不要从一个粗粒度标签推断完整结构。
用 Boolean() 解析布尔文本
修复方法: 为边界定义允许的文本,例如只接受 "true" 与 "false",并对其他拼写报错。只有输入本来就是语言值、且目标确实是其真值时才使用 Boolean()。
用 || 覆盖所有假值
修复方法: 缺失只指 null 或 undefined 时使用 ??。如果空字符串或零在当前领域也算缺失,应写出具名验证条件,让读者看见这项业务规则。
把转换成功当成输入有效
修复方法: 先验证完整输入语法,再转换并检查范围。整数还要根据领域选择 Number.isInteger() 或 Number.isSafeInteger();不要用全局 isNaN(),因为它会先转换参数。
忘记 + 的双重语义
修复方法: 在计算边界先解析成同一种数值类型,再执行算术。生成消息时使用模板字面量表达字符串意图,不要借用空字符串加法完成转换。
混淆值相等与对象内容
修复方法: 普通分支默认用 ===。NaN 用 Number.isNaN(),集合成员语义参考具体 API;领域对象则比较经过定义的键或实现符合契约的结构比较。
绑定、修改与包装对象
绑定与绑定当前保存的值是模型中的两个部分。把 let current = 1 重新赋值为 current = 2,改变的是绑定,不是 Number 值 1。修改则作用于对象的属性或内部状态。这项区别解释了为什么 const 对象仍可修改,而 let 保存的 String 仍不可变。
| 操作 | 绑定是否变化 | 现有值是否修改 |
|---|---|---|
current = other | 是 | 否 |
record.count = 2 | 否 | 是,前提是写入成功 |
text.toUpperCase() | 否 | 否;它返回新的 String 值 |
items.push(value) | 否 | 是;Array 对象发生变化 |
Object.freeze(record) | 否 | 对象通过普通数据属性变成浅层不可写 |
基本值的属性访问看起来像对象操作,是因为 JavaScript 临时提供了包装行为。因此,"hi".toUpperCase() 可以执行,但 String 值并不是对象。这套临时机制不会把原基本值变成可修改记录。
不带 new 调用 String(value)、Number(value) 或 Boolean(value) 会执行转换。使用 new String()、new Number() 或 new Boolean() 则会创建包装对象。所有对象都是真值,所以 new Boolean(false) 也是真值,几乎从来不是应用真正需要的值。
包装对象与其基本值载荷也不会通过严格相等比较:new Number(3) !== 3。普通 API 边界应接收基本值,除非契约明确需要对象标识。与旧代码集成时若要取出包装值,可调用 value.valueOf(),然后继续验证得到的基本类型。
Object.freeze() 是浅层操作:它阻止对冻结对象自有属性的普通修改,但嵌套对象仍可能可变。冻结是对象策略,不能让本来就不可变的基本值变得“更不可变”。
函数是可调用对象,虽然 typeof 为方便起见返回 "function"。函数可以拥有属性,也像其他对象一样按标识比较;这个特殊结果并没有产生第九种语言类型。
对象到基本值的转换
需要基本值的运算遇到对象时,会执行规范中的 ToPrimitive 抽象操作。若对象提供 [Symbol.toPrimitive](hint),运行时先调用它;返回值必须是基本值,否则抛出 TypeError。hint 可能是 "number"、"string" 或 "default",它表达语法期望,不是强制要求返回某一种基本类型。
没有自定义钩子时,普通转换会按提示尝试 valueOf() 与 toString()。字符串提示通常先试 toString(),其他提示通常先试 valueOf();只有方法返回基本值时才停止。Date 对默认提示有特殊处理,因此“对象加空字符串总是先调用 valueOf()”并不是通用规则。
加法先对两边执行基本值转换。只要转换后有一边是 String,就把两边转为 String 并拼接;否则把两边转为数值后相加。自定义 Symbol.toPrimitive 会影响日志、模板、比较与算术,若返回结果随隐藏状态变化,调用点就很难推断,应把这种钩子保持简单且有明确领域含义。
字符串转换 Symbol 时也有一处容易遗漏的差异。String(symbol) 会返回描述文本,但隐式字符串拼接和模板插值对 Symbol 值会抛出 TypeError。诊断代码需要接受字符串或 Symbol 时,应显式调用 String(key)。
相等算法的边界
ECMAScript 并非所有 API 都调用同一种相等关系。下面的表格概括四种常见语义;“对象”一栏全部按对象标识比较,不会自动检查属性内容。
| 算法或入口 | 是否强制转换 | NaN 与自身 | +0 与 -0 | 常见入口 |
|---|---|---|---|---|
| 抽象相等 | 是 | 不相等 | 相等 | == |
| 严格相等 | 否 | 不相等 | 相等 | ===、indexOf() |
| SameValue | 否 | 相等 | 不相等 | Object.is() |
| SameValueZero | 否 | 相等 | 相等 | includes()、Map、Set |
宽松相等不是随机规则,但它的分支多,且对象转换可能调用用户代码。value == null 是一个常见而窄的惯用法,只匹配 null 与 undefined;若团队采用它,应通过 lint 例外和注释保留这项意图。其他跨类型比较通常在边界先转换,再用严格相等更容易审查。
switch 的匹配使用严格相等,因此 case NaN 永远不会命中 NaN。数组的 indexOf(NaN) 也找不到 NaN,而 includes(NaN) 可以。测试查找逻辑时应调用实际生产 API,不能用另一种“看起来等价”的比较代替。
Number、BigInt 与序列化边界
Number 能精确表示的整数范围有限,Number.isSafeInteger() 用于判断整数是否仍有无歧义的表示。超出范围的连续整数可能舍入到同一个 Number,因此先用 Number(largeText) 转换、再检查结果是否安全,最多只能拒绝不安全结果,不能恢复已经丢失的数字。需要大整数时,应直接从原始文本构造 BigInt。
BigInt 不表示小数,也不能与 Number 混合进行大多数算术运算。关系比较允许跨这两种数值类型比较,但显式统一类型更能暴露精度策略。把任意 Number 转成 BigInt 还要求该 Number 是整数;若它早已越过安全整数范围,转换只会精确保留那个已经舍入的 Number。
默认的 JSON.stringify() 不支持 BigInt,会抛出 TypeError。接口必须决定把大整数编码成十进制字符串、限制在安全 Number 范围内,或使用另一个有明确整数类型的协议。读取 JSON 后也不能靠观察一个 Number 判断原始发送方是否已经丢失精度,契约应在传输前确定表示。
NaN、Infinity 和 -Infinity 是 Number 值,但 JSON 会把对象属性或数组元素中的它们写成 null。因此,JSON 往返不能保留所有 JavaScript Number。需要区分非有限值时,应在序列化前拒绝它们或使用约定的显式标签。
延伸阅读
4个问题 · 2 道输出预测题 · 1 道找错题