绑定完请刷新页面
取消
刷新

分享好友

×
取消 复制
【Step-By-Step】高频面试题深入解析 / 周刊04
2020-05-21 12:01:28

【Step-By-Step】

不积跬步无以至千里,每日一道经典面试题。

如需加群一起学习,可通过公众号加我为好友。


本周面试题一览:

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

  • 实现 Promise.all 方法

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

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

  • 可迭代对象有什么特点?

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

什么是闭包?

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

创建一个闭包

  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.能够访问函数定义时所在的词法作用域(阻止其被回收)。

2.私有化变量

  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. 模拟块级作用域

  1. var a = [];

  2. for (var i = ; i < 10; i++) {

  3. a[i] = (function(j){

  4. return function () {

  5. console.log(j);

  6. }

  7. })(i);

  8. }

  9. a[6](); // 6

  1. 创建模块

  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》)

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

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

闭包的缺点

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

2. 实现 Promise.all 方法

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

Promise.all 功能

Promise.all(iterable) 返回一个新的 Promise 实例。此实例在 iterable 参数内所有的 promise 都 fulfilled 或者参数中不包含 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 的特点

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

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

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

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

Promise.all 返回的 promise 的状态

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

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

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

Promise.all 实现

  1. Promise.all = function (promises) {

  2. return new Promise((resolve, reject) => {

  3. //Array.from 将可迭代对象转换成数组

  4. promises = Array.from(promises);

  5. if (promises.length === ) {

  6. resolve([]);

  7. } else {

  8. let result = [];

  9. let index = ;

  10. for (let i = ; i < promises.length; i++ ) {

  11. //考虑到 i 可能是 thenable 对象也可能是普通值

  12. Promise.resolve(promises[i]).then(data => {

  13. result[i] = data;

  14. if (++index === promises.length) {

  15. //所有的 promises 状态都是 fulfilled,promise.all返回的实例才变成 fulfilled 态

  16. resolve(result);

  17. }

  18. }, err => {

  19. reject(err);

  20. return;

  21. });

  22. }

  23. }

  24. });

  25. }

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

<script> 标签中增加 async(html5) 或者 defer(html4) 属性,脚本就会异步加载。

  1. <script src="../XXX.js" defer></script>

defer 和 async 的区别在于:

  • 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. }

4. 请实现一个 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]]));

5. 可迭代对象有什么特点

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 = ; 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/Blog


分享好友

分享这个小栈给你的朋友们,一起进步吧。

前端进阶之路
创建时间:2020-05-21 10:18:21
一位会拍照的程序媛来分享前端进阶之路
展开
订阅须知

• 所有用户可根据关注领域订阅专区或所有专区

• 付费订阅:虚拟交易,一经交易不退款;若特殊情况,可3日内客服咨询

• 专区发布评论属默认订阅所评论专区(除付费小栈外)

栈主、嘉宾

查看更多
  • 刘小夕
    栈主

小栈成员

查看更多
  • 小雨滴
  • ?
  • 栈栈
  • 叶子,你好
戳我,来吐槽~