先记住这个答案
除 null 和 undefined 外,每种原始类型都有对应的包装对象:String、Number、Boolean、Symbol、BigInt。当你写 'abc'.toUpperCase() 时,字符串本身没有这个方法,引擎会自动装箱:临时创建一个 String 包装对象,在其原型上找到方法并以该对象为 this 执行调用,随后把临时对象丢弃。所以你看到的方法能力来自包装对象的原型,而不是原始值本身。由于装箱只发生在属性访问的瞬间,给原始值赋属性不会报错但也不会保存。平时不需要也不应手动 new 这些包装器,因为它们与原始值类型不同且行为有差异。
- 方法来自包装对象原型,非原始值本身
- 属性访问时自动装箱,用完即丢弃
- 不要 new String/Number,类型和相等性会变
自动装箱:方法从哪里来、包装对象去哪里
当你对原始值做属性访问,例如 'hello'.length 或 (3.14).toFixed(2),引擎在内部执行一次装箱:以该原始值构造一个对应类型的临时包装对象,然后在这个对象及其原型链上查找属性或方法。查到的 length 是 String 包装对象自身的数据属性,toFixed 是 Number.prototype 上的方法。整个查找发生在包装对象身上,原始字符串和原始数字本身从不携带任何属性。
装箱是瞬时的。表达式求值一结束,这个临时对象就不再被引用,随后被垃圾回收。这解释了为什么 'abc'.foo = 1 静默无效:赋值发生在临时对象上,对象随即消失,下一次访问 'abc'.foo 又装箱出一个全新对象,自然读不到上次的值。严格模式下对原始值赋属性会直接抛 TypeError,把这种隐藏错误显式化。
对原始值挂标记位却读不到:一次缓存场景的排查
一个文本处理函数想给清洗过的字符串挂缓存标记:normalize(str) 内部执行 str._cached = true 后返回 str,调用方下次判断 if (input._cached) 跳过重算。测试发现标记永远为 undefined,缓存从不命中。原因正是装箱:赋值写入的是临时 String 对象,函数返回的仍是原始字符串,标记随临时对象一起被丢弃。
修复方式是改变设计而不是依赖包装对象:用 Map 以原始字符串为键记录缓存状态,或返回一个显式的对象结构 { text, cached }。结果缓存正常命中。这个例子也说明可操作判断:只要发现自己想给字符串或数字附加状态,就应该意识到原始值不可扩展,需要换一个能持有状态的容器,而不是尝试 new String 让赋值生效。
显式包装对象的陷阱与 null/undefined 的例外
手动 new String('a')、new Number(1) 会得到 typeof 为 'object' 的值,===' 与原始值比较为假,new Boolean(false) 在 if 条件中反而是真值,因为对象总是真值。这些差异会让类型判断和条件逻辑出错,所以实践中几乎从不需要显式构造包装对象;调用 String(x)、Number(x) 不带 new 时执行的是类型转换,返回原始值,这才是正确用途。
另一类边界是 null 和 undefined:它们没有对应包装器,任何属性访问都直接抛 TypeError,不会触发装箱。可选链 ?. 正是为这类值设计的短路机制。判断时记住:'abc'?.length 能工作是因为先装箱再读取,null?.length 能工作是因为 ?. 提前终止,两者机制完全不同,不能混为一谈。
容易答错的地方
- 认为原始值本身就是对象
- 错误。typeof 'abc' 是 'string',方法能力是访问瞬间装箱后从 String.prototype 借来的。可以用 'abc'.constructor === String 验证联系,但原始值本身不持有方法。
- 认为给字符串赋值属性可以保存状态
- 错误。赋值落在临时包装对象上,对象随后被丢弃,再读是 undefined。严格模式下这种赋值直接抛 TypeError,说明语言本身也把它视为错误操作。
面试官还会怎么问?
为什么 'abc'.length 可以读,而 null.length 直接报错?
字符串访问属性时触发装箱,length 是临时 String 包装对象自身的属性;null 和 undefined 没有包装对象,属性访问没有装箱路径可走,直接抛 TypeError,这就是引入可选链的原因。
new Number(1) 和 Number(1) 有什么区别?
带 new 创建包装对象,typeof 为 'object',与原始值 === 比较为假;不带 new 是类型转换函数,返回原始 number。日常需要转换时只用后者,几乎不应使用前者。
频繁调用字符串方法会因为装箱产生性能问题吗?
规范描述上会创建临时对象,但现代引擎对常见的原始值方法调用做了优化,多数情况不会真正分配对象。除非在极端热点循环中,否则不应为此改变写法。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。