你真的懂JS隐式类型转换吗?

499 阅读4分钟

先抛出3个问题:

  1. undefined + 10undefined + "10" 分别等于多少?
  1. 输出 2 > {} 结果是多少?
  1. 几种方法让 a == 1 && a == 2 && a == 3 等于 true

这些都是常见的面试题,看似五花八门,但其本质都是面试官在考我们JavaScript中的 隐式类型转换

基本类型值的隐式类型转换

首先要知道的是JS隐式类型转换发生在我们使用 + - * / == < > 这些运算符时,这些运算符只能操作基本数据类型,对于对象类型需要先调ToPrimitive算法转换成基本类型。 我们下面先说基本类型值的隐式类型转换规则

  • + 操作符两边至少有一个值是string类型时,都转为string;否则都转为number;
// 都转为string 1 -> "1"
console.log(1 + "23"); // "123"
// 都转为number,false -> 0
console.log(1 + false); // 1
// 都转为string,false -> "false"
console.log("1" + false); // "1false"
// 都转为number,true -> 1,false -> 0
console.log(true + false); // 1
  • - * / 操作符两边都转为number;
console.log(25 - "23"); // 2
console.log(1 * false); // 0
// Number("a")为NaN
console.log(1 / "a"); // NaN
  • == 操作符两边都转为number;
// true -> 0
console.log(2 == true); // false
// "0" -> 0, false -> 0
console.log("0" == false); // true
// "0" -> 0
console.log("0" == 0) ; // true
  • 对于 > <,全是字母比较按字母顺序,其余的都转换成number在比较;
// 按字母顺序g比f大
console.log("g" > "f"); // true
// 按字母顺序f比g小
console.log("fh" > "ga"); // false
// 转换成number,"12" -> 12
console.log("12" < 13); // true
// 转换成number,false -> 0
console.log(false < -1); // false
// 转换成number,"a" -> NaN, NaN做比较都是false
console.log("a" < 9999); // false

至此我们就可以回答第1个问题了:

// 转换为number类型,undefined -> NaN
console.log(undefined + 10); // NaN
// 转换为string类型,undefined -> "undefined"
console.log(undefined + "10");

ToPrimitive算法

ToPrimitive,是JS中每个值隐含的自带的方法,他可以将任何类型的值转换为基本类型的值。这是一个内部算法,是JS在内部执行时遵循的一套规则。

既然这些运算符只能操作基本数据类型,那必然有能将引用类型转换成基本类型机制,这个机制就是 ToPrimitive 算法。通过此算法将值转换成基本类型后,再进行上节中的基本类型值的比较。

调用ToPrimitive函数时,会被传递一个字符串参数 hint ,表示要转换到的原始值的预期类型。 hint 参数的取值是 "number""string""default" 中的任意一个。

  • 当hint为numbr或default时:

    1. 调用Object的valueOf方法,如果为原始值,则直接返回;否则进行下一步;
    2. 调用Object的toString方法,如果为原始值,则直接返回;否则进行下一步;
    3. 抛出TypeError异常;
  • 当hint为string时:

    1. 调用Object的toString方法,如果为原始值,则直接返回;否则进行下一步;
    2. 调用Object的valueOf方法,如果为原始值,则直接返回;否则进行下一步;
    3. 抛出TypeError异常;

那么hint参数是依据什么规则确定的呢? 有如下几点:

  1. 一般情况下,+ 连接运算符传入的参数是defaul;
  2. 当是 - * / > < == 操作符时,hint为number;
  3. 对于 String(str) 和模版字符串 ${str} 等情况,传入的参数是string;

接下来声明一个对象,手动赋予 Symbol.toPrimitive 属性,再来查看输出结果:

var obj1 = {
  [Symbol.toPrimitive](hint) {
    if (hint == "number") {
      return 10;
    }
    if (hint == "string") {
      return "WEB实习僧";
    }
    if (hint == "default") {
      return 1;
    }
  }
};
console.log(`Hi, ${obj1}`); // "Hi, WEB实习僧" hint参数为string
console.log(obj + 5); // 6 hint参数为default
console.log(obj - 5); // 5 hint参数为number

至此我们就可以回答第2个问题了:

/**
 * 1. {}不是基本数据类型,所以调用ToPrimitive
 * 2. hint为number,首先调用valueOf方法,返回{}
 * 3. {}还不是基本数据类型,调用toString()方法,返回"[object Object]"
 * 4. "[object Object]"是基本类型,结束ToPrimitive
 * 5. 比较2 > "[object Object]",需要都转换为number类型
 * 6. Number("[object Object]")为NaN
 * 7. 比较2 > NaN,返回false,结束
 */
console.log(2 > {}); // false

等等,好像第3个问题也能解决啦?每次进行 == 比较时,都会调用toPrimitive,所以可以在它身上做手脚!

const a = {
  i: 1,
  // 当然替换成valueOf或toString都是可以的
  [Symbol.toPrimitive]() {
    return this.i++
  }
};
if (a == 1 && a == 2 && a == 3) {
  console.log('WEB实习僧'); // WEB实习僧
}

toString与valueOf

  • toString():它的作用是返回一个反映这个对象的字符串;
  • valueOf():它的作用是返回它相应的原始值;

上面说到调用ToPrimitive方法后,会根据hint情况调用toString和valueOf方法来返回最终值,那么toString和valueOf是根据什么规则转化的呢? 下表展示了各种数据类型调用toString和valueOf方法后的输出值:

类型toStringvalueOf
Object返回"[object ObjectName]",其中 ObjectName 是对象类型的名称。对象本身。这是默认情况。
String返回 String 对象的值字符串值。
Number返回数值的字符串表示。数字值。
Boolean如果布尔值是true,则返回"true"。否则返回"false"。Boolean 值。
Array将 Array 的每个元素转换为字符串,并将它们依次连接起来,两个元素之间用英文逗号作为分隔符进行拼接。数组本身
Date返回日期的文本表示。存储的时间是从 1970 年 1 月 1 日午夜开始计的毫秒数 UTC
Function返回如下格式的字符串, "function functionname() { [native code] }"函数本身。

下面看一些示例:

let a = { age: 18 };
let b = [1, 2, 3];
let c = new Date();
let d = '123';

console.log(a.toString()); // "[object Object]"
console.log(b.toString()); // "1,2,3"
console.log(c.valueOf()); // 1651984111386
console.log(d.valueOf()); // 123

关于问题3

当然,让 a == 1 && a == 2 && a == 3true还有很多解法,这里不做详细解释,答案或附在文章结尾。

对于 == 有很多解法,但是对于===呢,没有了隐式类型转换,还有没有可能等于true 我还没想出类~

对于问题3,更多解法如下:

// 方法1: 从vue3的响应式中获取灵感,使用Proxy
let a = new Proxy({ i: 1 }, {
  get(target) {
    return () => target.i++
  }
})

// 方法2: 和方法1同理,也是vue2实现响应式的核心
let a = 1
Object.defineProperty(window, 'a', {
  get() {
    return a++
  }
})

// 方法2: 使用with
let a = 1
with ({
  get a() {
    return i++
  }
}) {
  if (a == 1 && a == 2 && a == 3) {
    console.log('MDN不建议使用with');
  }
}