16 读代码

Huy大约 4 分钟anonymousnote

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

console.log(['1', '2', '3'].map(parseInt))

代码最终输出 [1, NaN, NaN]

分析原因:

parseInt 的实际上有俩个参数,第二参数取值为进制,即取 2-36。若省略第二个参数,默认为 10 进制;若第二个参数以 0x 开头,则表示 16 进制 0x2F

因此有:

// 转换为:
const num = ['1', '2', '3']
num.map((item, index) => {
  // item1: '1', index: 0 -> 忽略 0 进制,按 10 进制处理
  // item1: '2', index: 1 -> 进制 1 不符合 2-36 进制的范围,即返回 NaN
  // item1: '3', index: 2 -> 进制 2 符合要求,但 2 进制下没有 3,因此依旧返回 NaN
  return parseInt(item, index)
})

进制 0 是按照不存在处理,即忽略该参数,所以进制变为 10 进制;

进制 1 是不存在该进制,因此返回结果为 NaN,如: console.log(['1', '1', '3'].map(parseInt))

返回结果依旧为: [1, NaN, NaN]

函数修改形参,能否影响实参?

function changeArg(x) {
  x = 200
}

let num = 100
changeArg(num)
console.log('changeArg num:', num)

let Obj = { name: '小明' }
changeArg(Obj)
console.log('changeArg Obj:', Obj)

结果为都不变:

changeArg num: 100
changeArg Obj: {name: '小明'}

关键点: 函数参数是赋值传递。先 let 定义 x,而后将参数 num、Obj 赋值给 x。

eslint 建议函数参数不要修改,当做常量。

对象和属性的连续赋值

let a = { n: 1 }
let b = a
a.x = a = { n: 2 }

console.log(a.x)
console.log(b.x)

输出结果为:

console.log(a.x) // undefined
console.log(b.x) // {n: 2}

分析原因:

  1. 在 js 中,连续赋值的执行顺序为从右到左进行的。这意味着右边的表达式先于左边的表达式进行求值和赋值。

    let a = 1
    let b = 2
    let c = 3
    let d
    
    d = c = b = a
    
    • 首先,右侧的表达式 a 被求值,其结果为 1。
    • 然后,右侧的表达式 b = a 被求值,将变量 b 的值设置为 1。
    • 接着,右侧的表达式 c = b 被求值,将变量 c 的值设置为 1。
    • 最后,左侧的表达式 d = c 被求值,将变量 d 的值设置为 1。
  2. a.x 的定义属性比赋值的优先级高,即表达式中a.x = a = { n: 2 },需要先定义 a.x = undefined 再进行赋值计算。

  3. 因此在a.x = a = { n: 2 }这个复合赋值语句中,首先执行 a.x,但此时 a 还是指向对象 { n: 1 },所以将对象 { n: 1 } 的属性 x 设置为 undefined;接着,执行 a = { n: 2 },将变量 a 的引用指向新的对象 { n: 2 };最后将对象 {n: 1, x: undefined}x 属性赋值为对象 { n: 2 }

构造函数和原型的重名属性

function Foo() {
  Foo.a = function () {
    console.log(1)
  }
  this.a = function () {
    console.log(2)
  }
}

Foo.prototype.a = function () {
  console.log(3)
}
Foo.a = function () {
  console.log(4)
}

Foo.a()
const obj = new Foo()
obj.a()
Foo.a()

输出结果为:

Foo.a() // 4
const obj = new Foo()
obj.a() // 2
Foo.a() // 1

promise 执行顺序问题

Promise.resolve()
  .then(() => {
    console.log(0)
    return Promise.resolve(4)
  })
  .then((res) => {
    console.log(res)
  })

Promise.resolve()
  .then(() => {
    console.log(1)
  })
  .then(() => {
    console.log(2)
  })
  .then(() => {
    console.log(3)
  })
  .then(() => {
    console.log(5)
  })
  .then(() => {
    console.log(6)
  })
  .then(() => {
    console.log(7)
  })

输出结果为:

0 1 2 3 4 5 6 7

梳理问题关键:

这里用到了 Promise 的状态吸收,另一个难点就是 Promise 的交替执行。不同的 promise 是“交替执行”,分别插入到微任务队列中。接下来模拟微任务的插入顺序:

  1. [console.log(0), console.log(1)] --> 0
  2. [console.log(1), return Promise.resolve(4)] --> 1
  3. [return Promise.resolve(4), console.log(2)]
  4. [console.log(2), return 4] --> 2
  5. [return 4, console.log(3)] --> 3
  6. [console.log(4), console.log(5)] --> 4 5
  7. ......

再具体一点,对于这个 promise.resolve(4) 的处理,可以拆分为:

// return promise.resolve(4) 等价于
return new Promise((resolve) => {
  resolve(4)
})

// 由于 promise 的状态吸收吸,额外包了一个 promise,等价于
new Promise((resolve) => {
  resolve(
    new Promise((resolve) => {
      resolve(4)
    })
  )
})

// 因此第一轮准备阶段得到 Promise.resolve(4) 的结果 4
// 第二轮吸收 promise 的结果 4, 即 return 4

关于 Promise 的吸收详细讲解可见Promise 的转态吸收

call 和 apply 的链式调用

const log = console.log.call.call.call.call.call.apply(
  (a) => a,
  [1, 2, 3, 4, 5]
)

console.log(log)

这个调用还是比较抽象的,最终输出结果为 2

// 上述代码可以具化成这样:
const fn = (a) => a
const log = console.log.call.call.call.call.call.apply(fn, [1, 2, 3, 4, 5])

// 进一步拆解:
console.log.__proto__ === Function.prototype // true
console.log.prototype === Function.prototype // false

console.log.call === Function.prototype.call // true
console.log.call.call === Function.prototype.call.call
// 因此log 函数等价于
const log1 = console.log.call.apply(fn, [1, 2, 3, 4, 5])

const log2 = Function.prototype.call.apply(fn, [1, 2, 3, 4, 5])

// 再进一步分析 apply 方法
// Fn.apply(thisArg, [arg1, arg2]) ==> thisArg.Fn(arg1, arg2)

// 因此得到:
const log3 = fn.call(1, 2, 3, 4, 5) // 最终运行结果为 2
Loading...