声明一个数字,有三种写法,看起来差不多,行为却差很多:
let a = 1 // 字面量,原始值
let b = Number(1) // 转成原始值
let c = new Number(1) // 造一个对象
typeof a // "number"
typeof b // "number"
typeof c // "object" ← 已经不是一个数字了
a === 1 // true
b === 1 // true
c === 1 // false ← 对象和原始值永远不严格相等
很多人第一次看到 c === 1 为 false 都会愣一下:明明打印出来是 1,怎么就不相等了?
这篇文章就把这三种写法彻底拆开:字面量、Number() 函数、new Number() 构造函数,各自产出什么、能调什么方法、什么时候会踩坑,最后给一张选型表。
一、三者到底各自产出了什么
先记住一句话:
- 字面量
1:产出原始值(primitive),类型是"number"。 Number(1):是一个普通函数调用(注意前面没有new),做类型转换,产出原始值。new Number(1):用构造函数造一个包装对象(wrapper object),类型是"object"。
let a = 1
let b = Number(1)
let c = new Number(1)
typeof a // "number"
typeof b // "number"
typeof c // "object"
c instanceof Number // true,它是 Number 的实例
a instanceof Number // false,原始值不是实例
new 是关键分水岭。带 new 的 Number 走的是构造调用,返回一个对象;不带 new 的 Number 走普通调用,返回原始值。同一个标识符,两种身份。
二、三个最直接的差异
2.1 类型:typeof 就不同
typeof 1 // "number"
typeof Number(1) // "number"
typeof new Number(1) // "object"
所以任何用 typeof x === "number" 做判断的地方,new Number(1) 都会漏掉。这是包装对象最典型的坑。
2.2 严格相等:对象和原始值永远不相等
1 === 1 // true
Number(1) === 1 // true
new Number(1) === 1 // false,object vs number
new Number(1) === new Number(1) // false,两个不同的对象引用
但宽松相等会隐式拆箱,结果又变成 true:
new Number(1) == 1 // true,比较前会调用 valueOf() 取出原始值
这就是 == 的坑之一:对象先转原始值再比。想彻底搞清 == 和 ===,可以看另一篇讲相等运算符的文章。
2.3 真假值:对象恒为真
new Number(0) ? '真' : '假' // "真",对象永远是 truthy
Number(0) ? '真' : '假' // "假"
0 ? '真' : '假' // "假"
new Number(0) 在 if 里是真。因为布尔转换只看"是不是对象",不看里面包了什么值。这也是包装对象最容易埋雷的地方。
三、Number() 的本来职责:类型转换
Number() 最常见的用法根本不是"声明数字",而是把别的类型转成数字:
Number('42') // 42
Number(' 42 ') // 42,会修剪首尾空白
Number('') // 0 ← 空字符串转成 0
Number('42px') // NaN ← 只要有一个字符不是数字就是 NaN
Number(true) // 1
Number(false) // 0
Number(null) // 0
Number(undefined) // NaN
Number([]) // 0,[] → '' → 0
Number([5]) // 5,[5] → '5' → 5
Number([1, 2]) // NaN,[1,2] → '1,2' → NaN
记住两条规则就够推:
Number(x)内部先对x做ToPrimitive,把对象(含数组)转成原始值。- 再把原始值转数字:
''→0,其他不能解析的 →NaN。
如果只想要整数、还允许遇到非数字字符就停下,用 parseInt:
Number('42px') // NaN
parseInt('42px') // 42,遇到 p 停止解析
parseInt('ff', 16) // 255,指定进制
日常最省事的转换其实是一元加号 +x,它和 Number(x) 完全等价:
+'42' // 42
+true // 1
+[] // 0
四、new Number() 到底造了个什么
new Number(1) 造出来的对象,内部有一个不可直接访问的内部槽 [[NumberData]] 保存着原始值 1。它就是靠这个槽把原始值"包"起来的。
因为它继承自 Number.prototype,所以能调用 Number 的方法:
let c = new Number(1.2345)
c.toFixed(2) // "1.23"
c.toExponential() // "1.2345e+0"
c.toPrecision(3) // "1.23"
c.toString() // "1.2345"
c.valueOf() // 1.2345,取回原始值
valueOf() 就是取出那个内部槽里的原始值,这也是前面 new Number(1) == 1 能成立的原因——比较时隐式调用了它。
五、关键:原始值也能调这些方法
看到上一节你可能会想:"那包装对象至少多个优点,能调 toFixed 吧?"
并不是。原始值一样能调:
(1.2345).toFixed(2) // "1.23"
1.2345.toFixed(2) // "1.23"
原理是自动装箱(auto-boxing):访问 1.2345.toFixed 时,引擎临时把原始值 1 包成一个 Number 对象,调用方法,用完立刻丢弃。所以你写 1..toFixed(2) 时得到的临时对象,和 new Number(1) 是同一种东西,区别只是后者被你留下来了。
换句话说,new Number() 能做的事,Number() 和字面量都能做。它没有提供任何额外能力。
那它唯一的"本事"是什么
给对象挂自定义属性——这一点原始值做不到:
let n = 1
n.foo = 1 // 静默失败(非严格模式),严格模式抛 TypeError
console.log(n.foo) // undefined
let c = new Number(1)
c.foo = 1 // ✅ 可以
console.log(c.foo) // 1
原理还是自动装箱:n.foo = 1 时临时包了个对象、挂上属性,然后对象立刻被丢弃,所以下一个 n.foo 又是新的临时对象,读不到。而 new Number(1) 是这个对象被真正保留了下来。
但你几乎不会需要一个"能挂属性的数字对象"。所以这点能力在实际开发里也没价值。
六、为什么会有 new Number()
答案不是为了解决什么问题,而是语言设计的历史一致性。
早期 JS 模仿 Java 的"万物皆对象",给每个原始类型都配了对应的包装对象构造函数:Number、String、Boolean。它们对应的写法就是 new Number()、new String()、new Boolean()。
但这套设计从第一天起就弊大于利,只留下了三个坑:
typeof会变成"object",所有类型判断失效。new Number(0)在条件里是true,真假判断全乱。===永远比不过原始值。
所以现代 JS 里,包装对象构造函数一律不要用 new 调用。需要转换就用 Number(x) 或 +x。
七、一张表选对写法
| 写法 | 结果类型 | typeof | 典型用途 |
|---|---|---|---|
1(字面量) | 原始值 | "number" | 直接写数字,99% 的场景 |
Number(x) | 原始值 | "number" | 显式类型转换 |
+x | 原始值 | "number" | 同上,更简洁 |
parseInt(x) / parseFloat(x) | 原始值 | "number" | 解析可带非数字字符的字符串 |
new Number(x) | 包装对象 | "object" | ❌ 基本没有正当用途 |
小结
- 字面量
1和Number(1)都产出原始值,类型是"number";new Number(1)产出对象,类型是"object"。 new Number(1) === 1为false;typeof是"object";new Number(0)在if里为true——这三个坑都来自"它是对象"。Number(x)的真正身份是类型转换工具:Number('')得0,Number('42px')得NaN;解析带杂字符的字符串用parseInt。- 包装对象能调的
toFixed、valueOf等方法,原始值靠自动装箱一样能调,所以new Number()没有额外能力。 - 需要转换就用
Number(x)或+x,需要原始值就写字面量。new Number()永远不要用。