Skip to content
大纲

代码分析

满足条件 a == 1 && a == 2 && a == 3

下面代码中 a 在什么情况下会打印 1

js
// var a = ?;
if (a == 1 && a == 2 && a == 3) {
  console.log(1)
}
  • 考察类型的隐式转换
  • 引用类型在比较运算(==)时,隐式转换会调用本类型的 toString/valueOf 方法
js
// code1
var a = [1, 2, 3]
a.toString = a.shift
if (a == 1 && a == 2 && a == 3) {
  console.log(1)
}
js
// code2
var a = { num: 0 }
a.valueOf = function () {
  return ++a.num
}
js
// code3
var a = {
  i: 1,
  toString() {
    return a.i++
  }
}
js
// code4
// prettier-ignore
var a = {
  [Symbol.toPrimitive]: ((i) => () => ++i)(0)
}
js
// code5
var a = {
  gn: (function* () {
    yield 1
    yield 2
    yield 3
  })(),
  valueOf() {
    return this.gn.next().value
  }
}
js
// code6
Object.defineProperty(window, 'a', {
  get: function () {
    if (this.value) {
      return (this.value += 1)
    } else {
      return (this.value = 1)
    }
  }
})
规范

3.5.7 相等操作符

确定两个变量是否相等是编程中的一个非常重要的操作。在比较字符串、数值和布尔值的相等性时,问题还比较简单。但在涉及到对象的比较时,问题就变得复杂了。最早的 ECMAScript 中的相等和不等模作符会在执行比较之前,先将对象转换成相似的类型。后来,有人提出了这种转换到底是否合理的质疑。最后,ECMAScript 的解决方案就是提供两组操作符:

  • 相等不相等——先转换再比,

  • 全等不全等——比较而不转换。

    1. 相等和不相等

    ECMASctipt 中的相等模作符由两个等于号(==)表示,如果两个作数相等,则返回 tue。而不相等模作符由号后跟等于号(与)表如果两人提作数不相等,则返回 u。这两人模作符都会先转换模作数(通常称为强制转型),然后再比软它们的相等件

    在转换不同的数据类型时,相等和不相等操作符遵循下列基本规则:

    • 如果有一个操作数是布尔值,则在比较相等性之前先将其转换为数值 false 转换为 0,而 true 转换为 1;
      • 如果一个操作数是字符串,另一个操作数是数值,在比较相等性之前先将字符串转换为数值;
    • 如果一个操作数是对象,另一个操作数不是,则调用对象的 valueOf() 方法,用得到的基本类型值按照前面的规则进行比较;

实现 (5).add(3).minus(2) 功能

js
Number.prototype.add = function (i = 0) {
  return this.valueOf() + i
}
Number.prototype.minus = function (i = 0) {
  return this.valueOf() - i
}

这里未考虑 JS 经典的浮点数陷阱

这里 github 上有一个阿里大佬的解法-JavaScript 浮点数陷阱及解法

注意

大数加减:直接通过 Number 原生的安全极值来进行判断,超出则直接取安全极值

超级多位数的小数加减:取 JS 安全极值位数-2 作为最高兼容小数位数。

实现 LazyMan('Tony').eat('lunch').sleepFirst(5)

要求设计 LazyMan 类,实现以下功能

js
LazyMan('Tony')
// Hi I am Tony

LazyMan('Tony').sleep(10).eat('lunch')
// Hi I am Tony
// 等待了 10 秒...
// I am eating lunch

LazyMan('Tony').eat('lunch').sleep(10).eat('dinner')
// Hi I am Tony
// I am eating lunch
// 等待了 10 秒...
// I am eating dinner

LazyMan('Tony')
  .eat('lunch')
  .eat('dinner')
  .sleepFirst(5)
  .sleep(10)
  .eat('junk food')
// Hi I am Tony
// 等待了 5 秒...
// I am eating lunch
// I am eating dinner
// 等待了 10 秒...
// I am eating junk food

实现

点我查看详细
js
class LazyManClass {
  constructor(name) {
    this.name = name
    this._queue = []
    console.log(`Hi I am ${name}`)

    // 把 this.next() 放到调用栈清空之后执行
    setTimeout(() => {
      this.next()
    }, 0)
  }

  sleepFirst(time) {
    this._sleepWrapper(time, true)
    return this // 链式调用
  }

  sleep(time) {
    this._sleepWrapper(time, false)
    return this
  }

  eat(food) {
    const fn = () => {
      setTimeout(() => {
        console.log(`I am eating ${food}`)
        this.next()
      }, 0)
    }
    this._queue.push(fn)
    return this
  }

  _sleepWrapper(time, first) {
    const task = = () => {
      setTimeout(() => {
        console.log(`等待了 ${time} 秒...`)
        this.next()
      }, time * 1000)
    }
    if (first) {
      this._queue.unshift(task)
    } else {
      this._queue.push(task)
    }
    return this;
  }

  next() {
    const fn = this._queue.shift()
    fn && fn()
  }
}

function LazyMan(name) {
  return new LazyManClass(name)
}

['1', '2', '3'].map(parseInt)返回值

先思考下结果,答案在最后

解析

  • map 是传入的函数是有 3 个参数的: value, index, arr
  • 而 parseInt 有两个参数 parseInt(string, radix)
md
string
要被解析的值。如果参数不是一个字符串,则将其转换为字符串(使用 toString 抽象操作)。字符串开头的空白符将会被忽略。

radix 可选
从 2 到 36,表示字符串的基数。例如指定 16 表示被解析值是十六进制数。请注意,10 不是默认值!

所以 ['1', '2', '3'].map(parseInt) 的过程是这样子的:

js
parseInt('1', 0) // radix是0的情况见如下解释
parseInt('2', 1) // radix基数只能取到 2 - 36 之间, 所以NaN
parseInt('3', 2) // radix=2 表示是二进制数, 只能有0和1, 解析的字符串是'3', 所以是NaN

解释

md
如果 radix 是 undefined、0 或未指定的,JavaScript 会假定以下情况:

1. 如果输入的 string 以 "0x"或 "0x"(一个 0,后面是小写或大写的 X)开头,那么 radix 被假定为 16,字符串的其余部分被当做十六进制数去解析。
2. 如果输入的 string 以 "0"(0)开头,radix 被假定为 8(八进制)或 10(十进制)。具体选择哪一个 radix 取决于实现。ECMAScript 5 澄清了应该使用 10 (十进制),但不是所有的浏览器都支持。因此,在使用 parseInt 时,一定要指定一个 radix。
3. 如果输入的 string 以任何其他值开头,radix 是 10 (十进制)。

答案: 返回值为 [1, NaN, NaN]

关于执行顺序

神说要有光,发了一篇公众号,提到一道赋值面试题,如下

js
let a = { n: 1 }
a.x = a = { n: 2 }
console.log(a.x)

// 输出什么

有点和以前理解的不一致,如下

js
// prettier-ignore
let a = b = 10
console.log(a) // 10

// 实际开启格式化,会变成下面这样
// 因运算符结合性,运算机制加上括号更直观
let a = (b = 10)

// 继续看,如下仍然有结合性的机制在,不同于最上面的例子
let b = { n: 1 }
let a = (b = { n: 2 })
console.log(a)

继续查了下,这是个老题了,参见JS 中一个赋值的问题

继续看,这里有个解释 由 ES 规范学 JavaScript(二):深入理解“连等赋值”问题

最后,我们再来看神光的解释——手写 JS 引擎来解释一道赋值面试题