toFrankie / blog

种一棵树,最好的时间是十年前。其次,是现在。
21 stars 1 forks source link

通过面试题 a == 1 && a == 2 && a == 3,你看到了什么? #292

Open toFrankie opened 1 year ago

toFrankie commented 1 year ago

配图源自 Freepik

一、背景

上周回家路上,逛社区给我推了这样一道「面试题」,要使其结果为 true,如何实现?

a == 1 && a == 2 && a == 3

心想不是很简单嘛,考察的是数据类型转换的知识。自实现一个 @@toPrimitive 方法就行,思路很简单。换句话说:实现这样一个方法,在每次调用时返回值增加 1。这样大家闭眼就能写出来了:

const a = {
  [Symbol.toPrimitive]() {
    if (this._val === undefined) this._val = 0
    this._val++
    return this._val
  }
}

console.log(a == 1 && a == 2 && a == 3) // true

这道题它的属性名 [Symbol.toPrimitive] 比较少用少见,仅此而已。

其中 @@toPrimitive 方法只能通过计算属性 [Symbol.toPrimitive] 的方式去表示。类似的还有很多,比如实现或重写迭代器的 @@iterator 方法表示为 [Symbol.iterator]

二、为什么?

这会该有人跳出来吐槽:实际项目中毫无意义,怎么还有面试官问这种傻逼问题?我尝试换位思考,面试官如何在短时间内判断一个候选人的专业技能?通过这些看似奇葩,但综合性较强的题目来考察不失为一种好方法(这题好像不太强)。

私以为,学好一个知识点,应做到「深入了解,举一反三」。

加个菜,若要使下面同时成立(临时想出来的),你还会做吗?

console.log(a == 1 && a == 2 && a == 3) // true
console.log(+a) // 100
console.log(`${a}`) // 1000

其实也很简单,原因是 @@toPrimitive 方法在执行时,会接收到一个 hint 参数,其值为 defaultstringnumber 之一。然后你又能立刻实现出来了:

const a = {
  [Symbol.toPrimitive](hint) {
    if (hint === 'number') return 100
    if (hint === 'string') return 1000
    if (this._val === undefined) this._val = 0
    this._val++
    return this._val
  },
}

console.log(a == 1 && a == 2 && a == 3) // true
console.log(+a) // 100
console.log(`${a}`) // 1000

私以为,不能做到举一反三,可能是没有完全掌握相关知识。这样的话,应该找时间去学习一下。

三、进一步深入?

在 ECMAScript 标准中,常见数据类型转换操作有这些:

注意,以上这些并不是 JavaScript 里面具体的方法,而是 ECMAScript 标准中的抽象操作(Abstract Operations)。其实不止这些,ECMAScript 标准还有很多转换方法,更多请看 Type Conversion 章节。如果你是第一次阅读 ECMAScript 标准,难免会有种羞涩难懂的感觉,这里推荐阮一峰老师读懂规范一文,或许有帮助。

此前其实已经写过一篇关于 JavaScript 数据类型转换的文章,里面也很详细地介绍了,可移步至:数据类型转换详解。还写下这篇文章其实是想「温故知新」。另外,我想读者可能更喜欢从一个实际问题出发,然后再深入其背后的原理。

就文章开头的题目而言,这里明显就是「ToPrimitive」操作了,给大家看下 ECMAScript 是如何定义(详见):

第一次阅读的时候,也曾感慨「不愧是抽象操作,确实很抽象」,但反复阅读下来之后,也不会很难。

本文将不会逐行逐字进行翻译,此前写那篇文章 数据类型转换详解 已经做了这件事。

ToPrimitive(obj, hint) 为例

  1. 如果 obj 为原始值,直接返回该值。

  2. 如果 obj 为引用值,且含有 @@toPrimitive 方法(必须是函数),然后 a. 若参数 hint 不存在,令 hint"default";若 hint 为字符串,令 hint"string";否则令 hint"number"。 b. 调用 @@toPrimitive 方法,若其返回值为「原始值」,则返回其值,否则抛出 TypeError。

  3. 若参数 hint 不存在,令 hint"number"

  4. hint"string",将会先后调用 objtoString()valueOf() 方法,然后 a. 若 toString 为函数,则调用该方法。若其返回值为「原始值」,则返回该值,否则进行往下。 b. 若 valueOf 为函数,则调用该方法。若其返回值为「原始值」,则返回该值,否则抛出 TypeError。

  5. hint"number",将会先后调用 objvalueOf()toString() 方法,然后

    a. 若 valueOf 为函数,就调用该方法。若其返回值为「原始值」,则返回该值,否则进行往下。 b. 若 toString 为函数,就调用该方法。若其返回值为「原始值」,则返回该值,否则抛出 TypeError。

目前 ECMAScript 内置的对象中,只有 DateSymbol 对象有实现其 @@toPrimitive 方法。因此,其他内置对象无非就是根据 valueOf()toString() 方法,得到其转换结果罢了,所以数据类型转换没那么神秘。

然后,回到文章开头那道题:

a == 1 && a == 2 && a == 3

a 成为一个引用值,比如对象。这时,除了实现 @@toPrimitive 方法,还可以改写其 toString() 方法去达到目的。

const a = {
  toString() {
    if (this._val === undefined) this._val = 0
    this._val++
    return this._val
  }
}

console.log(a == 1 && a == 2 && a == 3) // true

但如果要使成立,只能利用 @@toPrimitive 方法会传入 hint 参数的特点去实现了:

console.log(a == 1 && a == 2 && a == 3) // true
console.log(+a) // 100
console.log(`${a}`) // 1000

也就是

const a = {
  [Symbol.toPrimitive](hint) {
    if (hint === 'number') return 100
    if (hint === 'string') return 1000
    if (this._val === undefined) this._val = 0
    this._val++
    return this._val
  },
}

console.log(a == 1 && a == 2 && a == 3) // true,此时 hint 为 "default"
console.log(+a) // 100,此时 hint 为 "number"
console.log(`${a}`) // 1000,此时 hint 为 "string"

那么,这个 hint 有何规律呢?实在惭愧,我也没太琢磨出来。常见操作的 hint 如下:

const obj = {
  [Symbol.toPrimitive](hint) {
    if (hint === 'number') {
      return 1
    } else if (hint === 'string') {
      return 'string'
    } else {
      return 'default'
    }
  }
}

console.log(+obj)          // 1              hint is "number"
console.log(Number(obj))   // 1              hint is "number"

console.log(`${obj}`)      // "string"       hint is "string"
console.log(String(obj))   // "string"       hint is "string"

console.log(obj + '')      // "default"      hint is "default"
console.log(obj + 1)       // "default1"     hint is "default"

个人猜测:「显式转换」为字符串或数组时,对应为 "string""number",其他情况为 "default"

还有一个略麻烦的方法,在规范中通过 References 中一个一个找到引用此算法的其他操作,然后一层一层去查哪些方法有使用了这种操作,然后总结列出来。

四、其他

此前写过不少与 JavaScript 数据类型相关的文章,如果对此不熟的童鞋,可以看一看:

The end.