从 ECMA 规范看 JavaScript 类型转换

前言

JavaScript 中的类型转换一直都是让前端开发者最头疼的问题。前阵子,推特上有我的专门发了一张图说 JavaScript 让人难以想象。前端

image_1dm5s9qr814dvnsi96laugvg9.png-51.4kB

除了这个,还有不少经典的、让 JavaScript 开发者摸不着头脑的类型转换,譬以下面这些,你是否知道结果都是多少?面试

1 + {} === ?
{} + 1 === ?
1 + [] === ?
1 + '2' === ?

本文将带领你从 ECMA 规范开始,去深刻理解 JavaScript 中的类型转换,让类型转换再也不成为前端开发中的拦路虎。函数

数据类型

JS 中有六种简单数据类型:undefinednullbooleanstringnumbersymbol,以及一种复杂类型:object
可是 JavaScript 在声明时只有一种类型,只有到运行期间才会肯定当前类型。在运行期间,因为 JavaScript 没有对类型作严格限制,致使不一样类型之间能够进行运算,这样就须要容许类型之间互相转换。spa

类型转换

显式类型转换

显式类型转换就是手动地将一种值转换为另外一种值。通常来讲,显式类型转换也是严格按照上面的表格来进行类型转换的。prototype

经常使用的显式类型转换方法有 NumberStringBooleanparseIntparseFloattoString 等等。
这里须要注意一下 parseInt,有一道题偶尔会在面试中遇到。翻译

问:为何 [1, 2, 3].map(parseInt) 返回 [1,NaN,NaN]?
答:parseInt函数的第二个参数表示要解析的数字的基数。该值介于 2 ~ 36 之间。

若是省略该参数或其值为 0,则数字将以 10 为基础来解析。若是它以 “0x” 或 “0X” 开头,将以 16 为基数。3d

若是该参数小于 2 或者大于 36,则 parseInt() 将返回 NaN。
通常来讲,类型转换主要是基本类型转基本类型、复杂类型转基本类型两种。
转换的目标类型主要分为如下几种:code

  1. 转换为 string
  2. 转换为 number
  3. 转换为 boolean

我参考了 ECMA-262 的官方文档来总结一下这几种类型转换。ECMA 文档连接:ECMA-262对象

ToNumber

其余类型转换到 number 类型的规则见下方表格:blog

原始值 转换结果
Undefined NaN
Null 0
true 1
false 0
String 根据语法和转换规则来转换
Symbol Throw a TypeError exception
Object 先调用toPrimitive,再调用toNumber

String 转换为 Number 类型的规则:

  1. 若是字符串中只包含数字,那么就转换为对应的数字。
  2. 若是字符串中只包含十六进制格式,那么就转换为对应的十进制数字。
  3. 若是字符串为空,那么转换为0。
  4. 若是字符串包含上述以外的字符,那么转换为 NaN。

使用+能够将其余类型转为 number 类型,咱们用下面的例子来验证一下。

+undefined // NaN
+null // 0
+true // 1
+false // 0
+'111' // 111
+'0x100F' // 4111
+'' // 0
'b' + 'a' + + 'a' + 'a' // 'baNaNa'
+Symbol() // Uncaught TypeError: Cannot convert a Symbol value to a number

ToBoolean

原始值 转换结果
Undefined false
Boolean true or false
Number 0和NaN返回false,其余返回true
Symbol true
Object true

咱们也可使用 Boolean 构造函数来手动将其余类型转为 boolean 类型。

Boolean(undefined) // false
Boolean(1) // true
Boolean(0) // false
Boolean(NaN) // false
Boolean(Symbol()) // true
Boolean({}) // true

ToString

原始值 转换结果
Undefined 'Undefined'
Boolean 'true' or 'false'
Number 对应的字符串类型
String String
Symbol Throw a TypeError exception
Object 先调用toPrimitive,再调用toNumber

转换到 string 类型能够用模板字符串来实现。

`${undefined}` // 'undefined'
`${true}` // 'true'
`${false}` // 'false'
`${11}` // '11'
`${Symbol()}` // Cannot convert a Symbol value to a string
`${{}}`

隐式类型转换

隐式类型转换通常是在涉及到运算符的时候才会出现的状况,好比咱们将两个变量相加,或者比较两个变量是否相等。
隐式类型转换其实在咱们上面的例子中已经有所体现。对于对象转原始类型的转换,也会遵照 ToPrimitive 的规则,下面会进行细说。

从ES规范来看类型转换

ToPrimitive

在对象转原始类型的时候,通常会调用内置的 ToPrimitive 方法,而 ToPrimitive 方法则会调用 OrdinaryToPrimitive 方法,咱们能够看一下 ECMA 的官方文档。

image_1dard6av87ir24p140nv5d1vq9.png-182.5kB

我来翻译一下这段话。

ToPrimitive 方法接受两个参数,一个是输入的值 input,一个是指望转换的类型 PreferredType

  1. 若是没有传入 PreferredType 参数,让 hint 等于"default"
  2. 若是 PreferredTypehint String,让 hint 等于"string"
  3. 若是 PreferredTypehint Number,让 hint 等于"number"
  4. exoticToPrim 等于 GetMethod(input, @@toPrimitive),意思就是获取参数 input@@toPrimitive 方法
  5. 若是 exoticToPrim 不是 Undefined,那么就让 result 等于 Call(exoticToPrim, input, « hint »),意思就是执行 exoticToPrim(hint),若是执行后的结果 result 是原始数据类型,返回 result,不然就抛出类型错误的异常
  6. 若是 hint 是"default",让 hint 等于"number"
  7. 返回 OrdinaryToPrimitive(input, hint) 抽象操做的结果

OrdinaryToPrimitive

OrdinaryToPrimitive 方法也接受两个参数,一个是输入的值O,一个也是指望转换的类型 hint

  1. 若是输入的值是个对象
  2. 若是 hint 是个字符串而且值为'string'或者'number'
  3. 若是 hint 是'string',那么就将 methodNames 设置为 toStringvalueOf
  4. 若是 hint 是'number',那么就将 methodNames 设置为 valueOftoString
  5. 遍历 methodNames 拿到当前循环中的值 name,将 method 设置为 O[name](即拿到 valueOftoString 两个方法)
  6. 若是 method 能够被调用,那么就让 result 等于 method 执行后的结果,若是 result 不是对象就返回 result,不然就抛出一个类型错误的报错。

ToPrimitive 的代码实现

若是只用文字来描述,你确定会以为过于晦涩难懂,因此这里我就本身用代码来实现这两个方法帮助你的理解。

// 获取类型
const getType = (obj) => {
    return Object.prototype.toString.call(obj).slice(8,-1);
}
// 是否为原始类型
const isPrimitive = (obj) => {
    const types = ['String','Undefined','Null','Boolean','Number'];
      return types.indexOf(getType(obj)) !== -1;
}
const ToPrimitive = (input, preferredType) => {
    // 若是input是原始类型,那么不须要转换,直接返回
    if (isPrimitive(input)) {
        return input;
    }
    let hint = '', 
        exoticToPrim = null,
        methodNames = [];
    // 当没有提供可选参数preferredType的时候,hint会默认为"default";
    if (!preferredType) {
        hint = 'default'
    } else if (preferredType === 'string') {
        hint = 'string'
    } else if (preferredType === 'number') {
        hint = 'number'
    }
    exoticToPrim = input.@@toPrimitive;
    // 若是有toPrimitive方法
    if (exoticToPrim) {
        // 若是exoticToPrim执行后返回的是原始类型
        if (typeof (result = exoticToPrim.call(O, hint)) !== 'object') {
            return result;
        // 若是exoticToPrim执行后返回的是object类型
        } else {
            throw new TypeError('TypeError exception')
        }
    }
    // 这里给了默认hint值为number,Symbol和Date经过定义@@toPrimitive方法来修改默认值
    if (hint === 'default') {
        hint = 'number'
    }
    return OrdinaryToPrimitive(input, hint)
}
const OrdinaryToPrimitive = (O, hint) => {
    let methodNames = null,
        result = null;
    if (typeof O !== 'object') {
        return;
    }
    // 这里决定了先调用toString仍是valueOf
    if (hint === 'string') {
        methodNames = [input.toString, input.valueOf]
    } else {
        methodNames = [input.valueOf, input.toString]
    }
    for (let name in methodNames) {
        if (O[name]) {
            result = O[name]()
            if (typeof result !== 'object') {
                return result
            }
        }
    }
    throw new TypeError('TypeError exception')
}

总结一下,在进行类型转换的时候,通常是经过 ToPrimitive 方法将引用类型转为原始类型。若是引用类型上有 @@toPrimitive 方法,就调用 @@toPrimitive 方法,执行后的返回值为原始类型就直接返回,若是依然是对象,那么就抛出报错。

若是对象上没有 toPrimitive 方法,那么就根据转换的目标类型来判断先调用 toString 仍是 valueOf 方法,若是执行这两个方法后获得了原始类型的值,那么就返回。不然,将会抛出错误。

Symbol.toPrimitive

在 ES6 以后提供了 Symbol.toPrimitive 方法,该方法在类型转换的时候优先级最高。

const obj = {
  toString() {
    return '1111'
  },
  valueOf() {
    return 222
  },
  [Symbol.toPrimitive]() {
    return 666
  }
}
const num = 1 + obj; // 667
const str = '1' + obj; // '1666'

例子

也许上面关于 ToPrimitive 的代码讲解你仍是会以为晦涩难懂,那我接下来就举几个例子来讲明对象的类型转换。

var a = 1, 
    b = '2';
var c = a + b; // '12'

也许你会好奇,为何不是将后面的 b 转换为 number 类型,最后获得3?
咱们仍是要先看文档对加号的定义。

image_1davvk6ij3lnsisjsk1i8djf8p.png-243.3kB

首先会分别执行两个值的 toPrimitive 方法,由于 ab 都是原始类型,因此仍是获得了1和'2'。
从图上看到若是转换后的两个值的 Type 有一个是 String 类型,那么就将两个值通过 toString 转换后串起来。所以最后获得了'12',而不是3。

咱们还能够再看一个例子。

var a = 'hello ', b = {};
var c = a + b; // "hello [object Object]"

这里还会分别执行两个值的 toPrimitive 方法,a 仍是获得了'hello ',而b因为没有指定preferredType,因此会默认被转为 number 类型,先调用 valueOf,但 valueOf 仍是返回了一个空对象,不是原始类型,因此再调用 toString,获得了 '[object Object]',最后将二者链接起来就成了 "hello [object Object]"
若是咱们想返回 'hello world',那该怎么改呢?只须要修改 bvalueOf 方法就行了。

b.valueOf = function() {
    return 'world'
}
var c = a + b; // 'hello world'

也许你在面试题中看到过这个例子。

var a = [], b = [];
var c = a + b; // ''

这里为何 c 最后是''呢?由于 ab 在执行 valueOf 以后,获得的依然是个 [] ,这并不是原始类型,所以会继续执行 toString,最后获得'',两个''相加又获得了''。
咱们再看一个指定了 preferredType 的例子。

var a = [1, 2, 3], b = {
    [a]: 111
}

因为 a 是做为了 b 的键值,因此 preferredTypestring,这时会调用 a.toString 方法,最后获得了'1,2,3'

总结

类型转换一直是学 JS 的时候很难搞明白的一个概念,由于转换规则比较复杂,常常让人以为莫名其妙。可是若是从 ECMA 的规范去理解这些转换规则的原理,那么就会很容易知道为何最后会获得那些结果。

相关文章
相关标签/搜索