【JS 进阶】你真的掌握变量和类型了吗
本文保留原文关于变量复制、原始值和对象、
null/undefined、Symbol、浮点数、类型转换以及类型判断的完整路线,并按当前 ECMAScript 语义修正“只有 7 种类型”“所有值固定在栈/堆”“Symbol 可以防 XSS”“toString(2)就是内存位”等过时或错误说法。
导读
学习 JavaScript 时,经常会遇到这些问题:
- JavaScript 的变量到底保存了什么?
0.1 + 0.2为什么不等于0.3?- Symbol 和 BigInt 解决什么问题?
[] == ![]、[undefined] == false为什么为true?- 如何区分
Array、Date、跨窗口对象和普通对象? - 函数参数到底是值传递还是引用传递?
这些问题不能只靠“栈里放基本类型、堆里放引用类型”解释。更稳妥的学习方式是先掌握 ECMAScript 的值、对象身份、抽象操作和比较规则,再把引擎的内存实现作为性能背景。
一、JavaScript 数据类型
当前 ECMAScript 有 8 种语言类型,分为 7 种原始类型和 1 种对象类型。
1.1 原始类型
Undefined:值为undefined;Null:值为null;Boolean:true或false;Number:双精度二进制浮点数,包含NaN、Infinity、-Infinity和-0;BigInt:任意精度整数;String:字符串值;Symbol:唯一的符号值,可作为属性键。
1.2 对象类型
对象是属性集合,也可以拥有内部槽和行为。普通对象、数组、函数、日期、正则、Map、Set、Promise、TypedArray 等都属于对象。
早期文章把 BigInt 写成“ES10 中加入的第七种类型”。更准确地说,BigInt 在 ES2020(ECMAScript 2020)中标准化;因此今天不能再说 JavaScript 只有 7 种类型。
二、原始值和对象的区别
2.1 原始值不可变,但变量可以重新绑定
字符串的方法会返回新字符串,不会修改原字符串:
let str = 'ConardLi'
str.slice(1)
str.substring(1)
str.trim()
str.toLowerCase()
// 非严格模式下给字符串索引赋值通常无效;严格模式下可能抛错
// str[0] = 'X'
console.log(str) // ConardLi
变量重新赋值并不等于修改了原始值:
str += '6'
console.log(str) // ConardLi6
这里是计算了新的字符串值,再让 str 绑定到它。规范关注的是字符串不可变和变量绑定结果,不规定字符串一定位于某个栈槽中。

图片来源:原文配图。已下载为本地图片;原图来源站点的版权和许可信息仍然适用。
2.2 对象具有可变属性和身份
对象的属性通常可以修改,但“对象可变”不是所有对象的绝对性质:被冻结的对象、某些宿主对象和封装的类实例可能限制修改。
const obj1 = { name: 'ConardLi' }
const obj2 = { age: 18 }
const obj3 = function () {}
const obj4 = [1, 2, 3]
obj1.name = 'ConardLi6'
obj2.age = 19
obj4.length = 0
console.log(obj1) // { name: 'ConardLi6' }
console.log(obj2) // { age: 19 }
console.log(obj4) // []
数组的 pop、push、shift、unshift、reverse、sort、splice 会改变数组;map、filter、slice、toSorted 等通常返回新数组,但回调仍可能修改数组元素对象。

图片来源:原文配图。
2.3 不要把栈堆模型当作规范
“原始值在栈、对象在堆、对象变量保存指针”是便于入门的图示。ECMAScript 没有规定这些物理位置;引擎可以使用寄存器、指针压缩、内联小整数、逃逸分析和优化后的环境表示。
真正需要掌握的是:
const a = { value: 1 }
const b = a
console.log(a === b) // true,共享同一个对象身份
b.value = 2
console.log(a.value) // 2
如果需要新对象,要显式复制:
const shallow = { ...a }
shallow.value = 3
console.log(a.value) // 2
console.log(shallow === a) // false

图片来源:原文配图。
2.4 复制和比较
原始值比较的是值:
const name = 'ConardLi'
const name2 = 'ConardLi'
console.log(name === name2) // true
对象比较的是身份,而不是结构:
const first = { name: 'ConardLi' }
const second = { name: 'ConardLi' }
const third = first
console.log(first === second) // false
console.log(first === third) // true

图片来源:原文配图。

图片来源:原文配图。
三、函数参数是按值传递
ECMAScript 中函数参数按值传递。传入对象时,这个值是对象引用的副本,因此可以修改同一个对象的属性,但重新绑定参数不会影响调用者:
let name = 'ConardLi'
function changeValue(value) {
value = 'code秘密花园'
}
changeValue(name)
console.log(name) // ConardLi
const user = { name: 'ConardLi' }
function changeUser(value) {
value.name = 'code秘密花园'
value = { name: 'another user' }
}
changeUser(user)
console.log(user.name) // code秘密花园
因此“对象是引用传递”是容易造成误解的口语。更准确的说法是:所有参数都按值传递;对象参数的值是一个引用对象的值。
const obj = {}
function change(obj) {
obj.name = 'ConardLi'
obj = { name: 'code秘密花园' }
}
change(obj)
console.log(obj.name) // ConardLi
四、null 和 undefined
null 和 undefined 都是原始值,但表达的意图通常不同:
undefined常表示没有提供值、属性不存在或函数没有显式返回;null常表示程序明确设置了“没有对象/空值”。
这只是编码约定,不是强制规则。对象属性显式为 undefined 也可能是合法数据,不能简单地说“这是不正常的,必须 delete”。判断属性是否存在应使用 in 或 Object.hasOwn():
const a = {}
const b = { value: undefined }
console.log('value' in a) // false
console.log('value' in b) // true
console.log(Object.hasOwn(b, 'value')) // true
数值转换:
console.log(Number(null)) // 0
console.log(Number(undefined)) // NaN
console.log(null == undefined) // true
console.log(null === undefined) // false
typeof null === 'object' 是早期 JavaScript 的历史兼容结果,不是说 null 真的是对象。使用 value === null 可以准确判断 null。

图片来源:原文配图。
五、Symbol
5.1 独一无二和全局注册表
每次调用 Symbol() 都创建一个不同的 Symbol;描述只是调试信息,不参与相等比较:
const sym1 = Symbol('ConardLi')
const sym2 = Symbol('ConardLi')
console.log(sym1 === sym2) // false
console.log(sym1.description) // ConardLi
Symbol.for(key) 使用全局 Symbol 注册表,同一个注册表键可以取回同一个 Symbol:
const globalA = Symbol.for('ConardLi')
const globalB = Symbol.for('ConardLi')
console.log(globalA === globalB) // true
console.log(Symbol.keyFor(globalA)) // ConardLi
“全局”指当前 JavaScript agent 的注册表,不等于跨进程、跨页面或跨 Realm 的万能共享存储。
5.2 Symbol 不能用 new
console.log(typeof Symbol('x')) // symbol
// new Symbol('x') // TypeError: Symbol is not a constructor
5.3 Symbol 属性的枚举
Symbol 属性不会出现在 for...in、Object.keys() 或 Object.getOwnPropertyNames() 中,但它们不是私密属性;可以用 Object.getOwnPropertySymbols() 或 Reflect.ownKeys() 读取:
const key = Symbol('internal')
const object = {
name: 'Ada',
[key]: 42
}
console.log(Object.keys(object)) // ['name']
console.log(Object.getOwnPropertyNames(object)) // ['name']
console.log(Object.getOwnPropertySymbols(object)) // [Symbol(internal)]
console.log(Reflect.ownKeys(object)) // ['name', Symbol(internal)]
5.4 Symbol 的应用
防止属性名冲突
const internal = Symbol('internal')
const state = {
[internal]: { loading: false }
}
console.log(state[internal]) // { loading: false }
这可以降低属性名冲突,但不能提供真正的安全隔离;拿到 Symbol 的代码仍然可以访问该属性。
Well-known Symbols
Symbol.iterator、Symbol.asyncIterator、Symbol.toPrimitive、Symbol.toStringTag 等 well-known symbols 可以定制语言协议:
const range = {
start: 1,
end: 3,
*[Symbol.iterator]() {
for (let value = this.start; value <= this.end; value += 1) {
yield value
}
}
}
console.log([...range]) // [1, 2, 3]
React 示例的边界
早期文章把 React 元素的 $$typeof = Symbol.for('react.element') 解释成防止 XSS,这是错误的安全结论。它可以帮助 React 区分特定的数据结构,且 Symbol 不会被普通 JSON 直接表示,但它不能清洗 HTML、阻止恶意脚本或替代服务端/客户端的输出编码。
如果应用使用 dangerouslySetInnerHTML,仍然必须对 HTML 做可信来源控制和适当消毒。原文的 React 属性访问写法是无效语法;历史代码应写成普通属性名 object.$$typeof,并理解它只是库内部约定:
const REACT_ELEMENT_TYPE = Symbol.for('react.element')
function isReactElement(value) {
return Boolean(
value &&
typeof value === 'object' &&
value.$$typeof === REACT_ELEMENT_TYPE
)
}
六、Number、二进制浮点数和精度
6.1 为什么 0.1 + 0.2 !== 0.3
JavaScript 的普通 Number 使用 IEEE 754 binary64 双精度格式。许多十进制小数不能用有限位二进制精确表示,输入会先舍入到最接近的可表示值,运算结果再进行舍入:
console.log(0.1 + 0.2) // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3) // false
这不是简单的“JavaScript 只保留小数点后几位”。binary64 由以下部分组成:
- 1 位符号位;
- 11 位指数位;
- 52 位显式 fraction 位;
- 对普通数值而言,隐藏的最高有效位使有效精度通常为 53 个二进制位。

图片来源:原文配图。
6.2 toString(2) 不是原始内存位
console.log((0.1).toString(2))
console.log((0.2).toString(2))
Number.prototype.toString(2) 返回一个能代表该 Number 值的二进制字符串,通常采用足以往返恢复该值的最短表示;它不是把内存中的符号位、指数位和 fraction 位原样打印出来。原文把这两者等同起来是不准确的。
下面的示例可以查看某个 Number 的 binary64 位模式,但这只是观察工具:
function numberBits(value) {
const buffer = new ArrayBuffer(8)
new DataView(buffer).setFloat64(0, value, false)
return [...new Uint8Array(buffer)]
.map(byte => byte.toString(2).padStart(8, '0'))
.join('')
}
console.log(numberBits(0.1)) // 64 位编码字符串

图片来源:原文配图。

图片来源:原文配图。

图片来源:原文配图。
6.3 如何比较带误差的结果
Number.EPSILON 只适合某些接近 1 的相对误差判断,不是所有数量级的通用阈值。常用的相对/绝对误差函数如下:
function nearlyEqual(a, b, relative = 1e-12, absolute = Number.EPSILON) {
const difference = Math.abs(a - b)
if (difference <= absolute) return true
return difference <= relative * Math.max(Math.abs(a), Math.abs(b))
}
console.log(nearlyEqual(0.1 + 0.2, 0.3)) // true
金额、计量和需要十进制定点语义的场景,应使用整数最小单位、十进制定点库或经过验证的 decimal 库;不要只用 toFixed() 就认为计算已经精确,因为 toFixed() 主要是格式化。
6.4 最大值和安全整数
console.log(Number.MAX_VALUE) // 1.7976931348623157e+308
console.log(Number.MAX_SAFE_INTEGER) // 9007199254740991
console.log(Number.isSafeInteger(Number.MAX_SAFE_INTEGER)) // true
console.log(Number.isSafeInteger(Number.MAX_SAFE_INTEGER + 1)) // false
MAX_VALUE 是接近可表示上限的最大有限 Number;MAX_SAFE_INTEGER 讨论的是整数精确性,不是所有小数都能在这个范围内精确表示。
6.5 BigInt
const large = 9007199254740993n
console.log(large + 2n) // 9007199254740995n
console.log(large.toString()) // 9007199254740993
BigInt 用于任意精度整数,但不能与 Number 直接混合算术:
// 1n + 1 // TypeError
console.log(5n / 2n) // 2n,整数除法向零截断
BigInt 不能表示小数,不能直接用于 Math.*,JSON 也不能默认序列化它。与外部 API 交互时要明确使用字符串、数字或 BigInt 的边界。
七、包装对象、装箱和拆箱
原始值不是对象,但语言允许它们调用很多方法。引擎会根据需要执行临时的装箱行为:
const name = 'ConardLi'
const upper = name.toUpperCase()
console.log(upper) // CONARDLI
显式包装对象与原始值不同:
console.log(true === new Boolean(true)) // false
console.log(123 === new Number(123)) // false
console.log('x' === new String('x')) // false
console.log(typeof new String('x')) // object
通常不要用 new String()、new Number() 或 new Boolean() 保存业务值,因为它们会带来对象身份和真值陷阱:
if (new Boolean(false)) {
console.log('对象本身是真值')
}
八、ToPrimitive 和类型转换
对象参与运算时,通常会通过 ToPrimitive 转换成原始值。现代规范中,流程会优先检查 Symbol.toPrimitive;若没有,再根据 hint 调用 valueOf() 和 toString()。具体顺序不是简单地“所有数字都先 valueOf,所有字符串都先 toString”,还要考虑 hint 和 Date 等特殊对象。
const object = {
valueOf() {
console.log('valueOf')
return 123
},
toString() {
console.log('toString')
return 'ConardLi'
}
}
console.log(object - 1) // 通常先 valueOf,再得到 122
console.log(`${object}`) // 字符串 hint,通常先 toString
Symbol.toPrimitive 可以明确控制转换:
const price = {
value: 10,
[Symbol.toPrimitive](hint) {
if (hint === 'string') return `$${this.value}`
return this.value
}
}
console.log(price + 5) // 15
console.log(String(price)) // $10
如果转换方法都不返回原始值,会抛出 TypeError:
const invalid = {
valueOf: () => ({}),
toString: () => ({})
}
// invalid - 1 // TypeError
九、常见隐式类型转换
9.1 真值判断
以下值在 Boolean 上下文中是假值:
console.log(Boolean(false)) // false
console.log(Boolean(0)) // false
console.log(Boolean(-0)) // false
console.log(Boolean(NaN)) // false
console.log(Boolean('')) // false
console.log(Boolean(null)) // false
console.log(Boolean(undefined)) // false
console.log(Boolean(0n)) // false
空数组和空对象是真值:
console.log(Boolean([])) // true
console.log(Boolean({})) // true
浏览器中的 document.all 是历史兼容的特殊对象,具有非常规 typeof 和真值行为,不应推广到普通对象。

图片来源:原文配图。
9.2 数值运算和 +
减、乘、除通常会把操作数转换为 Number;BigInt 需要两侧都是 BigInt:
console.log(1 - true) // 0
console.log(1 - null) // 1
console.log(1 * undefined) // NaN
console.log(2 * ['5']) // 10
+ 会先把操作数转换为原始值;如果任一侧是字符串,最终进行字符串拼接,否则进行数值加法:
console.log(123 + '123') // '123123'
console.log(123 + null) // 123
console.log(123 + true) // 124
console.log(123 + {}) // '123[object Object]'
console.log('value: ' + [1, 2]) // 'value: 1,2'
9.3 == 和 ===
严格相等 === 不做类型转换;抽象相等 == 会按照规范执行一组转换规则。工程代码通常优先使用 ===,但理解 == 有助于分析旧代码。
NaN与任何值都不相等,包括自己;null == undefined为true,它们与其它普通值比较通常为false;- Boolean 会先转换为 Number;
- String 与 Number 比较时,字符串尝试转换为 Number;
- 对象会先通过
ToPrimitive转为原始值。
console.log(NaN == NaN) // false
console.log(true == 1) // true
console.log(true == '1') // true
console.log('' == 0) // true
console.log(null == undefined) // true
console.log(null == 0) // false
console.log(undefined == false) // false
[] == ![] 的过程可以拆开:
console.log(![]) // false,因为空数组是真值
console.log([] == false) // true
console.log([] == ![]) // true
右侧 false 转为 0,左侧空数组通过 ToPrimitive 得到空字符串,再转为 0,因此相等。数组中的 null 或 undefined 在数组转字符串时会形成空字符串:
console.log([null] == false) // true
console.log([undefined] == false) // true
console.log([1, 2].toString()) // '1,2'
这些规则可用于解释结果,但不建议用它们构建难以维护的业务判断。
9.4 一个会产生副作用的 == 示例
const a = {
values: [3, 2, 1],
valueOf() {
return this.values.pop()
}
}
console.log(a == 1) // true
console.log(a == 2) // true
console.log(a == 3) // true
这个例子依赖 valueOf() 的副作用和数组出栈顺序,说明了为什么不应把对象转换参与业务相等判断;它不是推荐技巧。
十、判断 JavaScript 数据类型
10.1 typeof
typeof 适合判断原始类型和函数:
console.log(typeof 'ConardLi') // string
console.log(typeof 123) // number
console.log(typeof true) // boolean
console.log(typeof 1n) // bigint
console.log(typeof Symbol()) // symbol
console.log(typeof undefined) // undefined
console.log(typeof function () {}) // function
局限包括:
console.log(typeof null) // object,历史兼容行为
console.log(typeof []) // object
console.log(typeof {}) // object
console.log(typeof new Date()) // object
对可能未声明的变量,typeof undeclaredName 不会抛错,但直接访问它会抛 ReferenceError。这只是兼容性特例,不应拿来代替模块导入或变量初始化检查。
10.2 instanceof
instanceof 检查构造函数的 prototype 是否位于对象原型链上:
console.log([] instanceof Array) // true
console.log(new Date() instanceof Date) // true
console.log([] instanceof Object) // true
它不适合作为所有类型的通用检测:
- 跨 iframe/Realm 的数组可能不是当前 Realm 的
Array实例; - 类可以自定义
Symbol.hasInstance; - 原型链可以被修改;
- 原始值不适合使用
instanceof。
数组使用 Array.isArray() 更可靠:
console.log(Array.isArray([])) // true
console.log(Array.isArray(new Array())) // true
10.3 Object.prototype.toString
const toString = Object.prototype.toString
console.log(toString.call([])) // [object Array]
console.log(toString.call(new Date())) // [object Date]
console.log(toString.call(/x/)) // [object RegExp]
console.log(toString.call(null)) // [object Null]
这比直接调用 value.toString() 更适合许多内置类型,但 Symbol.toStringTag 可以改变显示标签,代理和跨 Realm 仍需结合业务判断:
const value = {
get [Symbol.toStringTag]() {
return 'Array'
}
}
console.log(toString.call(value)) // [object Array],标签可以被伪造
console.log(Array.isArray(value)) // false

图片来源:原文配图。
10.4 推荐的类型判断策略
function isPlainObject(value) {
if (Object.prototype.toString.call(value) !== '[object Object]') {
return false
}
const prototype = Object.getPrototypeOf(value)
return prototype === Object.prototype || prototype === null
}
function getKind(value) {
if (value === null) return 'null'
if (Array.isArray(value)) return 'array'
if (typeof value === 'object') return 'object'
return typeof value
}
console.log(getKind(null)) // null
console.log(getKind([])) // array
console.log(getKind(new Map())) // object
jQuery 早期的 jQuery.type() 通过 Object.prototype.toString 和映射表兼容旧浏览器,这段代码有历史价值;现代项目通常使用 Array.isArray、typeof、instanceof、Object.getPrototypeOf 和领域校验组合判断,不必复制整套旧实现。
十一、总结
- 当前 JavaScript 有 8 种语言类型:7 种原始类型和 1 种对象类型;
- 原始值不可变,对象有身份,变量复制和函数参数都遵循按值传递;
- 栈/堆是实现和教学模型,不是 ECMAScript 对变量物理位置的承诺;
null、undefined、Symbol、BigInt 和 Number 都有明确的规范边界;- binary64 的精度问题来自二进制表示和舍入,
toString(2)不是原始位布局; ToPrimitive、==和+的结果来自规范化转换,不应靠直觉猜测;typeof、Array.isArray、instanceof和Object.prototype.toString各有适用范围;- 生产代码优先使用严格相等、明确的输入校验和领域类型,而不是依赖隐式转换。
参考资料: