从多种姿势了解JS装箱和拆箱📦

858 阅读5分钟

引入问题

本文已同步更新至公众号:想养猫的前端

今天对工作室的前端组实习成员进行了每周一次的突击考察,考察他们对JS基本功的掌握程度。我出了这样一道题:

const bool = new Boolean(false);
console.log(bool);

if (!bool) {
    console.log('ok');
} else {
    console.log('okk');
}

按照这样一个分析思路:第一行通过new调用JS的内置函数实例化了一个对象并赋给bool。所以第二行打印出来应该是一个对象。在条件语句中,由于bool是一个对象,所以!bool应该会隐式转换成false。所以第二个输出应该是okk

一切都看起来很顺利很合情合理,但是一细想,为什么呢?我们好像总是在背规则,但是很少去探究撑起这些规则的原理。

比如,看到下面这个代码我们会想到什么?

  const str = "Uni";
  
  console.log(str.length);

可能大多数同学想的都是控制台打印出这个字符串的长度,我们在执行一个获取字符串长度的操作。可以,我们有没有仔细想过,str.length这个操作呢?按理来说,str的类型是string是一个基本类型,其值是存在栈里面的,栈内存的空间是固定的。那为什么还能进行.length这种获取对象属性的操作呢?str并不是一个对象啊。这中间到底发生了什么?

再比如:

  const foo = function() {
    console.log(this);
  }
  
  const obj = 'Uni';
  
  foo.call(obj);

我们都知道可以使用

Function.prototype.call([thisArg[,arg1,...,argN]])

方法对this指向进行显示绑定,从而解决很多this指向不明的问题。但是如果我们把基本类型的数据传入参数thisArg这个位置呢?结果会是什么呢?这其中又会发生什么呢?

对于上述操作我们会有疑惑的原因在于,我们不够了解JS中的装箱拆箱

装箱

所谓装箱,简单来说就是将基本数据类型转换为对应的引用类型的操作。

而装箱又分为两种:隐式装箱显示装箱

显示装箱

显示装箱非常简单,就是通过内置对象或者说基本包装类型对基本数据类型进行操作。

比如:

  const name = new String("Uni");

这时候我们的name是一个对象,能够调用相应的方法或者原型链上的方法。比如:

  const name = new String("Uni");
  String.prototype.age = "20";
  
  console.log(name.age);

我们也可以打印一下这个对象看看:

  const name = new String("Uni");
  console.log(name);

我们可以看到浏览器控制台打印结果是这样:

其中length是一个不可枚举属性。

隐式装箱

刚刚我们看了显示装箱的例子,知道lengthString类型实例化后不可枚举的属性(当然String自身也有length属性)。那我们普通字面量形式声明的字符串,为什么也能调用length属性呢?

对于这段代码:

  const name = "Uni";
  const len = name.length;

在JS引擎中其实是这样的:

  const name = "Uni";
  let newName = new Object(name);
  const len = newName.length;
  newName = null;

只不过上述的一切都是在访问name.length的一瞬间完成的,访问结束便立马销毁为获取length而生成的实例。

总结一下隐式装箱的步骤便是:

  • 创建一个对应类型的实例
  • 在实例中调用需要的方法或属性
  • 销毁这个实例

这样就能够解释我们平时为什么通过[]形式就能够访问字符串的某一位,通过.length便能够获取字符串字面量的长度。仔细再来看看刚刚显示装箱那个实例的图细品一下:

由于经常打算法,获取某个字符串的长度这种场景经常发生。我突然就想,我们每次访问某个字符串字面量的length属性时,都会这样隐式装箱,会不会非常耗费性能?比如:

  const str = "abc";
  
  for (let i = 0; i < str.length; ++i) {
    console.log(str[i]);
  }

每次去判断i的时候都要隐式装箱获取str的长度。那我是不是先显示装箱讲长度取出来,存好就能够降低性能了?比如这样:

  const str = "abc";
  const len = new String(str);
  for (let i = 0; i < len; ++i) {
    console.log(str[i]);
  }

为了验证我的想法,上网搜集资料后发现,我这样做反而会降低性能!原来是浏览器对于这些常用的一些隐式装箱有着一定的预先处理,为的就是减少性能损耗。所以我们这样操作反而会聪明反被聪明误,会降低JS的执行效率...

拆箱

拆箱,就是装箱的反向操作,指的是将引用类型转换为对应的基本类型。常用的就是引用类型的valueOftoString两个方法。

就比如刚刚我在文章开头引入问题的这个部分提出的一个小问题:

const bool = new Boolean(false);
console.log(bool);

if (!bool) {
    console.log('ok');
} else {
    console.log('okk');
}

我们要怎么通过拆箱操作便能够让控制台打印出ok呢?看看吧:

const bool = new Boolean(false);
console.log(bool.valueOf());    // false

if (!bool.valueOf()) {
    console.log('ok');        // ok
} else {
    console.log('okk');
}