string 和 String 到底有什么区别?
写 JS 代码的人,大概率都干过这两件事:
let a = "hello";
let b = new String("hello");
一个用小写 string,一个用大写 String,看起来好像差不多,打印出来也都是 hello。但如果你哪天不小心写出了 new String(...),代码可能会在一些很隐蔽的地方给你使绊子。今天就把这两者的关系彻底捋清楚。
先说结论:一个是”纸条”,一个是”盒子”
想象一下:
- 小写的
string是一张写着文字的纸条。纸条本身就是内容,你拿这张纸条给别人看,别人看到的就是上面写的字。两张写着同样内容的纸条,可以看作是”一样的”。 - 大写的
String是一个盒子,盒子里面装着一张写着文字的纸条。哪怕两个盒子里装的纸条内容一模一样,这也是两个不同的盒子,摆在桌子的不同位置。
这个比喻对应到技术上,就是 JS 里非常核心的一个概念:原始值(primitive) 和 对象(object) 的区别。
"hello" 这种写法创建的是原始值类型的字符串,而 new String("hello") 创建的是一个 String 对象,这个对象内部包了一层,才装着真正的字符串内容。
用代码验证一下
先看类型:
typeof "hello"; // "string"
typeof new String("hello"); // "object"
typeof 已经很诚实地告诉你了:一个是字符串类型,另一个压根就是对象类型,只是这个对象比较特殊,专门用来装字符串。
再看 instanceof:
"hello" instanceof String; // false
new String("hello") instanceof String; // true
纸条不是盒子,纸条永远不会通过”是不是盒子”这个检测;只有真正被装进盒子里的那个东西,才会通过。
疑问来了:既然是原始值,为什么还能调用方法?
这是很多人困惑的地方。原始值不是对象,按理说不应该有方法可以调用,但我们天天写 "hello".toUpperCase()、"hello".length,用得好好的,这是为什么?
答案是 JS 引擎在背后偷偷帮你做了一件事,叫自动装箱(autoboxing)。
当你对一个原始字符串调用方法或访问属性时,JS 引擎会:
- 临时把这张”纸条”塞进一个一次性的
String盒子里; - 调用盒子上的方法,拿到结果;
- 用完之后立刻把这个临时盒子扔掉。
整个过程快到你感觉不到,好像纸条自己就会做事一样。但本质上,方法是长在”盒子”这个对象上的,纸条只是被临时装了一下、借用了一下盒子的能力。
这也是为什么 String.prototype 上挂着一堆方法(slice、indexOf、replace 等等),而你用的却是原始字符串——你调用方法的那一瞬间,引擎已经悄悄帮你完成了”纸条进盒子”的临时转换。
坑一:相等比较会翻车
这是 new String() 最容易埋雷的地方:
"hello" === "hello"; // true
new String("hello") === new String("hello"); // false
new String("hello") === "hello"; // false
原因很直接:=== 对原始值比较的是内容,两张纸条只要写的字一样就算相等;但对对象比较的是引用,也就是是不是同一个盒子。
两个盒子哪怕装的纸条内容完全相同,它们也是摆在内存里不同位置的两个盒子,天生就不可能相等。
如果非要比较 String 对象的内容,得先把纸条从盒子里拿出来:
new String("hello").valueOf() === "hello"; // true
坑二:if 判断也会翻车
这个坑更隐蔽,杀伤力也更大:
if (new String("")) {
console.log("居然进来了?!");
}
按常理,空字符串 "" 在 JS 里是”假值”(falsy),if ("") 是不会执行的。但 new String("") 是一个对象,而所有对象在布尔判断里都是”真值”(truthy),哪怕这个对象装的是个空纸条,盒子本身依然被判定为”存在”,于是上面这段代码真的会打印出那句话。
这种坑一旦出现在真实项目里,排查起来会很让人头疼,因为表面上看变量的”内容”是空的,逻辑却走向了相反的分支。
那为什么 String 对象还存在?
既然处处是坑,为什么 JS 还留着 String 这个构造函数,不干脆去掉?
一方面是历史遗留:JS 早期的设计里,需要有一种方式把字符串包装成对象,才能挂载方法、实现前面说的自动装箱机制,String、Number、Boolean 这三个包装对象就是干这个用的。另一方面,String 也确实有它正经的用途,比如判断类型时用 instanceof,或者极少数需要给字符串”对象”额外挂自定义属性的场景。但这些场景在日常业务代码里非常罕见。
结论
日常写代码,记住一句话就够了:
永远用小写的 "字符串" 或 String("hello")(不带 new),不要用 new String("hello")。
String("hello") 不带 new 调用时,只是做类型转换,返回的还是原始字符串,跟 new String("hello") 完全是两码事,这点也值得单独留意。
小写 string 简单、直接、可预测,绝大多数场景下都是你真正想要的东西;大写 String 对象更多时候只会在相等比较和真假值判断上给你埋雷。
搞清楚”纸条”和”盒子”的区别之后,再看到这两者,就不会再傻傻分不清楚了。