JS 中装箱和拆箱
概览
我们可以在基本类型值(比如字符串 "Hello")上访问属性 & 调用方法,但基本类型值并不支持属性和方法设置。能这样做的原因,是因为引擎内部做了装箱与拆箱操作。
JavaScript 中值的类型有两种:基本类性值(primitives)和复杂类性值(即对象,objects)。
两种类性值本质上不同的地方:
- 基本类型: 存储的是一个简单值。
- 对象:则使用引用访问,我们通过引用里记录的地址,找到对象数据在内存中实际存储的位置,并进行操作。
调用方法和返回属性是对象上所特有的访问数据的方式。
let num = new Number(1234.236);
// 访问属性
typeof num.toLocaleString; // "function"
// 调用方法
num.toLocaleString(); // "1,234.236"
但会发现,直接在 1234.236 上进行同样是 OK 的。
原始类性值居然也有属性和方法?其实 JS 引擎背地里做了操作:装箱和拆箱。
装箱和拆箱
装箱就是把原始类型值转为对应的包装对象,比如:
let num = 1234.236;
// 数值包装成 Number 对象
new Number(num);
let str = "Hello";
// 字符串包装成 String 对象
new String(str);
num 转为 new Number(num),str 转为 new String(str) 等。当然,这个过程对我们来说是不可见的。也正是因为 JS 内部做了如此的转换,我们才得以能够“看起来可以直接在原始类型值访问属性和调用方法”。
装箱使用完毕后,还有一个拆箱的过程。所谓拆箱,就是把包装对象转为对应的原始类型值表现形式。
// 将 new Number 拆箱成 1234.236
new Number(num).valueOf(); // 1234.236
// 将 new String 拆箱成 Hello
new String(num).valueOf();
也就是说下面代码的执行过程:
做了类似于如下的处理:
// 装箱:转换成对应的包装对象
num = new Number(1234.236);
// 使用
typeof num.toLocaleString; // "function"
num.toLocaleString(); // "1,234.236"
// 拆箱
num = num.valueOf();
好处
之所以这么做,肯定是因为这种处理方式利大于弊,可以简单总结为:
- 方便。操作基本类性值的场景还是比较多的。如果每次为了使用属性或者调用方法之前都要包装一次,未免太过麻烦。
- 省内存。大家知道存储同一个数据(比如 new Number(1) 和 1 ),对象对内存的开销比基本类型值要大。有了拆装箱的操作,我们只在使用的时候暂时包装成对象访问,其余时间还是以基本类型值的形式存在,能够节省不少的内存。
- 另外,我们其实不用担心这种方式的开销问题。因为 JS 自打出生就支持这个拆装箱特性了,内部也对这类操作做了优化,所以性能问题几乎可以忽略。
null/undefined 没有对应的包装函数
根据最新的语言标准,这 7 种语言类型是:1. Undefined;2. Null;3. Boolean;4. String;5. Number;6. Symbol;7. Object。
除了 null 和 undefined 之外,其他基本类型都要对应的包装函数。因此在 null 和 undefined 上访问属性是会报错的。
undefined 表示变量未初始化,null 表示变量为空。两者都没有什么实际的数据意义,因此没有对应的包装函数。
alert(null.test); // error