资讯专栏INFORMATION COLUMN

【Step-By-Step】高频面试题深入解析 / 周刊04

youkede / 3417人阅读

摘要:关于点击进入项目是我于开始的一个项目,每个工作日发布一道面试题。的状态由决定,分成以下两种情况只有的状态都变成,的状态才会变成,此时的返回值组成一个数组,传递给的回调函数。

关于【Step-By-Step】

</>复制代码

  1. Step-By-Step (点击进入项目) 是我于 2019-05-20 开始的一个项目,每个工作日发布一道面试题。

    每个周末我会仔细阅读大家的答案,整理最一份较优答案出来,因本人水平有限,有误的地方,大家及时指正。

  2. 如果想 加群 学习,可以通过文末的公众号,添加我为好友。

更多优质文章可戳: https://github.com/YvetteLau/...

__

</>复制代码

  1. 本周面试题一览:

什么是闭包?闭包的作用是什么?

实现 Promise.all 方法

异步加载 js 脚本的方法有哪些?

请实现一个 flattenDeep 函数,把嵌套的数组扁平化

可迭代对象有什么特点?

15. 什么是闭包?闭包的作用是什么? 什么是闭包?

闭包是指有权访问另一个函数作用域中的变量的函数,创建闭包最常用的方式就是在一个函数内部创建另一个函数。

创建一个闭包

</>复制代码

  1. function foo() {
  2. var a = 2;
  3. return function fn() {
  4. console.log(a);
  5. }
  6. }
  7. let func = foo();
  8. func(); //输出2

闭包使得函数可以继续访问定义时的词法作用域。拜 fn 所赐,在 foo() 执行后,foo 内部作用域不会被销毁。

无论通过何种手段将内部函数传递到所在的词法作用域之外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。如:

</>复制代码

  1. function foo() {
  2. var a = 2;
  3. function inner() {
  4. console.log(a);
  5. }
  6. outer(inner);
  7. }
  8. function outer(fn){
  9. fn(); //闭包
  10. }
  11. foo();
闭包的作用

能够访问函数定义时所在的词法作用域(阻止其被回收)。

私有化变量

</>复制代码

  1. function base() {
  2. let x = 10; //私有变量
  3. return {
  4. getX: function() {
  5. return x;
  6. }
  7. }
  8. }
  9. let obj = base();
  10. console.log(obj.getX()); //10

模拟块级作用域

</>复制代码

  1. var a = [];
  2. for (var i = 0; i < 10; i++) {
  3. a[i] = (function(j){
  4. return function () {
  5. console.log(j);
  6. }
  7. })(i);
  8. }
  9. a[6](); // 6

创建模块

</>复制代码

  1. function coolModule() {
  2. let name = "Yvette";
  3. let age = 20;
  4. function sayName() {
  5. console.log(name);
  6. }
  7. function sayAge() {
  8. console.log(age);
  9. }
  10. return {
  11. sayName,
  12. sayAge
  13. }
  14. }
  15. let info = coolModule();
  16. info.sayName(); //"Yvette"

模块模式具有两个必备的条件(来自《你不知道的JavaScript》)

必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)

封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。

闭包的缺点

闭包会导致函数的变量一直保存在内存中,过多的闭包可能会导致内存泄漏

16. 实现 Promise.all 方法

在实现 Promise.all 方法之前,我们首先要知道 Promise.all 的功能和特点,因为在清楚了 Promise.all 功能和特点的情况下,我们才能进一步去写实现。

Promise.all 功能

Promise.all(iterable) 返回一个新的 Promise 实例。此实例在 iterable 参数内所有的 promisefulfilled 或者参数中不包含 promise 时,状态变成 fulfilled;如果参数中 promise 有一个失败rejected,此实例回调失败,失败原因的是第一个失败 promise 的返回结果。

</>复制代码

  1. let p = Promise.all([p1, p2, p3]);

p的状态由 p1,p2,p3决定,分成以下;两种情况:

(1)只有p1、p2、p3的状态都变成 fulfilled,p的状态才会变成 fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。

(2)只要p1、p2、p3之中有一个被 rejected,p的状态就变成 rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

Promise.all 的特点

</>复制代码

  1. Promise.all 的返回值是一个 promise 实例

如果传入的参数为空的可迭代对象,Promise.all同步 返回一个已完成状态的 promise

如果传入的参数中不包含任何 promise,Promise.all异步 返回一个已完成状态的 promise

其它情况下,Promise.all 返回一个 处理中(pending) 状态的 promise.

</>复制代码

  1. Promise.all 返回的 promise 的状态

如果传入的参数中的 promise 都变成完成状态,Promise.all 返回的 promise 异步地变为完成。

如果传入的参数中,有一个 promise 失败,Promise.all 异步地将失败的那个结果给失败状态的回调函数,而不管其它 promise 是否完成

在任何情况下,Promise.all 返回的 promise 的完成状态的结果都是一个数组

Promise.all 实现

</>复制代码

  1. 仅考虑传入的参数是数组的情况

</>复制代码

  1. /** 仅考虑 promises 传入的是数组的情况时 */
  2. Promise.all = function (promises) {
  3. return new Promise((resolve, reject) => {
  4. if (promises.length === 0) {
  5. resolve([]);
  6. } else {
  7. let result = [];
  8. let index = 0;
  9. for (let i = 0; i < promises.length; i++ ) {
  10. //考虑到 i 可能是 thenable 对象也可能是普通值
  11. Promise.resolve(promises[i]).then(data => {
  12. result[i] = data;
  13. if (++index === promises.length) {
  14. //所有的 promises 状态都是 fulfilled,promise.all返回的实例才变成 fulfilled 态
  15. resolve(result);
  16. }
  17. }, err => {
  18. reject(err);
  19. return;
  20. });
  21. }
  22. }
  23. });
  24. }

可使用 MDN 上的代码进行测试

</>复制代码

  1. 考虑 iterable 对象

</>复制代码

  1. Promise.all = function (promises) {
  2. /** promises 是一个可迭代对象,省略对参数类型的判断 */
  3. return new Promise((resolve, reject) => {
  4. if (promises.length === 0) {
  5. //如果传入的参数是空的可迭代对象
  6. return resolve([]);
  7. } else {
  8. let result = [];
  9. let index = 0;
  10. let j = 0;
  11. for (let value of promises) {
  12. (function (i) {
  13. Promise.resolve(value).then(data => {
  14. result[i] = data; //保证顺序
  15. index++;
  16. if (index === j) {
  17. //此时的j是length.
  18. resolve(result);
  19. }
  20. }, err => {
  21. //某个promise失败
  22. reject(err);
  23. return;
  24. });
  25. })(j)
  26. j++; //length
  27. }
  28. }
  29. });
  30. }

测试代码:

</>复制代码

  1. let p2 = Promise.all({
  2. a: 1,
  3. [Symbol.iterator]() {
  4. let index = 0;
  5. return {
  6. next() {
  7. index++;
  8. if (index == 1) {
  9. return {
  10. value: new Promise((resolve, reject) => {
  11. setTimeout(resolve, 100, "foo");
  12. }), done: false
  13. }
  14. } else if (index == 2) {
  15. return {
  16. value: new Promise((resolve, reject) => {
  17. resolve(222);
  18. }), done: false
  19. }
  20. } else if(index === 3) {
  21. return {
  22. value: 3, done: false
  23. }
  24. }else {
  25. return { done: true }
  26. }
  27. }
  28. }
  29. }
  30. });
  31. setTimeout(() => {
  32. console.log(p2)
  33. }, 200);
17. 异步加载 js 脚本的方法有哪些?

deferasync 的区别在于:

defer 要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),在window.onload 之前执行;

async 一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。

如果有多个 defer 脚本,会按照它们在页面出现的顺序加载

多个 async 脚本不能保证加载顺序

动态创建 script 标签

动态创建的 script ,设置 src 并不会开始下载,而是要添加到文档中,JS文件才会开始下载。

</>复制代码

  1. let script = document.createElement("script");
  2. script.src = "XXX.js";
  3. // 添加到html文件中才会开始下载
  4. document.body.append(script);
XHR 异步加载JS

</>复制代码

  1. let xhr = new XMLHttpRequest();
  2. xhr.open("get", "js/xxx.js",true);
  3. xhr.send();
  4. xhr.onreadystatechange = function() {
  5. if (xhr.readyState == 4 && xhr.status == 200) {
  6. eval(xhr.responseText);
  7. }
  8. }
18. 请实现一个 flattenDeep 函数,把嵌套的数组扁平化 利用 Array.prototype.flat

ES6 为数组实例新增了 flat 方法,用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数组没有影响。

flat 默认只会 “拉平” 一层,如果想要 “拉平” 多层的嵌套数组,需要给 flat 传递一个整数,表示想要拉平的层数。

</>复制代码

  1. function flattenDeep(arr, deepLength) {
  2. return arr.flat(deepLength);
  3. }
  4. console.log(flattenDeep([1, [2, [3, [4]], 5]], 3));

当传递的整数大于数组嵌套的层数时,会将数组拉平为一维数组,JS能表示的最大数字为 Math.pow(2, 53) - 1,因此我们可以这样定义 flattenDeep 函数

</>复制代码

  1. function flattenDeep(arr) {
  2. //当然,大多时候我们并不会有这么多层级的嵌套
  3. return arr.flat(Math.pow(2,53) - 1);
  4. }
  5. console.log(flattenDeep([1, [2, [3, [4]], 5]]));
利用 reduce 和 concat

</>复制代码

  1. function flattenDeep(arr){
  2. return arr.reduce((acc, val) => Array.isArray(val) ? acc.concat(flattenDeep(val)) : acc.concat(val), []);
  3. }
  4. console.log(flattenDeep([1, [2, [3, [4]], 5]]));
使用 stack 无限反嵌套多层嵌套数组

</>复制代码

  1. function flattenDeep(input) {
  2. const stack = [...input];
  3. const res = [];
  4. while (stack.length) {
  5. // 使用 pop 从 stack 中取出并移除值
  6. const next = stack.pop();
  7. if (Array.isArray(next)) {
  8. // 使用 push 送回内层数组中的元素,不会改动原始输入 original input
  9. stack.push(...next);
  10. } else {
  11. res.push(next);
  12. }
  13. }
  14. // 使用 reverse 恢复原数组的顺序
  15. return res.reverse();
  16. }
  17. console.log(flattenDeep([1, [2, [3, [4]], 5]]));
19. 可迭代对象有什么特点

ES6 规定,默认的 Iterator 接口部署在数据结构的 Symbol.iterator 属性,换个角度,也可以认为,一个数据结构只要具有 Symbol.iterator 属性(Symbol.iterator 方法对应的是遍历器生成函数,返回的是一个遍历器对象),那么就可以其认为是可迭代的。

可迭代对象的特点

具有 Symbol.iterator 属性,Symbol.iterator() 返回的是一个遍历器对象

可以使用 for ... of 进行循环

</>复制代码

  1. let arry = [1, 2, 3, 4];
  2. let iter = arry[Symbol.iterator]();
  3. console.log(iter.next()); //{ value: 1, done: false }
  4. console.log(iter.next()); //{ value: 2, done: false }
  5. console.log(iter.next()); //{ value: 3, done: false }
原生具有 Iterator 接口的数据结构:

Array

Map

Set

String

TypedArray

函数的 arguments 对象

NodeList 对象

自定义一个可迭代对象

上面我们说,一个对象只有具有正确的 Symbol.iterator 属性,那么其就是可迭代的,因此,我们可以通过给对象新增 Symbol.iterator 使其可迭代。

</>复制代码

  1. let obj = {
  2. name: "Yvette",
  3. age: 18,
  4. job: "engineer",
  5. *[Symbol.iterator]() {
  6. const self = this;
  7. const keys = Object.keys(self);
  8. for (let index = 0; index < keys.length; index++) {
  9. yield self[keys[index]];//yield表达式仅能使用在 Generator 函数中
  10. }
  11. }
  12. };
  13. for (var key of obj) {
  14. console.log(key); //Yvette 18 engineer
  15. }
参考文章:

[1] MDN Promise.all

[2] Promise

[3] Iterator

谢谢各位小伙伴愿意花费宝贵的时间阅读本文,如果本文给了您一点帮助或者是启发,请不要吝啬你的赞和Star,您的肯定是我前进的最大动力。 https://github.com/YvetteLau/...

</>复制代码

  1. 关注公众号,加入技术交流群。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/104782.html

相关文章

  • Step-By-Step高频面试深入解析 / 周刊05

    摘要:关于点击进入项目是我于开始的一个项目,每个工作日发布一道面试题。那个率先改变的实例的返回值,就传递给的回调函数。通过插入标签的方式来实现跨域,参数只能通过传入,仅能支持请求。因此清除浮动,只需要触发一个即可。 关于【Step-By-Step】 Step-By-Step (点击进入项目) 是我于 2019-05-20 开始的一个项目,每个工作日发布一道面试题。每个周末我会仔细阅读大家的...

    xiangchaobin 评论0 收藏0
  • Step-By-Step高频面试深入解析 / 周刊06

    摘要:实例拥有构造函数属性,该属性返回创建实例对象的构造函数。在考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。在子类的构造函数中,只有调用之后,才能使用关键字,否则报错。 不积跬步无以至千里。 关于【Step-By-Step】 Step-By-Step (点击进入项目) 是我于 2019-05-20 开始的一个项目,每个工作日发布一道面试题。每个周末我会仔细阅读...

    LiuRhoRamen 评论0 收藏0

发表评论

0条评论

youkede

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<