JS基本数据类型=>JS类型转换原理,看这篇就够了

1,146 阅读8分钟

JS基本数据类型

  • undefined
  • null
  • number
  • string
  • symbol(ES6 2015)
  • boolean
  • bigInt(ES10 2019)

1.JS分为基本数据类型与引用类型,基本数据类型中的string,number,boolean在创建的时候,会自动形成一个包装类型,此包装类型与引用类型的主要区别就是生存周期,时候用new操作符创建的引用类型的实例,在执行流离开当前作用域之前都一直保存在内存中,而自动创建的基本包装类型的对象,则只存在于一行代码的执行瞬间,然后会被立即销毁,这意味着不能给包装类型添加属性和方法

可以将包装类型的创建看做以下过程

  1. 创建String类型的一个实例
  2. 在实例上调用指定的方法
  3. 销毁实例

当然可以显式的调用Boolean、Number、String来创建基本包装类型的对象,不过请不要这么做,因为这样的做法很容易让人无法区分是“基本类型”还是“引用类型”

const a = new Boolean(false);
const b = false;

console.log(typeof a);   // object
console.log(typeof b);    // boolean
console.log(a instanceof Boolean);  // true
console.log(b instanceof Boolean);   // false

const c = new Object(false);
console.log(typeof c);             // object
console.log(c instanceof Object);  // true
console.log(c instanceof Boolean); // true

通过new Object创建的对象,根据传入的类型,如果是number,boolean,string,则会返回包装类型,所以instanceof通过原型链,既能找到Object构造函数,又能找到Boolean构造函数

将Number、String、Boolean作为构造函数调用,类型就已经是object了,通过原型链判断,可以判断构造函数为Boolean

如果不将Number、String、Boolean作为构造函数调用,也就是不加new,则就是作为普通函数调用,会把传入的参数转为number,string,boolean类型(就是基本数据类型,不是包装类型)

类型转换:ToPrimitive 算法

在做条件判断时,除了null,undefined,flase,NaN,'',0,-0,其他所有值都转为true

JavaScript 对象转换到基本类型值时,会使用 ToPrimitive 算法,这是一个内部算法,是编程语言在内部执行时遵循的一套规则。

ToPrimitive算法执行时,会传入一个hint,表示这是什么类型的转换运算(也有叫做期望值),根据这个hint参数,会决定如何执行ToPrimitive算法逻辑

hint参数取值只能是以下3个之一:

  • string
  • number
  • default

当对象发生转换成基本类型时,对按照以下逻辑调用对象上的方法

  1. 如果存在[Symbol.toPrimitive],则优先调用
  2. 如果1不存在,且hintstring,则依次调用toString()和valueOf()(如果toString()返回了基本类型,则不会继续调用)
  3. 如果1不存在,且hintnumber或者default,则依次调用valueOf()和toString()(如果valueOf()返回了基本类型,则不会继续调用)

详细过程:

ToPrimitive(input [, PreferredType]) 该抽象操作接受一个参数input和一个可选的参数PreferredType。该抽象操作的目的是把参数input转化为非对象数据类型,也就是原始数据类型。如果input可以同时转化为多个原始数据,那么会优先参考PreferredType的值。

如果input的类型是对象,则执行以下步骤:

  1. 如果没有传入PreferredType参数,则hint等于"default"
  2. 如果PreferredType是string,则hint为string,number同理
  3. 判断是否存在[Symbol.toPrimitive]方法如果存在,则调用 (1)执行[Symbol.toPrimitive]方法(2)如果结果是原始数据类型,则返回(3)抛出类型错误的异常
  4. 如果hint是"default",则让hint等于"number"
  5. 返回OrdinaryToPrimitive(object,hint)操作结果

OrdinaryToPrimitive就是一个判断转换顺序的逻辑

  1. 如果hint是"string",则执行顺序为toString=>valueOf
  2. 如果hint是"number",则执行顺序为valueOf=>toString
  3. 如果无法返回基本数据类型,则抛出错误

并不要求[Symbol.toPrimitive]toString()/valueOf()返回hint期望的值,但是

  1. [Symbol.toPrimitive]和toString方法返回的值必须是基本类型
  2. valueOf可以返回基本类型和非基本类型

Symbol.toPrimitive 是一个内置的 Symbol 值,它是作为对象的函数值属性存在的,当一个对象转换为对应的原始值时,会调用此函数。

默认情况下,普通对象具有toStringvalueOf方法

  • toString方法返回一个字符串"[object Object]"
  • valueOf返回对象自身
// 一个没有提供 Symbol.toPrimitive属性的对象,参与运算时的输出结果
const obj1 = {};
console.log(+obj1);     // NaN
console.log(`${obj1}`); // "[object Object]"
console.log(obj1 + ""); // "[object Object]"

// 接下面声明一个对象,手动赋予了Symbol.toPrimitive属性,再来查看输出结果
const obj2 = {
  [Symbol.toPrimitive](hint) {
    if (hint == "number") {
      return 10;
    }
    if (hint == "string") {
      return "hello";
    }
    return true;
  }
};
console.log(+obj2);     // 10      -- hint 参数值是 "number"
console.log(`${obj2}`); // "hello" -- hint 参数值是 "string"
console.log(obj2 + ""); // "true"  -- hint 参数值是 "default"

Symbol和Date已经默认定义了ToPrimitive方法。 Date默认定义的方法是Date.prototype[Symbol.toPrimitive]、 Symbol默认定义的方法是Symbol.prototype[Symbol.toPrimitive]

Date 对象的 @@toPrimitive 方法可以返回一个原始值,或是 string,或是number。 如果 hint 是 "string" 或 "default",@@toPrimitive 将会调用 toString。如果 toString 属性不存在,则调用 valueOf。如果 valueOf 也不存在,则抛出一个TypeError。 如果 hint 是 "number",@@toPrimitive 会首先尝试 valueOf,若失败再尝试 toString。 当期望一个原始值却收到一个对象时,JavaScript 可以自动的调用 @@toPrimitive 方法来将一个对象转化成原始值,所以你很少会需要自己调用这个方法。

从标准看ToPrimitive 算法

ECMA-262 5.1标准

从加法逻辑开始,可以看到在加法逻辑中使用了ToPrimitive方法(第5、6步)

加法逻辑.png

ToPrimitive方法逻辑原解释,可以看到ToPrimitive算法,会将所有基本类型数据都直接返回,只有对象会进行[[DefaultValue]]逻辑处理

ToPrimitive逻辑.png

[[DefaultValue]](hint)逻辑原解释

DefaultValue逻辑.png

再看减法操作,可以看到减法就有一个ToNumber的处理(5、6步)

减法逻辑.png

再看ToNumber,可以发现ToNumber的逻辑是先进行ToPrimitive操作,再返回了基本数据类型后,再进行一次ToNumber操作,返回number类型的最终结果

ToNumber.png

可以简单理解一下这个逻辑

const abc = {}; // 定义一个对象
abc - 1; // 执行逻辑减法:最后值为NaN,
abc + 1; // 执行逻辑加法:最后值为"[object Object]1"

减法分析步骤

  1. 对abc进行ToNumber操作
  2. 判断abc是对象,对abc进行ToPrimitive逻辑处理
  3. [[DefaultValue]](Number)对abc进行处理,判断abc是否有Symbol.toPrimitive对象,如果有就返回,如果没有,优先调用valueOf()
  4. abc.valueOf() 的值为{},继续进行toString()处理
  5. abc.toString() 的值为"[object Object]",返回
  6. 对上一步返回的值再进行ToNumber操作,即等同于Number("[object Object]"),得到值NaN
  7. 最后执行减法,NaN - 1,值为 NaN

加法分析步骤

  1. 对abc进行ToPrimitive操作
  2. 判断abc是对象,对abc进行[[DefaultValue]](default)处理,判断abc是否有Symbol.toPrimitive对象,如果有就返回,如果没有,优先调用valueOf()
  3. abc.valueOf() 的值为{},继续进行toString()处理
  4. abc.valueOf() 的值为{},继续进行toString()处理
  5. abc.toString() 的值为"[object Object]",返回
  6. 逻辑操作已经变成了"[object Object]" + 1,根据加法规则第7条,任意一方是string,则对另一方进行ToString操作,类似String(),所以这里会对1进行String(1)处理,变成"1";
  7. 最后执行字符串追加,结果为"[object Object]1"

关键点:

  1. 可以看到ToPrimitive会对object使用[[DefaultValue]](hint)的转换逻辑,[[DefaultValue]](hint)的处理逻辑其实就是上面OrdinaryToPrimitive方法的内容,简单说来就是hint为string,则toString()优先调用,如果hint是number,则valueOf()优先调用,任意一个调用返回了基本数据类型,就返回,如果都没有基本数据类型返回,则抛出错误
  2. 注意到加法操作很特殊,特别是第七条和第八条,核心解释就是加法操作有任意一方直接就是string或者通过ToPrimitive转换后是string,就会优先执行字符串的追加,否则才会进行ToNumber转换,进行数学加法处理,这也是为什么是以下结果的原因
const abc = {};
const abc2 = NaN;
const abc3 = null;
abc + 1; // "[object Object]1"
abc2 + 1; // NaN,因为abc2判断是number,在ToPrimitive的时候,只有object会进行进一步处理,其他的值都会直接返回,返回后,进行ToNumber处理,参考ToNumber的处理表,number会直接返回值,所以这里是  NaN + 1 = NaN
abc3 + 1 // 1 ,这里也是同样,abc2判断是null,在ToPrimitive的时候,返回了null,判断null的类型不是string,就会进行ToNumber处理,参考ToNumber的处理表,ToNumber会对null处理成+0,所以这里的结果就是 +0 + 1 = 1
  1. 除了ToNumber还有ToString、ToBoolean等,这些都是指代js的转换逻辑,每一个都有一张转换表,对应什么类型做什么样的转换,比如ToNumber遇到number类型就直接返回,遇到对象就就行ToPrimitive逻辑处理,取得了基本数据类型后,再进行一次ToNumber处理,体现到方法就是Number()、Boolean()、String()

其他计算操作符的逻辑

一元操作符

  1. ++UnaryExpression 【ToNumber】
  2. --UnaryExpression 【ToNumber】
  3. +UnaryExpression 【ToNumber】
  4. -UnaryExpression 【ToNumber】
  5. ~UnaryExpression 【ToInt32】
  6. !UnaryExpression 【ToBoolean】

二元操作符

  • MultiplicativeExpression * UnaryExpression
  • MultiplicativeExpression / UnaryExpression
  • MultiplicativeExpression % UnaryExpression
  • / % - 操作,全部都是【ToNumber】处理

唯独只有 + 操作,会先进行ToPrimitive处理,再依据左右两边的值进行字符串拼接或者ToNumber

比较操作符

比较操作符也是ToPrimitive逻辑处理,但是指定了hint为number,这里要注意Date,Date的默认ToPrimitive的hint是string,在作为比较操作的时候,Date会优先进行hint为number的转换

比较操作符逻辑.png

等比较操作符

等比较操作符,重点只关注==,都知道==会做隐式转换,===则不会,所以要关注的就是==的转换规则

==操作转换.png

再贴上中文版的

==操作转换中文.png

这里就可以解释经典的[] == ![] 返回true的疑惑了

  1. 等式右边![] 返回为 false,等式变为[] == false
  2. false通过ToNumber转换,等式变为 [] == 0
  3. 等式左边[]通过ToPrimitive处理,等式变为 "" == 0
  4. 对等式左边进行ToNumber处理,等式变为 0 == 0
  5. 返回true

相关参考:ToPrimitive解释博客1博客2博客3