先记住这个答案
String.prototype.valueOf 和 String.prototype.toString 在规范里的实现一模一样:检查 this 是原始字符串还是 String 包装对象,然后返回里面的原始字符串。所以对 new String('hi') 调用任意一个,拿到的都是原始值 'hi',而不是对象。两者真正的区别体现在隐式类型转换上:转成字符串的场景(模板字符串、String())先调 toString,默认转换的场景(+ 拼接、== 比较)先调 valueOf。
- 两个方法都返回内部的原始字符串,结果相同
- 包装字符串经
valueOf得到原始值 - 模板字符串与
+的转换提示不同 this不是字符串时两者都抛TypeError
包装对象和原始值的区别
'hi' 是原始值,new String('hi') 是一个对象,对象内部存着那个原始字符串。可以把包装对象想象成一个盒子,valueOf 和 toString 都是把盒子里的东西拿出来。
const prim = 'hi'
const wrapped = new String('hi')
// 两个方法返回的都是原始字符串
console.log(typeof wrapped, typeof wrapped.valueOf(), typeof wrapped.toString())
console.log(wrapped === 'hi', wrapped.valueOf() === 'hi')
// 原始值调用方法时,引擎临时包一层,调用完就丢掉
console.log(prim.valueOf() === prim.toString())
// 两者都要求 this 是字符串或 String 对象
try {
String.prototype.valueOf.call({})
} catch (err) {
console.log(err.name)
}
// 只改 toString,看两种转换各调了谁
wrapped.toString = () => 'custom'
console.log(`${wrapped}`, wrapped + '')查看输出与解释
object string string
false true
true
TypeError
custom hiwrapped === 'hi' 是 false,因为严格相等不做类型转换,一个是对象一个是字符串。取出原始值后再比较就是 true。
最后一行最关键。给 wrapped 单独覆盖了 toString,模板字符串走「转字符串」流程,先调 toString,输出 custom;+ 走默认转换流程,先调 valueOf,拿到的还是原来的 hi。
平时为什么感觉不到包装对象
写 'hi'.toUpperCase() 时,原始值本身没有方法,引擎会临时创建一个 String 包装对象,调用完方法就丢弃,这个过程叫自动装箱。所以原始字符串也能调用 valueOf 和 toString,结果就是它自己。
真正需要 valueOf 的场景很少,基本只有拿到了一个 new String() 创建的对象,想把它还原成原始值时。更常见的写法是直接 String(x),它对包装对象和原始值都能返回原始字符串。
容易答错的地方
toString返回字符串,valueOf返回对象本身- 这是把
Object.prototype.valueOf的行为套到了字符串上。普通对象的valueOf确实返回对象自身,但String.prototype.valueOf被重写过,返回的是原始字符串。 new String('a') == new String('a')是true- 是
false。两边都是对象,==比较的是引用,不会触发valueOf。只有一边是原始值时,==才会把对象转成原始值再比。 - 用
new String()创建字符串更规范 - 没有任何好处。包装对象
typeof是object,在条件判断里永远为真,new String('')也是真值,很容易引出 bug。直接写字面量就行。
面试官还会怎么问?
Number 和 Boolean 的 valueOf 也是这样吗?
是。Number.prototype.valueOf 返回内部的原始数字,Boolean.prototype.valueOf 返回内部的原始布尔值,思路完全一样。区别只是 toString 的输出格式:数字的 toString 还能传进制参数。
自定义对象想控制转字符串的结果,该重写哪个?
只关心字符串结果就重写 toString,模板字符串和 String() 会用到它。想同时控制 + 和数学运算,就实现 Symbol.toPrimitive,它接收 hint 参数,一个方法就能覆盖所有转换场景,优先级也最高。
怎么判断一个值是字符串对象还是原始字符串?
typeof 能区分:原始字符串是 string,包装对象是 object。如果两种都想识别为字符串,用 Object.prototype.toString.call(x) === '[object String]',两者都会返回这个标签。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。