JavaScript中的类型转换

似曾相识

正文之前,先抛出几组问题:

// 第一组
[] == [] //false

// 第二组
[] == ![] //true

{} == !{} //false

{} == ![] // false

[] == !{} //true

{} == 1 // false

// 第三组
{} < 1 // false

{} > 1 // false

看到这几个问题,是不是一脸懵逼?

稍微有点基础的同学,应该一眼就能看出 [] == [] 输出 false,因为 Object 是引用类型,两个引用类型做 == 比较,如果它们引用的是同一个地址,输出 true,否则输出 false。但是后面几道题可能会有一点点麻烦。

后面几道题都涉及到 JavaScript 中的一个难点:隐式转换。本文将会带领大家深入了解 JavaScript 中的类型转换机制。

1,JavaScript 数据类型

js数据类型分为两大类:

  • 基本类型(原始值):Undefined,Null,Boolean,Number,String, Symbol
  • 对象类型:Object

2,ECMAScript 规范中的抽象操作

2.1 ToPrimitive ( input [, PreferredType] ) 转换为原始值

抽象操作 ToPrimitive(input[, PreferredType]),将input参数转换为一个非对象类型的值,即原始值类型。转换规则如下:

  • Undefined:返回原始值,不转换
  • Null:返回原始值,不转换
  • Boolean:返回原始值,不转换
  • Number:返回原始值,不转换
  • String:返回原始值,不转换
  • Symbol:返回原始值,不转换
  • Object:见下文

Type(input)Object 时,可以将抽象操作 ToPrimitive(input[, PreferredType]) 的执行过程用如下代码解释:

// 仿抽象操作 ToPrimitive(input[, PreferredType]) 的执行过程
function ToPrimitive(input: Object, PreferredType: undefined | 'String' | 'Number') {
  let hint;
  // 如果没有送 PreferredType ,hint 为 'default'
  // 如果 PreferredType 为 'String', hint 为 'string'
  // 如果 PreferredType 为 'Number', hint 为 'number'
  if(!PreferredType) {
    hint = 'default';
  } else if (PreferredType === 'String') {
    hint = 'string';
  } else if(PreferredType === 'Number') {
    hint = 'number';
  }

  // 获取对象的 @@toPrimitive 方法,如果对象自身没有,会一直顺着原型链查找
  let exoticToPrim = GetMethod(input, Symbol.toPrimitive);

  // 如果该方法不为undefined,用对象调用该方法,赋值给result
  // 如果 result 不是 Object 类型,返回 result;否则抛出 TypeError 异常
  if(exoticToPrim !== undefined) {
    let result = exoticToPrim.call(input, hint);
    if (typeof result !== 'object') {
      return  result;
    }
    throw new TypeError();
  }

  // 如果该方法为undefined
  // 如果 hint 为 'default',令 hint 为 'number'
  // 返回 抽象操作 OrdinaryToPrimitive 的调用结果
  if(hint === 'default'){
    hint = 'number';
  }
  return OrdinaryToPrimitive(input,hint);
}

这里涉及到另一个抽象操作 GetMethod,我们先看一下ECMAScript 6 规范中对 GetMethod 的定义:

图片描述

简单说来就是: 抽象操作 GetMethod(O,P) 获取对象 O 的 P 属性,如果 该属性是 undefined,返回 undefined;如果该属性是一个函数,返回此函数;否则抛出一个 TypeError 异常。

下面我们看一下 抽象操作 OrdinaryToPrimitive 的过程:

// 仿抽象操作 OrdinaryToPrimitive(O, hint) 的执行过程
function OrdinaryToPrimitive(O: Object, hint: 'string' | 'number') {
  // 假定 hint 是一个字符串,并且其值只能是'string' 或 'number'
  // 如果 hint === 'string',令 methodNames = ['toString', 'valueOf']
  // 如果 hint === 'number',令 methodNames = ['valueOf', 'toString']
  let methodNames;
  if(hint === 'string') {
    methodNames = ['toString', 'valueOf'];
  } else {
    methodNames = ['valueOf', 'toString'];
  }

  // 遍历 methodNames,获取对象的方法,赋值给 result
  // 如果 result 不是 Object,终止遍历,并返回 result
  // 抛出一个 TypeError 异常
  for (let item of methodNames) {
    let method = O[item];
    let result = method.call(O);
    if(typeof(result) !== 'object') {
      return result;
    }
  }

  throw new TypeError();
}

Date 对象和 Symbol 对象的原型上已经部署了 [@@toPrimitive] 方法,这个方法是不可枚举(enumerable: false),不可改写的(writable: false)。对于Date对象原型上的[@@toPrimitive] 方法,如果没有送hint,会将hint当作'string'

我们可以使用 Symbol.toPrimitive 来给 Object 添加 [@@toPrimitive] 方法:

Object.prototype[Symbol.toPrimitive] = function(hint) {
  if(hint === 'default') {
    let thisType = Object.prototype.toString.call(this);
    if(thisType === '[object Date]') {
      hint = 'string';
    } else {
      hint = 'number';
    }
  }

  let methodNames;
  if(hint === 'string') {
    methodNames = ['toString', 'valueOf'];
  } else if(hint === 'number') {
    methodNames = ['valueOf', 'toString'];
  } else {
    throw new TypeError('Invalid hint: ' + hint);
  }

  for (let key of methodNames) {
    let method = this[key];
    let result = method.call(this);
    if(typeof(result) !== 'object') {
      return result;
    }
  }

  throw new TypeError();
}

2.1.1 对象的 valueOf() 方法和 toString() 方法

对象在执行 ToPrimitive 转换时,需要用到对象的valueOf()toString()方法。我们可以在Object.prototype上找到这两个方法。在JavaScript中,Object.prototype是所有对象原型链的顶层原型,因此,任何对象都有valueOf()toString()方法。

JavaScript的许多内置对象都重写了这两个方法,以实现更适合自身的功能需要。

不同类型对象的valueOf()方法的返回值:

  • Array: 返回数组对象本身。
  • Boolean: 布尔值。
  • Date: 存储的时间是从 1970 年 1 月 1 日午夜开始计的毫秒数 UTC。
  • Function: 函数本身。
  • Number: 数字值。
  • Object: 对象本身。这是默认情况。
  • String: 字符串值。
  • Symbol:Symbol值本身。

不同类型对象的toString()方法的返回值:

  • Array:连接数组并返回一个字符串,其中包含用逗号分隔的每个数组元素。
  • Boolean:返回字符串 "true""false"
  • Date:返回一个美式英语日期格式的字符串。
  • Function:返回一个字符串,其中包含用于定义函数的源文本段。
  • Number: 返回指定 Number 对象的字符串表示形式。
  • Object: 返回 "[object type]",其中 type 是对象的类型。
  • String: 字符串值。
  • Symbol:返回当前 Symbol 对象的字符串表示。

2.2 ToBoolean 转换为布尔值类型

抽象操作 ToBoolean 根据下列规则将其参数转换为布尔值类型的值:

  • Undefinedfalse
  • Nullfalse
  • Boolean:结果等于输入的参数(不转换)。
  • Number:如果参数是 +0, -0,NaN,结果为 false ;否则结果为 true
  • String:如果参数是空字符串(其长度为零),结果为 false,否则结果为 true
  • Symboltrue
  • Objecttrue

2.3 ToNumber 转换为数值类型

抽象操作 ToNumber 根据下列规则将其参数转换为数值类型的值:

  • Undefined:NaN
  • Null:+0
  • Boolean:如果参数是 true,结果为 1。如果参数是 false,此结果为 +0
  • Number:结果等于输入的参数(不转换)。
  • String:参见下文
  • Symbol:抛出 TypeError 异常
  • Object:先进行 ToPrimitive 转换,得到原始值,再进行 ToNumber 转换

2.3.1 对字符串类型应用 ToNumber

对字符串应用 ToNumber 时,如果符合如下规则,转为数值:

  • 十进制的字符串数值常量,可有任意位数的0在前面,如 '000123''123' 都会被转为 123
  • 指数形式的字符串数值常量,如 '1e2' 转为 100
  • 带符号的十进制字符串数值常量或指数字符串数值常量,如'-100', '-1e2' 都会转为 -100
  • 二进制,八进制,十六进制的字符串数值常量,如'0b11', '0o11', '0x11' 分别转为 3, 9, 17
  • 符合上述条件的字符串数值常量开头或结尾,可以包含任意多个空格。如' 0b11 ' 转为 3
  • 空字符串(长度为零的字符串)或只有空格的字符串,转为 0

如果字符串不符合上述规则,将转为NaN

2.4 ToString 转为字符串类型

抽象操作 ToString 根据下列规则将其参数转换为字符串类型的值:

  • Undefined"undefined"
  • Null"null"
  • Boolean:如果参数是 true,那么结果为 "true"。 如果参数是 false,那么结果为 "false"
  • String:结果等于输入的参数(不转换)。
  • Number:参见下文。
  • Symbol:抛出 TypeError 异常
  • Object:先进行 ToPrimitive 转换,hint'string',得到原始值,再进行 ToString 转换

2.4.1 对数值类型应用 ToString

抽象操作 ToString 运算符将数字 m 转换为字符串格式的给出如下所示:

  1. 如果 m 是 NaN,返回字符串 "NaN"
  2. 如果 m 是 +0-0,返回字符串 "0"
  3. 如果 m 小于零,返回 "-m"
  4. 如果 m 正无限大,返回字符串 "Infinity"。如果 m 负无限大,返回字符串 "-Infinity"
  5. 否则,返回 "m" 或 m 的指数形式的字符串数值常量

2.5,抽象操作 GetValue

先看一下 ECMAScript 规范中定义的 GetValue 方法:

图片描述

注意区分这一句:2. If Type(V) is not Reference, return V. 中的 Reference 和我们平时说的 引用类型 的区别。

我们平时说的 引用类型 指的是 ECMAScript 规范中 语言类型的 Object 类型(例如 Object, Array, Date 等);而这里的 Reference 指的是ECMAScript 规范中 规范类型的 Reference 类型 ,是一个抽象的概念。

按规范的描述,Reference 是一个 name binding,由三部分组成:

  • base:一个 undefined,Object, Boolean, String, Symbol, Number 或者 环境记录(Environment Record)
  • referreference name:一个字符串 或 Symbol 值
  • strict mode flag:严格引用标志

举个例子:赋值语句 let obj.a = 1 中的 obj.a 产生的 Reference,base 是 obj,referreference name 是 'b',至于 strict mode flag 是用来检测是否处于严格模式。

Reference 和 环境记录(Environment Record) 这些概念是为了更好地描述语言的底层行为逻辑才存在的,并不存在于我们实际的 js 代码中。

4,逻辑非运算符(!)

逻辑非运算符(!) 按下列过程将表达式转换为布尔值

  1. expr 为表达式求值的结果
  2. oldValue = ToBoolean(GetValue(expr))
  3. 如果 oldValue === true,返回 false;否则返回 true

因此,逻辑非运算符(!)可以当作是:对 ToBoolean 操作的结果取反

5,== 运算符

比较运算 x==y,按如下规则进行:

1,若 Type(x) 与 Type(y) 相同, 则

    1) 若 Type(x) 为 Undefined, 返回 true。
    2) 若 Type(x) 为 Null, 返回 true。
    3) 若 Type(x) 为 Number, 则
  
        (1)、若 x 为 NaN, 返回 false。
        (2)、若 y 为 NaN, 返回 false。
        (3)、若 x 与 y 为相等数值, 返回 true。
        (4)、若 x 为 +0 且 y 为 −0, 返回 true。
        (5)、若 x 为 −0 且 y 为 +0, 返回 true。
        (6)、返回 false。
        
    4) 若 Type(x) 为 String,则当 x 和 y 为完全相同的字符序列时返回 true。 否则,返回 false。
    5) 若 Type(x) 为 Boolean,当 x 和 y 为同为 true 或者同为 false 时返回 true。 否则, 返回 false。
    6) 若 Type(x) 为 Symbol,如果 x 和 y 是同一个 Symbol,返回true。否则,返回 false。
    7) 若 Type(x) 为 Object,当 x 和 y 是对同一对象的引用时返回 true。否则,返回 false。
     
2,若 x 为 null 且 y 为 undefined,返回 true。
3,若 x 为 undefined 且 y 为 null,返回 true。

4,若 Type(x) 为 Number 且 Type(y) 为 String,返回 x == ToNumber(y)的结果。
5,若 Type(x) 为 String 且 Type(y) 为 Number,返回 ToNumber(x) == y的结果。

6,若 Type(x) 为 Boolean, 返回 ToNumber(x) == y 的结果。
7,若 Type(y) 为 Boolean, 返回 x == ToNumber(y) 的结果。

8、若 Type(x) 为 String 或 Number 或 Symbol,且 Type(y) 为 Object,返回 x == ToPrimitive(y) 的结果。
9、若 Type(x) 为 Object 且 Type(y) 为 String 或 Number 或 Symbol, 返回 ToPrimitive(x) == y 的结果。

10、返回 false。

现在,我们来分析一下文章开头提出的问题:[] == ![] // true

  1. 根据 上面 逻辑非运算符 和 ToPrimitive 的规则,![] 返回 false,因此,我们接下来需要比较的是 [] == false
  2. [] == false 符合上面规则中的第 7 条,需要对 false 执行 ToNumber 转换,得到 0,接下来要比较 [] == 0
  3. [] == 0 符合上面规则中的第 9 条,对 [] 进行 ToPrimitive 转换,得到空字符串 '',接下来要比较 '' == 0
  4. '' == 0 符合上面规则中的第 5 条,对 '' 进行 ToNumber 转换,得到 0
  5. 接下来比较 0 == 0,得到true

其他几道题我就不一一分析了,有兴趣的同学们可以自己分析验证。提示一下,需要注意 Object.prototype.toStringArray.prototype.toString 的区别

6,比较运算符

比较运算 x < y,按照如下规则执行

1,令 px = ToPrimitive(x),令 py = ToPrimitive(y)。

2,如果 Type(px) 和 Type(py) 都是 String,则

    1)、如果 py 是 px 的前缀,返回 false。
    2)、如果 px 是 py 的前缀,返回 true。
    3)、找出 px 和 py 中 相同下标处第一个不同的字符串单元,将其 词典排序 分别记为 m 和 n。
    4)、如果 m < n,返回 true,否则,返回 false。
    
3,令 nx = ToNumber(px),令 ny = ToNumber(py)

    1)、如果 Type(nx) 是 NaN,返回 false。
    2)、如果 Type(ny) 是 NaN,返回 false。
    3)、如果 nx 是 +0,ny 是 -0,返回 true。
    4)、如果 nx 是 -0,ny 是 +0,返回 true。
    5)、如果 nx 是 +Infinity,返回 false。
    6)、如果 ny 是 +Infinity,返回 true 。
    7)、如果 ny 是 -Infinity,返回 false。
    8)、如果 nx 是 -Infinity,返回 true。
    9)、如果 nx 的数学值小于 ny 的数学值,返回 true,否则返回 false。
    
阅读 234
147 声望
4 粉丝
0 条评论
你知道吗?

147 声望
4 粉丝
宣传栏