先记住这个答案
字符串、数字、布尔这些原始值本身不是对象,身上没有任何属性。当你写 'hello'.toUpperCase() 时,引擎发现要在原始值上访问属性,就先用对应的构造函数把它临时包成一个对象,也就是 String 包装对象,然后从这个对象的原型 String.prototype 上找到 toUpperCase 调用,最后把临时对象丢掉。这就是自动装箱。所以原始值「看起来」有方法,实际上方法都借自包装类型的原型,而且每次访问都是一个新的临时对象。
- 访问原始值的属性时,会临时包装成对象
- 方法在
String.prototype上,不在值本身 - 给原始值加属性不会报错,但下一行就读不到了
- 别用
new String创建包装对象
装箱的过程和它留下的痕迹
临时对象用完就扔,这一点可以从「给字符串加属性」的结果看出来。
const s = 'hello'
console.log(s.toUpperCase(), s.length) // 临时包装成 String 对象再调用
s.tag = 'x' // 属性加在了临时对象上,随后被丢弃
console.log(s.tag) // undefined
// 方法并不在 s 身上,而是在 String.prototype 上
console.log(s.toUpperCase === String.prototype.toUpperCase)
console.log(typeof s, typeof Object(s))查看输出与解释
HELLO 5
undefined
true
string objects.tag = 'x' 这一行,引擎创建了一个临时的 String 对象,把 tag 挂在它上面,然后这个对象就没人引用了。下一行读 s.tag 时又创建了一个新的临时对象,上面当然没有 tag。这段代码在非严格模式下静默失败;在严格模式下,给原始值设置属性会直接抛 TypeError,更容易发现问题。
Object(s) 会显式地做一次装箱,得到的是真正的对象,这也说明 typeof 的结果取决于你手里拿的是原始值还是包装对象。
- 引擎真的每次都创建对象吗
- 规范是这样描述行为的,但 V8 这类引擎会做优化,调用
'abc'.length时并不会真的分配一个对象。所以不用担心在循环里调用字符串方法有额外开销,这里讲的是语义,不是实现。
手动创建包装对象的坑
String、Number、Boolean 也可以用 new 调用,得到一个长期存在的包装对象。它和原始值在很多地方表现不同,实际项目里几乎没有理由这样写。
new String 和原始值对比JavaScriptconst a = new String('hi')
const b = 'hi'
console.log(typeof a, typeof b) // object string
console.log(a == b, a === b) // 宽松相等会拆箱,严格相等不会
console.log(new String('hi') === new String('hi')) // 两个不同对象
const flag = new Boolean(false)
if (flag) console.log('对象永远是真值')
console.log(flag.valueOf()) // 拆箱拿回原始值查看输出与解释
object string
true false
false
对象永远是真值
false最危险的是最后一段。new Boolean(false) 是一个对象,所有对象在条件判断里都是真值,所以 if 会进去,逻辑完全反了。想把值转成布尔,写 Boolean(x) 或 !!x,不带 new。
Symbol和BigInt不能new- ES6 之后新增的两个原始类型干脆禁止了
new,new Symbol()和new BigInt(1)都会抛TypeError,就是为了不让大家再踩上面这些坑。它们依然能自动装箱,所以Symbol('a').description、10n.toString()都能正常用。
容易答错的地方
- 说「字符串是对象,所以有方法」
- 字符串是原始值,
typeof返回string。它能调用方法靠的是临时装箱,不是因为它本身是对象。这个区别解释了为什么字符串不可变,也解释了为什么给它加属性会丢。 - 以为
null和undefined也会装箱 - 这两个值没有对应的构造函数和原型,访问它们的任何属性都会抛
TypeError,比如常见的Cannot read properties of undefined。所以接口数据不确定时要写data?.name,而不是指望引擎兜底。 - 用
instanceof判断字符串 'hi' instanceof String是false,因为左边是原始值,不是对象。判断原始类型用typeof,只有new String创建的包装对象才会让instanceof返回true。
面试官还会怎么问?
给 String.prototype 加方法,所有字符串都能用吗?
能。装箱出来的临时对象原型就是 String.prototype,加上去的方法对所有字符串都生效。但在业务代码里修改内置原型会和第三方库、未来的标准方法冲突,历史上 MooTools 就因为这个导致标准方法被迫改名,所以要写成独立的工具函数。
(1).toString() 为什么要加括号?
1.toString() 会报语法错误,因为词法分析器把 1. 当成了一个带小数点的数字,后面紧跟 toString 就不合法了。加括号、写成 1..toString() 或 1 .toString() 都可以。数字能调方法的原理和字符串一样,是装箱成了 Number 对象。
方法里的 this 是原始值还是包装对象?
取决于是否严格模式。严格模式下,在原始值上调用方法时 this 就是原始值本身;非严格模式下 this 是包装对象。内置方法的行为按规范处理,不受影响,但自己往原型上加的方法里判断 typeof this 时会看到差别。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。