[JS]从隐式转换到闲聊

790 阅读6分钟

本文涉及ES6规格文档, 看完后也许会打开一个新世界.

前言

相信大部分准备提桶的JSer对下面这道题目都不陌生, 也都知道这题涉及到了隐式转换, 那么什么是隐式转换, 除了重写toString()方法外还有其他解法吗? 对这些感兴趣的话, 就和我一起入坑吧.

/// 题目
var a = ?;
if(a == 1 && a == 2 && a == 3){
    console.log(1);
}
/// 其中一个答案
var a = {
  i: 1,
  toString() {
    return a.i++;
  }
}

什么是隐式转换

要了解什么是隐式转换还得先从==运算符的处理逻辑开始说, 那么从哪了解呢? 简单, 遇事不决量子力学那当然是直接翻 ECMAScript 规格文档了, 先定位到 ==运算符的规格定义, 内容如下:

The comparison x == y, where x and y are values, produces true or false. Such a comparison is performed as follows:

ReturnIfAbrupt(x).
ReturnIfAbrupt(y).
If Type(x) is the same as Type(y), then
    Return the result of performing Strict Equality Comparison x === y.
If x is null and y is undefined, return true.
If x is undefined and y is null, return true.
If Type(x) is Number and Type(y) is String,
    return the result of the comparison x == ToNumber(y).
If Type(x) is String and Type(y) is Number,
    return the result of the comparison ToNumber(x) == y.
If Type(x) is Boolean, return the result of the comparison ToNumber(x) == y.
If Type(y) is Boolean, return the result of the comparison x == ToNumber(y).
If Type(x) is either String, Number, or Symbol and Type(y) is Object, then
    return the result of the comparison x == ToPrimitive(y).
If Type(x) is Object and Type(y) is either String, Number, or Symbol, then
    return the result of the comparison ToPrimitive(x) == y.
Return false.

总结一下就是, 假设==运算符左右两边的操作符分别为x, y.

  1. 如果x,y的类型相同, 按照x === y的结果返回. // ===运算符的规格定义, 这个有兴趣的自己看下.
  2. 如果x,y类型不同
    • 2.1 如果x,y中一方是String, 另一方是Number, 将String类型的转成Number再比较
    • 2.2 如果x,y中有一方是Boolean类型, 将Boolean转成Number后再比较 // Number(true) -> 1; Number(false) -> 0
    • 2.3 如果x,y中有一方是Object类型, 另一方的类型是[String, Number, or Symbol]中的一个, 需要调用ToPrimitive()将Object转为基本类型后再比较.
  3. 如果以上都不满足, 返回false

从以上结论可推测出==运算符是会存在递归调用的. 好了回归主题, 上面的内容中出现了一个ToPrimitive(), 这是啥东西, 从方法名上看是一个将目标值转成原始类型的操作, 隐式转换难不成就是由他来执行的?

ToPrimitive()

找到ToPrimitive()的ES规范, 内容如下:

ToPrimitive ( input [, PreferredType] )

The abstract operation ToPrimitive takes an input argument and an optional argument PreferredType. The abstract operation ToPrimitive converts its input argument to a non-Object type. If an object is capable of converting to more than one primitive type, it may use the optional hint PreferredType to favour that type. Conversion occurs according to Table 9

总结起来就是将输入的参数转化成一个基本类型的数据, 也就是说Object类型在这里会进行隐式转换被转换成一个基本类型. 基本类型会直接返回本身. 顺道提一下JS里的基本类型是这几个: Undefined, Null, Boolean, Number, String, Symbol(ES6新增).

Object 隐式转换过程

摘抄规格中的关键部分如下, 里面有个关键字hint就是ToPrimitive ( input [, PreferredType] )的第二个参数PreferredType

When Type(input) is Object, the following steps are taken:

If PreferredType was not passed, let hint be "default".
Else if PreferredType is hint String, let hint be "string".
Else PreferredType is hint Number, let hint be "number".
Let exoticToPrim be GetMethod(input, @@toPrimitive).
.
.
.
If hint is "string", then
    Let methodNames be «"toString", "valueOf"».
Else,
    Let methodNames be «"valueOf", "toString"».
For each name in methodNames in List order, do
    Let method be Get(O, name).
    ReturnIfAbrupt(method).
    If IsCallable(method) is true, then
        Let result be Call(method, O).
        ReturnIfAbrupt(result).
        If Type(result) is not Object, return result.
Throw a TypeError exception.

Note: When ToPrimitive is called with no hint, then it generally behaves as if the hint were Number. However, objects may over-ride this behaviour by defining a @@toPrimitive method. Of the objects defined in this specification only Date objects (see 20.3.4.45) and Symbol objects (see 19.4.3.4) over-ride the default ToPrimitive behaviour. Date objects treat no hint as if the hint were String.

总结一下就是:

  1. 先看Object上有没有@@toPrimitive这个方法, 如果有的话就先调用它来获取结果, 若未返回了期待的基本类型则继续往下走, @@toPrimitive这个看上去很怪的东西等下再讲.

    • 2.1 如果'hint'参数是'string', 则按顺序调用Object上的toString(), valueOf()方法, 其中一个返回了基本类型就结束
    • 2.2 如果'hint'参数是'number', 则按顺序调用Object上的valueOf(), toString(), 方法, 其中一个返回了基本类型就结束
  2. 咋搞, 我不知道怎么转换啊, 只能给你抛个前端很熟悉的TypeError异常了.

还有一个注意点是, 隐式转换默认的hintnumber类型, 是会按照2.2的规则执行的, 对于存在@@toPrimitive方法的对象, 会先根据这个方法转换. ES规格里默认重写了Date对象的这个方法, 所以new Date()才会返回一串表示时间的字符.

@@toPrimitive 是什么?

同样, 先找到对应的规格说明, 关键内容摘抄一句如下.

The value of the name property of this function is "[Symbol.toPrimitive]".

A method that converts an object to a corresponding primitive value. Called by the ToPrimitive abstract operation.

关于Date对象上对该方法的重写参考此链接, 默认转换成string, 也就是会先调用toString()方法.

(new Date()).toString() ///'Wed Aug 31 2022 13:35:54 GMT+0800 (China Standard Time)'

上面的摘抄内容中提到, 可以重写[Symbol.toPrimitive]来参与隐式转换的过程, 所以举个🌰:

let a = {
    i: 1,
    [Symbol.toPrimitive]: () => {
        console.log(a.i)
        return a.i++
    }
}
a == 1; // true

此题的其他解法

扯了这么多, 相信你对隐式转换已经有了一定的了解, 现在提供一下该面试题的其他解法:

/// 由于Object默认`hint`为`number`, 所以可以valueOf()先执行, 可以在这里直接返回
let a = {
    i: 1,
    valueOf(){
    	return a.i++
    }
}
if(a == 1 && a == 2 && a == 3){
    console.log(1);
}
/// [Symbol.toPrimitive]的优先级比valueOf(), toString()都高
let a = {
    i: 1,
    [Symbol.toPrimitive]: () => {
        return a.i++
    }
}
if(a == 1 && a == 2 && a == 3){
    console.log(1);
}

继续扯点别的

  1. Object隐式转换默认都会返回,'[object Object]', 是不是很熟悉.以后不要再惊讶这鬼东西哪来的了.
  2. 执行 {}.toString(), 按照上面讲的, 你是不以为也会返回'[object Object]', 然而大出所料的是, 这一句竟然报错了! Uncaught SyntaxError: Unexpected token '.'
  • 为什么呢, 看上去没问题啊, JS引擎出错了吗? 这里就涉及到JS左值运算符了, {}在这个地方是被当做一个闭包处理的, 对闭包调用toString()自然会报错...
  • 咋解决呢? ({}).toString(); 这样子就好了, 这时候{}就会被正确当做Object处理了.
  1. iPhone上的所有浏览器都是webkit内核, 所以iPhone上的所有浏览器都是Safari换皮, iPhone的Chrome也是哦.

参考链接

ES规格文档
{}.toString()报错解答