带你深入浅出的理解隐式转换时js计算中的过程

一、基础概念回顾

什么是隐式转换

JavaScript 是一种弱类型或者说动态语言。这意味着你不用提前声明变量的类型,在程序运行过程中,类型会被自动确定。这也意味着你可以使用同一个变量保存不同类型的数据:

var foo = 42;     // foo is a Number now
foo = "bar";     // foo is a String now
foo = true;     // foo is a Boolean now
  • js中,当运算符在运算时,如果两边数据类型不统一,CPU就无法计算,这时我们编译器会自动将运算符两边的数据做一个数据类型转换,转成一样的数据类型再计算。这种无需程序员手动转换,而由编译器自动转换的方式就称为隐式转换。

如:1 + '1' // 执行时不会报错


数据类型

JavaScript 中有7种数据类型,可以分为两类:原始类型、对象类型:

基本数据类型(原始类型):Undefined、 Null、 String、 Number、 Boolean、 Symbol (ES6新增)

复杂数据类型(对象类型):Object


关于 valueOf() 和 toString()

1、valueOf()

默认情况下,valueOf方法由Object后面的每个对象继承。 每个内置的核心对象都会覆盖此方法以返回适当的值。如果对象没有原始值,则valueOf将返回对象本身。

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

2、toString()

每个对象都有一个toString()方法,当该对象被表示为一个文本值时,或者一个对象以预期的字符串方式引用时自动调用。默认情况下,toString()方法被每个Object对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回 "[object type]",其中type是对象的类型。

对象 返回值 是否覆盖自Object的方法
Array 返回一个字符串,表示指定的数组及其元素。
Boolean 返回指定的布尔对象的字符串形式。
Date 返回一个字符串,表示该Date对象。
Function 返回一个表示当前函数源代码的字符串。
Number 返回指定 Number 对象的字符串表示形式。
Object 返回一个表示该对象的字符串。 -
String 返回指定对象的字符串形式。
Symbol symbol 对象的字符串表示。
Error 返回一个指定的错误对象(Error object)的字符串表示。
Math 没有 toString 方法,方法继承自Object。




二、隐式转换规则

隐式转换中主要涉及到三种类型转换:

  1. 转成string类型:
    +(加法运算符,字符串拼接)

  2. 转成number类型:
    ++ --(自增自减运算符) ,+ - * / %(算术运算符)
    > < >= <= == === != !==(关系、比较运算符)

  3. 转成boolean类型:
    && || !(逻辑且或非运算符)


对象通过ToPrimitive获得原始值

将值转为原始值ToPrimitive(),会执行toNumber或者toString的操作

js引擎内部的抽象操作ToPrimitive有着这样的签名:ToPrimitive(input, PreferredType?)

input是要转换的值,PreferredType是可选参数,可以是Number或String类型。它只是一个转换标志,转化后的结果并不一定是这个参数所值的类型,但是转换结果一定是一个原始值(否则报错)。


  • 如果PreferredType被标记为Number,则会进行下面的操作流程来转换输入的值。
  1. 如果输入的值已经是一个原始值,则直接返回它
  2. 否则,如果输入的值是一个对象,则调用该对象的valueOf()方法,如果valueOf()方法的返回值是一个原始值,则返回这个原始值。
  3. 否则,调用这个对象的toString()方法,如果toString()方法返回的是一个原始值,则返回这个原始值。
  4. 否则,抛出TypeError异常。

  • 如果PreferredType被标记为String,则会进行下面的操作流程来转换输入的值。
  1. 如果输入的值已经是一个原始值,则直接返回它
  2. 否则,调用这个对象的toString()方法,如果toString()方法返回的是一个原始值,则返回这个原始值。
  3. 否则,如果输入的值是一个对象,则调用该对象的valueOf()方法,如果valueOf()方法的返回值是一个原始值,则返回这个原始值。
  4. 否则,抛出TypeError异常。

  • 既然PreferredType是可选参数,那么如果没有这个参数时,怎么转换呢?PreferredType的值会按照这样的规则来自动设置:
  1. 该对象为Date类型,则PreferredType被设置为String
  2. 否则,PreferredType被设置为Number


来看下面的表格

toPrimitive Number String Boolean
false 0 “false” false
true 1 “true” true
0 0 “0” false
1 1 “1” true
“0” 0 “0” true
“1” 1 “1” true
NaN NaN “NaN” false
Infinity Infinity “Infinity” true
-Infinity -Infinity “-Infinity” true
“” 0 “” false
“ “ 0 “ “ true
“2” 2 “2” true
“two” NaN “two” true
[ ] 0 “” true
[0] 0 “0” true
[1,2] NaN “1,2” true
[“one”] NaN “one” true
[“one”,”two”] NaN “one,two” true
function(){} NaN “function(){}” true
{ } NaN “[object Object]” true
null 0 “null” false
undefined NaN “undefined” false


三、举几个栗子

1、比较运算符

在 ECMAScript 中,等号由双等号(==)表示,当且仅当两个运算数相等时,它返回 true。
非等号由感叹号加等号(!=)表示,当且仅当两个运算数不相等时,它返回true。
为确定两个运算数是否相等,这两个运算符都会进行类型转换。

执行类型转换的规则如下:
如果一个运算数是 Boolean值,在检查相等性之前,把它转换成数字值。
如果一个运算数是字符串,另一个是数字,在检查相等性之前,要尝试把字符串转换成数字。
如果一个运算数是对象,另一个是字符串,在检查相等性之前,要尝试把对象转换成字符串。
如果一个运算数是对象,另一个是数字,在检查相等性之前,要尝试把对象转换成数字。

在比较时,该运算符还遵守下列规则:
值 null 和 undefined 相等。
在检查相等性时,不能把 null 和 undefined 转换成其他值。
如果某个运算数是 NaN,等号将返回 false,非等号将返回 true。
如果两个运算数都是对象,那么比较的是它们的引用值。
如果两个运算数指向同一对象,那么等号返回 true,否则两个运算数不等。
[] == 0;        true    // [] => '' => 0
![] == 0;       true    // ![] => false => 0

[] == ![];      true    // [] => '' => 0 , ![] => false => 0
[] == [];       false   // 对比的是它们的引用值

{} == !{}       false   // {} => '[object Object]' => NaN ,
                        // !{} => false => 0
{} == {}        false   // 对比的是它们的引用值

如何使(a == 1 && a == 2 && a == 3)成立?

1.转换原始值

const a = {
  i: 1,
  valueOf: function () {
    return a.i++;
  },
  toString: function () {
    return a.i++;
  }
}

if (a == 1 && a == 2 && a == 3) {
  console.log('hello world!');
}

//a先会调用valueOf()方法,如有原始值则返回这个原始值
//否则,调用这个对象的toString()方法
//所以改写其中任一方法均可

2.闭包

let a = {
  [Symbol.toPrimitive]: (function() {
    let i = 1;
    return function() {
      return i++;
    }
  })()
}

3.Object.defineProperty

let val = 1;
Object.defineProperty(window, 'a', {
  get: function() {
    return val++;
  }
});

4.数组

var a = [1,2,3];
a.join = a.shift;
// 数组的 toString 方法返回一个字符串,
// 该字符串由数组中的每个元素的 toString() 返回值经调用 join() 方法连接(由逗号隔开)组成。

这么坑的面试题,你咋不上天呢?

特殊情况(无视规则):如果数据是undefined 、 null 、 NaN , 得出固定结果

在检查相等性时,不能把 null 和 undefined 转换成其他值。(重要)

undefined == undefined          // true
undefined === undefined         // true
undefined == null               // true
null == null                    // true
null === null                   // true
null == 0                       // false   null没有转换

NaN == NaN                      // false
NaN === NaN                     // false
NaN == null                     // false
NaN == undefined                // false


2、关系运算符

"2" > 10;       // false    2 < 10
"2" > "10";     // true     比较的是Unicode
"abc" > "b";    // false    比较的是Unicode
"abc" > "abd"   // true     比较的是Unicode,第一个相等且还有后续就对比第二个,
                //          出大小的结果就终止对比了


3、字符串连接符与算术运算符

1 + "true";     // "1true"
1 + true;       // 2
1 + undefined;  // NaN
1 + null;       // 1


等等,是不是忘了什么?

四、 symbol

在 JavaScript 中,虽然大多数类型的对象在某些操作下都会自动的隐式调用自身的 valueOf() 方法或者 toString() 方法来将自己转换成一个原始值,但 symbol 对象不会这么干,symbol 对象无法隐式转换成对应的原始值:

1
2
3
4
5
6
7
8
9
10
11
12
Object(Symbol("foo")) + "bar";
// TypeError: can't convert symbol object to primitive
// 无法隐式的调用 valueOf() 方法

Object(Symbol("foo")).valueOf() + "bar";
// TypeError: can't convert symbol to string
// 手动调用 valueOf() 方法,虽然转换成了原始值,但 symbol 原始值不能转换为字符串

Symbol("foo").toString() + "bar"
// "Symbol(foo)bar",就相当于下面的:
Object(Symbol("foo")).toString() + "bar";
// "Symbol(foo)bar",需要手动调用 toString() 方法才行

换句话说,在 Symbol.toPrimitive() 方法内部判断了值类型,根据类型进行后续不同的操作,而不是简单的调用 toString() & valueOf()方法,对于 Symbol 类型,它的处理就是抛出异常。


资料查阅:MDN 、 w3school