你真的掌握了 JavaScript 变量和类型嘛?(下)

619 阅读9分钟

这是深入理解 JavaScript 语法合集的第三篇,其余文章见文末。

前言

前面一篇内容 我们讲解了基本数据类型,接着讨论了它们是如何进行存储的。在此同时,我们就 原始数据类型对象类型 的区别进行了分析。那么这一篇我们继续讨论,这些数据类型之间是如何进行转换的?又还有哪些其它对象类型?JavaScript 中的内置对象有哪些?装箱和拆箱又是如何触发的?我们又该怎么判断变量的数据类型呢?

JavaScript 的数据类型

一、JavaScript 类型转换

在介绍 JavaScript 类型转换之前,我们先来介绍一些前置知识吧。

(1)还有哪些其它对象类型?

前一篇 文章中讲到,最新的 ECMAScript 中定义了八种数据类型。总的来说可以分为两种类型,一种是原始数据类型,另一种是对象类型,即 Object。但实际上,Object 类型是对象类型的统称。它不仅仅包含 Object 类型,它还包含一些特殊的对象类型:ArrayDateRegExpFunction等类型。

虽然这些特殊类型生成的对象并不是由 Object 直接构造的,但它们的原型链终点都是 Object

(2)什么是基本包装类型?

基本包装类型也是一种特殊的对象类型。为了便于原始数据类型的操作,基本包装类型为原始数据类型提供了一套转换机制。基本包装类型和其它对象类型的区别在于生存期不同,前者在代码执行后就会销毁实例。

基本包装类型包括:StringNumberBoolean

我们来看看基本包装类型的生存周期:

let str = "car";
str.name = "HongQi";
console.log(str.name); // undefined

出乎意料的是,最后输出的值是 undefined。我们把目光放到第二行,我们为该变量定义了一个属性 name,设置为 HongQi。我们知道,设置属性是对象的事情,原始数据类型是没有的。这说明,当我们为其设置属性时,String 类型(原始数据类型中的 String)的变量被自动 装箱 成了包装类型的对象,而由于其在代码执行后被销毁了,所以我们在第三行访问变量时其值变为 undefined

注:NumberBooleanString 三种类型也可以通过 new 关键字创建对应的基本包装类型实例。

(3)装箱、拆箱

刚刚稍微提到了,装箱 就是将原始数据类型转换成包装类型。而拆箱则相反,将包装类型反过来转换成原始数据类型。

先考虑一下,为什么都有了原始数据类型了还要有基本包装类型呢?

我们知道,原始数据类型本身并不能扩展属性和方法。这时倘若我们需要操作原始数据类型的变量,那我们就不能使用那些常用方法。这将让操作变的非常不方便。

1. 装箱:

假如现在我们要操作一个字符串,希望拿到它的其中一部分。我们可能会这样操作:

let str = "MyBooks";
let partOfStr = str.substring(2);

console.log(partOfStr); // Books

第二行原始类型 str 调用了 substring() 方法,然后将操作后的值给了 partOfStr。这实际上发生了几个过程:

  1. 创建一个 String 的包装类型实例
  2. 在实例上调用 substring 方法
  3. 返回操作后的值同时销毁实例

也就是说,当我们使用原始数据类型调用方法时,就会自动进行装箱操作。类似的,我们对原始数据类型 NumberBoolean 的变量使用相应的方法也会自动的进行对应的装箱操作。

2. 拆箱:

从对象类型到原始数据类型的转换就是拆箱过程。在拆箱的过程中,会遵循 ECMAScript 规范规定的 toPrimitive 原则:ToPrimitive(input[, PreferredType])

该原则将输入的 input 从对象类型转换为原始数据类型(non-Object type)。PreferredType 是可选参数,可以是 NumberString 类型, 只是一个转换标志。转化后的结果并不一定是这个参数所设置的类型。但是它的转换结果一定是一个原始值(或者报错), 也就是说设置 Number 时也可能转换为 String, 设置为 String 时 也可能转化为 Number

根据转换算法,我们可以得出:

  • PreferredType 未设置时,则
    1. inputDate 类型时,PreferredType = String;
    2. 否则,PreferredType = Number;
  • PreferredType = Number
    1. input 是原始值,返回原始值;
    2. input 不是原始值,调用 input.valueOf();如果返回值是原始值,则返回原始值结果;
    3. 若步骤 2 的结果不是原始值,调用 input.toString()。如果返回值是原始值,则返回原始值结果;
    4. 如果 3 的结果不是原始值,抛出 TypeError 异常。
  • PreferredType = String
    1. input 是原始值,返回原始值;
    2. input 不是原始值,调用 input.toString();如果返回值是原始值,则返回原始值结果。
    3. 如果 2 的结果不是原始值,调用 input.valueOf(),如果返回值是原始值,则返回原始值结果;
    4. 如果 3 的结果不是原始值,抛出 TypeError 异常。

总结就是:

  1. PreferredType 未被设置时,若 inputDate ,则将 PreferredType 设置为 String。否则设置为 Number
  2. PreferredTypeNumber 时,先调用 inputvalueOf,若结果为原始值则返回;否则调用 toString,若结果为原始值则返回;否则返回 TypeError
  3. PreferredTypeString 时,先调用 toString ,若结果为原始值则返回;否则调用 valueOf ,若结果为原始值则返回;否则返回 TypeError

3. valueOf() 方法和 toString() 方法

这两个方法是 Object.prototype 的方法,也就是所有的对象都有这两个方法。

  • valueOf():原则是,能转化为原始值就转化为原始值,不能转化为原始值的返回 this,也就是对象本身;Date 对象转化为毫秒级数值
Number('123').valueOf() // 123
String('123fs').valueOf() // '123fs'
Boolean(true).valueOf() // true

new Date().valueOf() // 1530706938289

var arr = ['1', '2'];
arr.valueOf() // [ '1', '2' ]

var obj = {};
obj.valueOf() // {}
  • toString():将对象转成字符串

除了程序中的自动装箱和自动拆箱,我们还可以手动进行拆箱和装箱操作。直接调用包装类型的 valueOftoString

let num = new Number("123");

console.log(typeof num.valueOf()); // number
console.log(typeof num.toString()); // string

(4)强制类型转换

前置内容讲完了,现在我们来讲类型转换。

在 《你不知道的 JavaScript》中卷中这样写道:

将值从一种类型转换成另一种类型成为类型转换,这是显示的情况;隐式的情况称为强制类型转换。我们也可以这样来区分:类型转换发生在静态类型语言的编译阶段,而强制类型转换则发生在动态类型语言的运行时。然而,在 JavaScript 中通常将它们统称为强制类型转换。

这里我们就用 “隐式强制类型转换” 和 “显示强制类型转换” 来区分。显示强制类型转换这里就不再赘述了,主要来说说隐式类型转换。

类型转换主要包括:原始数据类型之间的相互转换、原始数据与对象类型的相互转换。后者我们刚刚已经在 “装箱” “拆箱” 中提到:原始数据类型是如何转换成相应的对象类型,对象类型转换成原始数据类型又该遵循怎样的原则。接下来介绍前者。

在转换的过程中,我们会使用到规范第 9 节中定义的 “抽象操作”:ToStringToNumberToBoolean

ToNumber规则:

undefined         NaN
null              +0
boolean           true: 1/ false: +0
number            number
string            '123': 123 / 'qwer': NaN
object            ToPrimitive(input, Number).ToNumber()

ToString规则:

undefined         "undefined"
null              "null"
boolean           true: "true"/ false: "false"
number            "number"
string            string
object            ToPrimitive(input, String).ToString()

ToBoolean规则:

// 以下值在进行强制类型转换时均被转换为假值,即 false
undefined
null
false
+0、-0NaN
""

// 其余所有值在强制转换的过程中均为真值,即 true
{}
[]

涉及到隐式转换的运算符最多的是 -*/+== 等运算。其中,当 -*/ 对非 Number 类型进行操作时,会将非 Number 类型转换为 Number类型,即符合 ToNumber 规则。

1 - undefined // NaN
1 - null // 1
2 * true // 2
123 / '123' // 1

这里需要注意的是符号 + 。当执行该操作时满足以下规则:

  1. 当其中一个操作数为字符串(或者能通过 ToPrimitive 操作得到字符串),则执行字符串拼接操作。非字符串的操作数将优先转换为字符串类型。
  2. 否则,执行数字加法。
123 + '123' // 123123
123 + {}  // 123[object Object]

123 + null  // 123
123 + true // 124

而对于运算符 ==,在它执行的过程中会对两侧的操作数进行隐式转换(如果值为非 Boolean 的话),即遵循 ToBoolean 原则。

(5)总结

上面讲解的内容中,虽然没有将所有类型转换一一列出来,但是我们把最主要的规则做了介绍,了解这一点是至关重要的。

我们先介绍了除 Object 之外的对象类型以及部分原始数据的包装类型。接着我们了解了原始数据类型是如何通过隐式的方式转换成包装类型的以及对象类型转换成原始数据类型的详细过程。最后还介绍了对象类型与原始数据类型、原始数据类型之间的转换规则。

二、如何判断变量的数据类型?

(1)typeof 是如何用的?

我们经常听到可以使用 typeof 操作符来判断变量的类型,但是好像又听到有些情况它又分辨不清楚,这到底是怎么回事呢?现在,先来回忆以下我们都介绍了哪些类型:

原始数据类型:UndefinedNullBooleanNumberStringSymbolBigInt

对象类型:ObjectArrayDateRegExpFunction

在对象类型中,其中ObjectArrayDateRegExp可以统称为对象类型系统。

我们先来做个实验:

console.log( typeof undefined ) // undefined
console.log( typeof true )  // boolean
console.log( typeof 123 )  // number
console.log( typeof 'qianzhu' )  // string
console.log( typeof Symbol() )  // symbol

console.log( typeof null ) // object

console.log( typeof {} ) // object
console.log( typeof [] ) // object
console.log( typeof new Date() ) // object
console.log( (typeof /^\d+$/) )   // object

console.log( (typeof function(){} ))   // function

观察一下,对于原始数据类型,除了 null,使用 typeof 运算符都能准确的得知变量的类型。而对于对象类型,我们除了 Function 类型,其他的(对象类型系统)均无法准确的检测出其类型。

对于 null ,它检测出来的类型为 object。这种现象在 JavaScript 诞生以来就是这样。在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签为 0 。由于 null 代表的是空指针(大对数平台下值为 0x00)。因此,null 的类型标签也是 0。这也就是为什么 typeof null 会返回 object的原因。

那么对于对象系统中的类型我们又该如何判断呢?

(2)instanceof

instanceof 操作符的语法是:

object instanceof constructor

它可以帮助我们判断对象系统中变量的具体类型:

console.log( [] instanceof Array); // true
console.log( new Date() instanceof Date); // true
console.log( new RegExp() instanceof RegExp); // true
console.log( {} instanceof Object); // true

但是,如果你这样写,它也会返回 true

console.log( [] instanceof Object); 

这是因为 instanceof 的作用是用来检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。很显然,它检测出来的结果并不准确。

(3)toString

我们前面讲过,toString 类型是 Object.prototype 上面的方法,也就是所有对象类型都具有的方法。在该方法未被覆盖的情况下,当我们调用该方法,它就会返回 "[object, type]",其中的 type 就是对象的类型。

不过可能会让你失望,大部分引用类型都重写了 toString 方法。那我们应该怎么办呢?

既然 toString 方法被重写了,那我们就回到原型链的顶端来检测:

console.log(Object.prototype.toString.call(undefined));  // [object Undefined]
console.log(Object.prototype.toString.call(null));  // [object Null]
console.log(Object.prototype.toString.call(true));  // [object Boolean]
console.log(Object.prototype.toString.call(123));  // [object Number]
console.log(Object.prototype.toString.call('123'));  // [object String]
console.log(Object.prototype.toString.call(Symbol()));  // [object Symbol]
console.log(Object.prototype.toString.call({}));  // [object Object]
console.log(Object.prototype.toString.call([]));  // [object Array]
console.log(Object.prototype.toString.call(new Date()));  // [object Date]
console.log(Object.prototype.toString.call(new RegExp()));  // [object RegExp]
console.log(Object.prototype.toString.call(function(){}));  // [object Function]

(4)总结

我们通过两篇文章的篇幅大致的讲解了一下 JavaScript 中的变量及类型。主要的路线为: JavaScript 有哪些数据类型?这些类型的变量又是怎么存储的呢?这些类型的变量是如何相互转换的?最后,我们又应该如何去检测这些变量呢?

关于数据类型及变量的知识远不止这些,今天先开个头,今后我们慢慢探索。


这是深入理解 JavaScript 语法合集的其中一篇,其它合集:
1. 深入理解 JavaScript —— 从设计一门语言讲起
2. 你真的掌握了 JavaScript 变量和类型嘛?(上)
4. 原型和原型链
5. JavaScript 中的对象

参考文章:
1. JavaScript - 数据类型及隐式转换-2017-06-05 by 面向信仰
2. 你真的掌握变量和类型了吗-2019-05-28 by ConardLi
3.《你不知道的 JavaScript 》中卷 by Kyle Simpson
4. Javascript原始值-2016-12-11 by Web技术研究室 lyz810
5. MDN Web docs——instanceof-2019-09-27 by MDN contributors