从 ECMA 规范看 JavaScript 类型转换

2019-10-09 admin

前言

JavaScript 中的类型转换一直都是让前端开发者最头疼的问题。前阵子,推特上有个人专门发了一张图说 JavaScript 让人不可思议。

image_1dm5s9qr814dvnsi96laugvg9.png-51.4kB

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

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

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

数据类型

JS 中有六种简单数据类型:undefinednullbooleanstringnumbersymbol,以及一种复杂类型:object。 但是 JavaScript 在声明时只有一种类型,只有到运行期间才会确定当前类型。在运行期间,由于 JavaScript 没有对类型做严格限制,导致不同类型之间可以进行运算,这样就需要允许类型之间互相转换。

类型转换

显式类型转换

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

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

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

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

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

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

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

ToNumber

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

原始值 转换结果
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 的规范去理解这些转换规则的原理,那么就会很容易知道为什么最后会得到那些结果。

[转载]原文链接:https://segmentfault.com/a/1190000020624225

本站文章除注明转载外,均为本站原创或编译。欢迎任何形式的转载,但请务必注明出处。

转载请注明:文章转载自 JavaScript中文网 [https://www.javascriptcn.com]

本文地址:https://www.javascriptcn.com/read-76925.html

文章标题:从 ECMA 规范看 JavaScript 类型转换

相关文章
破解前端面试(80% 应聘者不及格系列):从 闭包说起
不起眼的开始 招聘前端工程师,尤其是中高级前端工程师,扎实的 JS 基础绝对是必要条件,基础不扎实的工程师在面对前端开发中的各种问题时大概率会束手无策。在考察候选人 JS 基础的时候,我经常会提供下面这段代码,然后让候选人分析它实际运行的结...
2017-06-02
JavaScript教程:JS中的原型
Keith Peters 几年前发表的一篇博文,关于学习没有“new”的世界,其中解释了使用原型继承代替构造函数。两者都是纯粹的原型编码。 标准方法(The Standard Way) 一直以来,我们学习的在 JavaScript 里创建对...
2015-11-12
javascript是什么意思
avaScript是Netscape开发的一个对象脚本语言,它使用在世界各地数以百万计的网页和服务器应用程序上。 网景的JavaScript是ecma - 262版的标准脚本语言,和公布的标准只有轻微的差异。 与广为流行的错误理解相反,Ja...
2015-11-12
21天学通javascript
简介: 本书是Javascript入门教程。Javascript是Web开发中应用最早、发展最成熟、用户最多的脚本语言。其语法简洁,代码可读性在众多脚本语言中最好,它在使用时不用考虑数据类型,是真正意义上的动态语言。本书总分为四篇,共21章...
2015-11-16
JavaScript的组成
一个完整的JavaScript由3个部分组成:核心(ECMAScript) 文档对象模型(DOM) 浏览器对象模型(BOM) ECMAScript 描述了该语言的语法和基本对象 ; DOM 描述了处理网页内容的方法和接口 ; BOM 描...
2015-11-12
javaScript+turn.js实现图书翻页效果实例代码
为了实现图书翻页的效果我们在网上可以看到很多教程 在这里推荐turn.js 网上的turn.js 有api 不过是英文的  很多人看起来不方便 .关于代码也是奇形怪状在这里我将详细讲解如何使用turn.js实现翻页效果 ,本篇文章只是讲解 ...
2017-03-16
JavaScript 事件流、事件处理程序及事件对象总结
JS与HTML之间的交互通过事件实现。事件就是文档或浏览器窗口中发生的一些特定的交互瞬间。可以使用监听器(或处理程序)来预定事件,以便事件发生时执行相应的代码。这种在传统软件工程中被称为观察员模式,支持页面的行为与页面的外观之间的松散耦合。...
2017-04-05
JavaScript变量的声明
声明变量 变量在脚本中的首次亮相是在其声明中。 在变量首次出现时将会在内存中设置它,因此您稍后可在脚本中引用它。 应在使用变量之前先声明变量。 可以使用 var 关键字实现此目的。 <span id=“mt9” class=“sent...
2015-11-12
AngularJS vs. jQuery,看看谁更胜一筹
很多Web开发新手都会有这样的疑问“我应该使用什么开发框架呢,如何快速学会Web开发呢?”这个问题其实没有一个统一的正确答案,其中讨论最多的就是AngularJS和jQuery的差别。这两者的之间的比较很微妙,有时就像拿苹果和橘子在作比较,...
2015-11-12
可以从CSS框架中借鉴到什么
现在很多人会使用 CSS 框架进行快速建站。   那 CSS 框架是什么呢,它通常是一些 CSS 文件的集合,这些文件包括基本布局、表单样式、网格、简单组件、以及样式重置。使用 CSS 框架大大降低工作成本进行快速建站。   当然对于一些大...
2016-03-11
回到顶部