JavaScript的装箱拆箱机制?:自动类型转换

“Programming is like sex: one mistake and you’re providing support for a lifetime.” —Michael Sinz

介绍

最近在通过看红宝书《javascript 高级教程》学习js, 由于自己有过编写Java程序的基础,所以在学习JS过程中潜意识地会与Java语言中的相关特性进行对比。在学习javascript基本的数据类型时,书中大致有这么一段内容:

1
2
3
> var str1 = "some text";
> var str2 = str1.substring(3);
>

字符串当然是基本类型基本类型值,然而这里却调用它substring方法,从逻辑上将它不应该有方法,其实,为了让我们实现这种直观的操作,后台已经自动完成了一系列的处理:
(1) 创建String类型的一个实例;
(2) 在实例上调用指定的方法;
(3) 销毁这个实例
经过这种处理,基本的字符串类型就变得跟对象一样了。而且上面的三个步骤也适用于Boolean和Number类型对应的布尔值和数字值。

看到这里,我想说这不就是Java中的自动装箱吗?

什么叫自动装箱

Java中,自动装箱(autoboxing)是指将原始值转换为相应包装器类的对象称为自动装箱。例如,将int转换为Integer类。以下情况中Java编译器会应用自动装箱:

  • 将原始值作为参数传递给一个期望接收该原始值对应包装类的对象的方法;
  • 将原始值赋值给一个类型为其对应的包装类对象的变量;
    当然自动拆箱就是装箱的逆过程,主要发生在:
  • 将包装类对象作为参数传递给一个期望接收对应原始值的方法时;
  • 将包装类对象赋值给一个类型为其对应原始值的变量;

JavaScript的自动拆装箱机制

其实JavaScript语言规范中,并没有提及自动拆装箱的概念,不过MDN中倒是有提到过类似概念:

function.apply(thisArg, [argArry])
thisArg: The value of this provided for the call to func. Note that this may not be the actual value seen by the method: if the method is a function in non-strict mode code, null and undefined will be replaced with the global object, and primitive values will be boxed. This argument is not optional

与Java这种完全面向对象的静态语言截然不同,JavaScript是一门动态的语言,它甚至都没有类概念(即使ES6带来class,但是它也只不过语法糖),这使得它具有很大的灵活性,当然也带来了很多让人很难理解掌握的特性,比如=====;所以对于Java中的自动拆装箱机制,在JS中其实可以理解为大家熟知的类型转换中的一种特殊情况。

JavaScript中的类型转换

类型转换发生在静态类型语言的编译阶段,而强制类型转换则发生在动态类型语言的运行时(runtime)。然而在 JavaScript 中通常将它们统称为强制类型转换,我个人则倾向于用“隐式强制类型转换”(implicit coercion)和“显式强制类型转换”(explicit coercion)来区分。
—— 引用自《你不知道的JS 上》

JavaScript是一种动态类型语言,对变量不会像静态语言那样做类型检查,可以随时赋予任意值,但是JS中各种运算符或表达式对数据类型是有要求的。如果运算符发现,运算子的类型与预期不符,就会自动转换类型。例如最常见的,if语句需要一个布尔值,因此在括号中定义的任何内容都将转换为布尔值,while也是如此。

Javascript类型转换中有很多坑,就连Douglas Crockford在《Javascript: The Good Parts》一书中也极力 ‘吐槽’ 。

类型转换也分为显示类型转换和隐式类型转换,

隐式类型转换

当编译器在没有程序员干预的情况下自动执行类型转换,称为隐式类型转换。

上述的if语句中默认将值转换成布尔类型就属于隐式类型转换,10 + '10' == 20 也属于隐式类型转换;
JS中的隐式类型转换其实理解为Java中的自动拆装箱。我们再来看上文介绍中的例子:

1
2
3
> var str1 = "some text";
> var str2 = str1.substring(3);
>

这里我们可以看出,str1在调用subString方法时,JS引擎自动将String类型str1变量通过new String()的方式隐式转换成了字符串对象,所以此时他才能访问subString属性方法,这个过程就对应着自动装箱;那么对应的,在执行完subString方法后,我们继续访问str1变量,那它的值依然会是”some text”,这又是问什么呢?按理说自动装箱后str1不应该是一个字符串对象吗?原来在执行完方法后,JS解释器又将str1变量通过str1.toString()(这里也可以通过str1.valueOf())隐式转换成了String类型,这个过程即为自动拆箱。

JS中的“自动装箱”机制其实就是将原始值类型隐式转换成引用类型的过程,这种情况主要发生在两种情况下:

  1. 在非严格模式下,传递一个原始值类型作为function.call/applythis参数时
  2. 原始值类型访问属性时,例如: "paopao lee".split(' ')

在第一种情况中,非严格模式下,当一个函数执行时,如果this值不是一个对象,那么它将其自动转换为对象,进一步了解

The following steps are performed when control enters the 》》 > execution context for function code contained in function object F, a caller provided thisArg, and a caller provided argumentsList:

  1. if the function code is strict code, set the ThisBinding to thisArg.
  2. else if thisArg is null or undefined, set the ThisBinding to the global object.
  3. else if Type(thisArg) is not Object, set the ThisBinding to ToObject(thisArg).

从上面可以看到,第3步中,原始值类型会被使用ToObject(thisArg)转换为对象。

第二种情况中,当你通过[].访问属性时会发生类似的事情。这里引用的部分解释了JS如何计算表达式foo[bar]

The production MemberExpression: MemberExpression[Expression] is evaluated as follows:

  1. Let baseReference be the result of evaluating MemberExpression.
  2. Let baseValue be GetValue(baseReference).
  3. Return a value of type Reference whose base value is > baseValue and whose referenced name is propertyNameString, and whose strict mode flag is strict.

最重要的最后一步,无论MemberExpression计算什么,它都转换为Reference类型的值。这是一种仅在规范中使用的数据类型,并且包含关于如何从引用检索实际值的附加信息(不要与实际JavaScript代码中的对象引用混淆!)
为了从Reference中获得“真实的”值或结果,会调用内部函数GetValue(V)(就像上面的步骤2):

The following [[Get]] internal method is used by GetValue when V is a property reference with a primitive base value. It is called using base as its this value and with property P as its argument. The following steps are taken:

  1. Let O be ToObject(base).

举个🌰:

1
var foo = "BAR`.toLowerCase();

这是一个赋值表达式,其计算方法如下:

The production AssignmentExpression : LeftHandSideExpression = AssignmentExpression is evaluated as follows:

  1. Let lref be the result of evaluating LeftHandSideExpression.
  2. Let rref be the result of evaluating AssignmentExpression.
  3. Let rval be GetValue(rref).

第1步: 左边求值,也就是标识符’foo’。标识符的解析方式并不重要
第2步: 对右边求值,即"BAR".toLowerCase(), 该计算的内部结果将是一个Reference并存储在rref中,类似于:

1
2
3
4
5
REFERENCE = {
base: "BAR",
propertyNameString: "toLowerCase",
strict: false
}

第3步: GetValue(rref)被调用,由于rref的引用值(REFERENCE)的base属性值是字符串”BAR”,是原始类,所以会调用ToObject将它暂时转换为String对象,此外,REFERENCE实际上代表了一个属性访问,因此GetValue(rref)最终会调用String对象上的方法toLowerCase并返回方法的结果。

显式类型转换

程序员强制执行的类型转换称为显式类型转换。基本上,程序员强制表达式为特定类型。 Explict类型转换也称为类型转换。

显示转换其实很好理解

1
2
3
4
5
6
7
8
9
10
// 字符串转换
var a = 42;
var b = String(a);
// 数字转换
var c = '3.14';
var d = Number(c);
// 布尔值转换
var e = [];
var f = Boolean(e)
var f = !![];

参考链接