Advanced-Frontend / Daily-Interview-Question

我是依扬(木易杨),公众号「高级前端进阶」作者,每天搞定一道前端大厂面试题,祝大家天天进步,一年后会看到不一样的自己。

Home Page:https://muyiy.cn/question/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

第 160 题:输出以下代码运行结果,为什么?如果希望每隔 1s 输出一个结果,应该如何改造?注意不可改动 square 方法

yygmind opened this issue · comments

const list = [1, 2, 3]
const square = num => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(num * num)
    }, 1000)
  })
}

function test() {
  list.forEach(async x=> {
    const res = await square(x)
    console.log(res)
  })
}
test()
commented

forEach是不能阻塞的,默认是请求并行发起,所以是同时输出1、4、9。

串行解决方案:

async function test() {
  for (let i = 0; i < list.length; i++) {
    let x = list[i]
    const res = await square(x)
    console.log(res)
  }
}

当然,也可以用 for of 语法,就是帅:

async function test() {
  for (let x of list) {
    const res = await square(x)
    console.log(res)
  }
}

还有一个更硬核点的,也是 axios 源码里所用到的,利用 promise 本身的链式调用来实现串行。

let promise = Promise.resolve()
function test(i = 0) {
  if (i === list.length) return
  promise = promise.then(() => square(list[i]))
  test(i + 1)
}
test()

一秒后同时输出 1、4、9

如果要每隔一秒输出把 forEach 换成普通 for 循环或者 for...of... 循环即可

这里并行进行是因为 forEach 实现的问题,源码里用 while 来一次性执行了所有回调

具体参考官网 polyfill: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach

forEach是不能阻塞的,默认是请求并行发起,所以是同时输出1、2、3。

串行解决方案:

async function test() {
  for (let i = 0; i < list.length; i++) {
    let x = list[i]
    const res = await square(x)
    console.log(res)
  }
}

同时输出的是 1、2、3 的平方也就是 1 、4、9

async function test() {
  var n = list.length;

    while(n--) {
        const res = await s(list[n]);
        console.log(res);
    }
}
每隔一秒输出 9 4 1

一秒后同时输出 1、4、9

如果要每隔一秒输出把 forEach 换成普通 for 循环或者 for...of... 循环即可

这里并行进行是因为 forEach 实现的问题,源码里用 while 来一次性执行了所有回调

具体参考官网 polyfill: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach

你这个用 while 来一次性执行了所有回调描述不太准确吧,普通的for 等于同一个块作用域连续await,而forEach的回调是一个个单独的函数,跟其他回调同时执行,互不干扰

function test() {
    list.forEach(async x=> {
      const res = await square(x)
      console.log(res)
    })

    //forEach循环等于三个匿名函数;
    (async (x) => {
        const res = await square(x)
        console.log(res)
    })(1);
    (async (x) => {
        const res = await square(x)
        console.log(res)
    })(2);
    (async (x) => {
        const res = await square(x)
        console.log(res)
    })(3);

    // 上面的任务是同时进行
  }

  async function test() {
    for (let x of list) {
      const res = await square(x)
      console.log(res)
    }
  }
  //等价于

  async function test() {
      const res = await square(1)
      console.log(res)
      const res2 = await square(2)
      console.log(res)
      const res3 = await square(3)
      console.log(res)
  }

@sl1673495 窃以为您的第三个串行方案的第四行代码后面应该加上.then(res => console.log(res))

《ES6 入门》中还提到了使用 reduce 解决:

list.reduce(async (_, x) => {
  await _
  const res = await square(x)
  console.log(res)
}, undefined)

直接在forEach 里面套个setTimeOut 不就可以了

function test() {
list.forEach(async x => {
setTimeout(async () => {
const res = await square(x)
console.log(res)
}, 1000 * x)
})
}

《ES6 入门》中还提到了使用 reduce 解决:

list.reduce(async (_, x) => {
  await _
  const res = await square(x)
  console.log(res)
}, undefined)

await _ 这个怎么理解呀 大哥

《ES6 入门》里面是这样介绍的:
reduce 方法的第一个参数是 async 函数,导致该函数的第一个参数是前一步操作返回的 Promise 对象,所以必须使用await等待它操作结束

如果没有 await _ 这一行,得到的结果和 forEach() 是一样的,可以理解为 @linsicong003 所说的:源码里用 while 来一次性执行了所有回调

添加一行打印:

function test() {
  list.reduce(async (_, x) => {
    console.log(_, x) // 打印
    await _
    const res = await square(x)
    console.log(res)
  }, undefined)
}

test()

打印结果:

马上打印:
undefined 1
Promise { <pending> } 2
Promise { <pending> } 3

每隔一秒打印:
1
4
9

为什么大厂面试的题都这么蹊跷???

commented
const list = [1, 2, 3]
const square = num => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(num * num)
    }, 1000)
  })
}
function test() {
  ;(async () => {
    for await (let num of list) {
      const res = await square(num)
      console.log(res)
    }
  })()
}
test()
const list = [1, 2, 3]
const square = num => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(num * num)
        }, 1000)
    })
}

function test() {
    list.forEach(x=> {
        setTimeout(async x => {
            const res = await square(x)
            console.log(res)
        }, x*1000, x)
    })
}
test()
commented
function test() {
 list.reduce(
   (pre, cur) => pre.then(() => square(cur)).then(console.log),
   Promise.resolve()
 );
}

test();
// 解法一,for循环

async function test() {
  for (let i = 0, len = list.length; i < len; i++) {
    const res = await square(list[i])
    console.log(res)
  }
}
// 解法2 for of循环
async function test() {
  for (x of list) {
    const res = await square(x)
    console.log(res)
  }
}
// 解法3 类似koa里面的compose**解决
async function test() {
  const compose = (middleware, next) => {
    let index = -1
    return (ctx) => {
      const dispatch = (i) => {
        if (index < i ) {
          index = i
        }
        let fn = middleware[ index ]

        if (index === middleware.length) {
          fn = next
        }

        if (!fn) {
          return Promise.resolve()
        }

        return Promise.resolve(fn(ctx, async () => {
          return dispatch(index + 1)
        }))
      }

      dispatch(0)
    }
  }
  
  const middleware = []
  list.forEach((it) => {
    middleware.push(async (ctx, next) => {
      const res = await square(it)
      console.log(res)
      await next()
    })
  })

  compose(middleware, () => {
    console.log('end')
  })({ name: 'qianlongo' })
}

// 解法4 next** 也是koa1的中间件执行**

async function test() {
  let middleware = []

  list.forEach((it) =>{
    middleware.push(async (cb) => {
      const res = await square(it)
      console.log(res)
      cb && cb()
    })
  })
  
  const bindNext = (cbs) => {
    let next = function () {
      console.log('111')
    }
    let len = cbs.length

    while (len--) {
      next = cbs[ len ].bind(null, next)
    }

    return next
  }

  bindNext(middleware)()
}

// 解法5 利用promise的链式调用
function test() {
  let promise = Promise.resolve()

  list.forEach(x => {
    promise = promise.then(() => square(x)).then((res) => {
      console.log(res)
    })
  })
}


test()
commented

题目本身有些问题,定时功能实现 square 中需要进行误差修正的

forEach的回调是一个个单独的函数,跟其他回调同时执行

精品

《ES6 入门》中还提到了使用 reduce 解决:

list.reduce(async (_, x) => {
  await _
  const res = await square(x)
  console.log(res)
}, undefined)

大哥 这里面都没有return 为什么await _ (_) 会是上一次的方法呢 怎么累积的呢 ???
传统的 不都是这样的吗
var sum = [0, 1, 2, 3].reduce(function (accumulator, currentValue) {
return accumulator + currentValue;
}, 0);
能帮忙解答下吗 迷茫。。。。。

commented

forEach是不能阻塞的,默认是请求并行发起,所以是同时输出1、4、9。

串行解决方案:

async function test() {
  for (let i = 0; i < list.length; i++) {
    let x = list[i]
    const res = await square(x)
    console.log(res)
  }
}

当然,也可以用 for of 语法,就是帅:

async function test() {
  for (let x of list) {
    const res = await square(x)
    console.log(res)
  }
}

还有一个更硬核点的,也是 axios 源码里所用到的,利用 promise 本身的链式调用来实现串行。

let promise = Promise.resolve()
function test(i = 0) {
  if (i === list.length) return
  promise = promise.then(() => square(list[i]))
  test(i + 1)
}
test()

promise的实现根本没有输出呢

commented

《ES6 入门》中还提到了使用 reduce 解决:

list.reduce(async (_, x) => {
  await _
  const res = await square(x)
  console.log(res)
}, undefined)

大哥 这里面都没有return 为什么await _ (_) 会是上一次的方法呢 怎么累积的呢 ???
传统的 不都是这样的吗
var sum = [0, 1, 2, 3].reduce(function (accumulator, currentValue) {
return accumulator + currentValue;
}, 0);
能帮忙解答下吗 迷茫。。。。。

因为async函数的返回值是 Promise 对象

const asyncFunc = async () => {}
console.log(asyncFunc()) // Promise {<resolved>: undefined}
commented

一道题可以导出好多知识点,受教了各位大佬

const list = [1, 2, 3]
const square = num => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(num * num)
    }, 1000)
  })
}
//迭代器实现
async function test () {
  var iter = list[Symbol.iterator]();
  var flag = iter.next();
  while (!flag.done) {
    await square(flag.value).then(res => console.log(res));
    flag = iter.next();
  }
}
test();

forEach大概可以这么理解

Array.prototype.forEach = function (callback) {
  // this represents our array
  for (let index = 0; index < this.length; index++) {
    // We call the callback for each entry
    callback(this[index], index, this)
  }
}

所以是1秒后输出1, 4, 9; 几乎同时;但是有调用先后顺序;
改进的话, 大家都答了, 我就不嫌丑了...

commented

我以为我会,我答出来了,过来一看,我还是太年轻了😅你们都是大佬

commented
const list = [1, 2, 3];
const square = num => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(num * num)
    }, 1000)
  })
}
async function test() {
  for (let x of list) {
    const res = await square(x)
    console.log(res)
  }
}
test()

《ES6 入门》中还提到了使用 reduce 解决:

list.reduce(async (_, x) => {
  await _
  const res = await square(x)
  console.log(res)
}, undefined)

大哥 这里面都没有return 为什么await _ (_) 会是上一次的方法呢 怎么累积的呢 ???
传统的 不都是这样的吗
var sum = [0, 1, 2, 3].reduce(function (accumulator, currentValue) {
return accumulator + currentValue;
}, 0);
能帮忙解答下吗 迷茫。。。。。

console.log((async ()=>{})()) // 函数执行返回的是一个 Promise
commented
function test() {
 async function next(list,index=0){
     if(index===list.length)return;
    const res = await square(list[index])
    next(list,index+1)
  }
  next(list)
}
const list = [1, 2, 3]
const square = num => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(num * num)
    }, 1000)
  })
}

function test() {
  list.forEach(async x=> {
    const res = await square(x)
    setTimeout(() => console.log(res), (x - 1) * 1000)    
  })
}
test()

打印结果是一秒后直接打印 1 4 9
原因:forEach方式是不能阻塞的,在一瞬间执行完之后会同时产生3个SetTimeout任务,然后再一秒后同时打印

解决思路:将结果保存在Promise中,用then的方式依次调用

function test() {
  let p = Promise.resolve();
  list.forEach((index) => {
    p = p.then(res => {
      return square(index).then(res => {
        console.log(res)
      })
    })
  });
}
const list = [1, 2, 3]
const square = num => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(num * num)
    }, 1000)
  })
}

function test() {
await Promises.all(list.forEach(async x=> {             /* Promises.all takes all promises and return them when they all resolve */
    const res = await square(x)
    console.log(res)
  }));
}
test()`

forEach是不能阻塞的,默认是请求并行发起,所以是同时输出1、4、9。

串行解决方案:

async function test() {
  for (let i = 0; i < list.length; i++) {
    let x = list[i]
    const res = await square(x)
    console.log(res)
  }
}

当然,也可以用 for of 语法,就是帅:

async function test() {
  for (let x of list) {
    const res = await square(x)
    console.log(res)
  }
}

还有一个更硬核点的,也是 axios 源码里所用到的,利用 promise 本身的链式调用来实现串行。

let promise = Promise.resolve()
function test(i = 0) {
  if (i === list.length) return
  promise = promise.then(() => square(list[i]))
  test(i + 1)
}
test()

第二个方法忘记输出了 因为 square 返回一个 promise 所以可以这样输出:

promise = promise.then(() => square(list[i]))
// 修改为
promise = promise.then(() => square(list[i]).then(num => console.log(num)))

Promise 链

function test() {
    let promise = Promise.resolve();
    const start = Date.now();
    for (let i = 0; i < list.length; i++) {
        promise = promise.then(() => {
            const res = square(list[i]);
            return res;
        }).then((value) => {
            console.log(`${Date.now() - start}`, value)
        });
    }
}
commented
function test() {
  const newlist = list.map(item => square(item))
  let index = arguments[0] || 0
  if (index === newlist.length) {
    return
  }
  newlist[index].then(res => {
    console.log(res)
    test(index+=1)
  })
}
commented
// for of
async function test() {
   for(let x of list){
     const res = await square(x)
     console.log(res)
   }
}

// reduce + promise
function test(){
  list.reduce((p, v) => {
    return p.then(()=> square(v).then(console.log))
  }, Promise.resolve())
}

// 和上面基本一致
function test(){
  let p = Promise.resolve()
  let i=0

  while(list[i]!==undefined){
    let v = list[i++]
    p = p.then(()=>square(v).then(console.log))
  }
}
if (i === list.length) return

第三种方法中,if (i === list.length) return
条件应该是 list.length - 1 吧
因为你是从0开始的

// while 循环
async function test1() {
  while (list.length) {
    const num = list.shift();
    const res = await square(num);
    console.log(res);
  }
}
// 传统的for
async function test2() {
  for (const item of list) {
    const res = await square(item);
    console.log(res);
  }
}

// promise 队列
async function test3() {
  let p = Promise.resolve();
  for (let i = 0; i < list.length; i += 1) {
    p = p.then(() => square(list[i])).then((res) => console.log(res));
  }
}
const list = [1, 2, 3]
const square = num => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(num * num)
    }, 1000)
  })
}

 function test() {
  list.reduce((prev,next)=>{
    return prev.then(()=> square(next).then(res=>console.log(res)))
  },Promise.resolve())
}
test()

const list = [1, 2, 3];

const square = async (num) => {
return await new Promise((resolve, reject) => {
setTimeout(() => {
resolve(num * num);
}, 1000);
});
};

const test = async () => {
for (let x of list) {
const res = await square(x);
console.log(res);
}
};

test();

以前面试遇到一个差不多的题,当时用的是链式递归实现的:

function test(i = 0) {
        if (i  === list.length) return
        return square(list[i]).then(res => {
            console.log(res);
            return test(++i)
        })
    }
    test()

对于forEach的并行处理,可以用下面这段代码来学习一下:

const count = (item) => {
  return new Promise((resolve) => {
	  if (typeof item === 'number') {
      setTimeout(() => {
        console.log(item);
        resolve();
      }, 500);
    } else {
      setTimeout(() => {
        console.log(item);
        resolve();
      }, 1000);
    }
  })
}

// forEach
let arr = [1, 'a', 'b', 'c', 2];
arr.forEach(async item => {
  const ret = await count(item);
});

// 1
// 2
// a
// b
// c

// for
(async function test() {
  for (let i = 0; i < arr.length; i++) {
    await count(arr[i]);
  }
})()

// for ... of
(async function test() {
  for (let x of arr) {
    const res = await count(x)
  }
})()
const list = [1, 2, 3]
const square = num => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(num * num)
    }, 1000)
  })
}

function test(i=0) {
        console.log('i', i)
      square(list[i]).then(num=>{
        console.log(num)
        if(list[i+1]){
            test(i+1)
         }
      })
}

test()
const list = [1, 2, 3]
const square = num => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(num * num)
    }, 1000)
  })
}

(async function test() {
  for (let x of list) {
    const res = await square(x);
    console.log(res);
  }
})()
const list = [1, 2, 3]
const square = num => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(num * num)
        }, 1000)
    })
}

function test() {
    test.p = Promise.resolve();
    list.forEach(x => {
        test.p = test.p.then(() => square(x)).then(console.log)
    })
}
test()
const list = [1, 2, 3]
const square = num => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(num * num)
    }, 1000)
  })
}

function test() {
  let promise  = Promise.resolve();
  list.forEach(x => {
    promise = promise.then(() => square(x)).then(res => console.log(res))
  })
}
test()

第三种方式是有缺陷的,1秒后同时输出1,4,9
给出改进的方法
const list = [1, 2, 3] const square = num => { return new Promise((resolve, reject) => { setTimeout(() => { resolve(num * num) }, 1000) }) } let promise = Promise.resolve(); function test(i = 0) { if (i === list.length) return; promise = promise.then(async () => { await square(list[i]).then(res => { console.log('res', res) }) }); test(i + 1) } test()

直接在forEach 里面套个setTimeOut 不就可以了

function test() {
list.forEach(async x => {
setTimeout(async () => {
const res = await square(x)
console.log(res)
}, 1000 * x)
})
}

这角度相当刁钻了,就是但凡数组不是连续整数这代码还得改。。。应该用index去乘。

function test() {
    var p = Promise.resolve();
    list.forEach(x => {
        p = p.then(() => square(x).then(res => console.log(res)));
    });
}

function test() { list.reduce(async (p, x)=> { await p; const res = await square(x) console.log(res) }, Promise.resolve()) }

function test() {
  list.reduce(async (p, x)=> {
    await p;
    const res = await square(x)
    console.log(res, +new Date() / 1000)
  }, Promise.resolve())
}
commented

`const list = [1, 2, 3]
const square = num => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(num * num)
}, 1000)
})
}

function test() {
(async()=>{
for (let index = 0; index < 3; index++) {
const res = await square(index)
console.log(new Date(), res)
}
})()
}
test()
`

改造test就好了

function test() {
  list.forEach(async(x, index)=> {
    const res = await square(x)
    setTimeout(() => {console.log(res)}, 1000*index)
  })
}

用reduce实现链式调用

const list = [1, 2, 3]
const square = num=>{
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            resolve(num * num)
        }
        , 1000)
    }
    )
}

function test() {
    list.reduce((p,x)=>{
        return p.then(async()=>{
            const res = await square(x)
            console.log(res)
        }
        )
    }
    , Promise.resolve())
}
test()

重写 forEach

Array.prototype.forEach = async function(callback, thisArg) {
    let bindThis, idx;
    
    if (this === null) {
      throw new TypeError('this is null or not defined');
    }
    
    let arrayObj = Object(this);
    
    let len = O.length >>> 0;
    
    if (typeof callback !== 'function') {
      throw new TypeError(callback + 'is not a function')
    }
    
    if (arguments.length > 1) {
      bindThis = thisArg;
    }
    
    idx = 0
    
    while (idx < len) {
      let item;
      if (idx in arrayObj) {
        item = arrayObj[idx];
        await callback.call(bindThis, item, idx, arrayObj);
      }
      idx++;
    }
  }

arr = new Array(1,2,3);
async function foo(n) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(n + n);
    }, 1000);
  });
}
arr.forEach(async (item) => {
  console.log(await foo(item));
});
const list = [1, 2, 3];
const square = (num) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(num * num);
    }, 1000);
  });
};

async function test() {
  for (const item of list) {
    const res = await square(item);
    console.log(res);
  }
}
test();

for
for of

reduce

function test() {
    list.reduce((acc,cur,index)=> acc.then(()=>square(list[index])),Promise.resolve());
}
commented
const list = [1, 2, 3];
const square = (num) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(num * num);
    }, 1000);
  });
};

async function test(index) {
  const val = list[index];
  if (val === undefined) return;
  const res = await square(val);
  console.log(res);
  test(index + 1);
}

test(0);
const list = [1, 2, 3]
const square = num => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(num * num)
    }, 1000)
  })
}

function test() {
  list.forEach(async (x, i)=> {
    const res = await square(x)
    setTimeout(() => console.log(res), i * 1000)
  })
}
test()

《ES6 入门》中还提到了使用 reduce 解决:

list.reduce(async (_, x) => {
  await _
  const res = await square(x)
  console.log(res)
}, undefined)

大哥 这里面都没有return 为什么await _ (_) 会是上一次的方法呢 怎么累积的呢 ??? 传统的 不都是这样的吗 var sum = [0, 1, 2, 3].reduce(function (accumulator, currentValue) { return accumulator + currentValue; }, 0); 能帮忙解答下吗 迷茫。。。。。

list.reduce((_, x) => { console.log(_, x) }, undefined)

_ 是初始值或者上次回调函数的执行结果,如

[3, 2, 1].reduce((_, x) => { console.log(_, x); return `init${x}`; }, 'init')

结果为

init 3
init3 2
init2 1

当加入 async 后上次回调函数的执行结果将变为 pending 状态的 promise
所以回到题解中第一次回调执行,可以看作是

async function firstCb(_, x) {
  await undefined;
  const res = await square(x);
  console.log(res);
}
firstCb(undefined, 1);

第二次回调执行可以看作是

async function secondCb(_, x) {
  await _;
  const res = await square(x);
  console.log(res);
}
secondCb(firstCb(), 2);

在执行 secondCb 的时候,里面会等待 firstCb 的完成,再接着等待 square(x) 的完成,以此类推,所以能实现每隔一秒打印的效果。

commented

1秒后同时输出 1, 4, 9。因为forEach方法传入的函数是通过内部的循环去执行,将它写为async函数并不能阻塞循环。想要理解问题的关键点,首先要知道forEach是如何实现的。

forEach的简单实现

Array.prototype.forEach = function (fun) {
    for (let i = 0; i < this.length; i++) {
        fun.call(this, this[i], i, this);  // 此时fun为async函数
    }
} 

改造test:

async function test() {
  for(let x of list) {
    const res = await square(x)
    console.log(res)
  }
}
test()