Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
toFrankie opened this issue Feb 26, 2023 · 0 comments
Open
Labels
2022 2022 年撰写 JS 与 JavaScript、ECMAScript 相关的文章

Comments

@toFrankie
Copy link
Owner

toFrankie commented Feb 26, 2023

配图源自 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.

@toFrankie toFrankie added the JS 与 JavaScript、ECMAScript 相关的文章 label Feb 26, 2023
@toFrankie toFrankie added the 2022 2022 年撰写 label Apr 26, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
2022 2022 年撰写 JS 与 JavaScript、ECMAScript 相关的文章
Projects
None yet
Development

No branches or pull requests

1 participant