關於【Step-By-Step】
Step-By-Step (點擊進入項目) 是我於2019-05-20
開始的一個項目,每個工作日發佈一道面試題。每個週末我會仔細閱讀大家的答案,整理最一份較優答案出來,因本人水平有限,有誤的地方,大家及時指正。
如果想 加羣 學習,可以通過文末的公衆號,添加我爲好友。
__
本週面試題一覽:
- 什麼是閉包?閉包的作用是什麼?
- 實現 Promise.all 方法
- 異步加載 js 腳本的方法有哪些?
- 請實現一個 flattenDeep 函數,把嵌套的數組扁平化
- 可迭代對象有什麼特點?
15. 什麼是閉包?閉包的作用是什麼?
什麼是閉包?
閉包是指有權訪問另一個函數作用域中的變量的函數,創建閉包最常用的方式就是在一個函數內部創建另一個函數。
創建一個閉包
function foo() {
var a = 2;
return function fn() {
console.log(a);
}
}
let func = foo();
func(); //輸出2
閉包使得函數可以繼續訪問定義時的詞法作用域。拜 fn 所賜,在 foo() 執行後,foo 內部作用域不會被銷燬。
無論通過何種手段將內部函數傳遞到所在的詞法作用域之外,它都會持有對原始定義作用域的引用,無論在何處執行這個函數都會使用閉包。如:
function foo() {
var a = 2;
function inner() {
console.log(a);
}
outer(inner);
}
function outer(fn){
fn(); //閉包
}
foo();
閉包的作用
- 能夠訪問函數定義時所在的詞法作用域(阻止其被回收)。
- 私有化變量
function base() {
let x = 10; //私有變量
return {
getX: function() {
return x;
}
}
}
let obj = base();
console.log(obj.getX()); //10
- 模擬塊級作用域
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = (function(j){
return function () {
console.log(j);
}
})(i);
}
a[6](); // 6
- 創建模塊
function coolModule() {
let name = 'Yvette';
let age = 20;
function sayName() {
console.log(name);
}
function sayAge() {
console.log(age);
}
return {
sayName,
sayAge
}
}
let info = coolModule();
info.sayName(); //'Yvette'
模塊模式具有兩個必備的條件(來自《你不知道的JavaScript》)
- 必須有外部的封閉函數,該函數必須至少被調用一次(每次調用都會創建一個新的模塊實例)
- 封閉函數必須返回至少一個內部函數,這樣內部函數才能在私有作用域中形成閉包,並且可以訪問或者修改私有的狀態。
閉包的缺點
閉包會導致函數的變量一直保存在內存中,過多的閉包可能會導致內存泄漏
16. 實現 Promise.all 方法
在實現 Promise.all 方法之前,我們首先要知道 Promise.all 的功能和特點,因爲在清楚了 Promise.all 功能和特點的情況下,我們才能進一步去寫實現。
Promise.all 功能
Promise.all(iterable)
返回一個新的 Promise 實例。此實例在 iterable
參數內所有的 promise
都 fulfilled
或者參數中不包含 promise
時,狀態變成 fulfilled
;如果參數中 promise
有一個失敗rejected
,此實例回調失敗,失敗原因的是第一個失敗 promise
的返回結果。
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 實現
僅考慮傳入的參數是數組的情況
/** 僅考慮 promises 傳入的是數組的情況時 */
Promise.all = function (promises) {
return new Promise((resolve, reject) => {
if (promises.length === 0) {
resolve([]);
} else {
let result = [];
let index = 0;
for (let i = 0; i < promises.length; i++ ) {
//考慮到 i 可能是 thenable 對象也可能是普通值
Promise.resolve(promises[i]).then(data => {
result[i] = data;
if (++index === promises.length) {
//所有的 promises 狀態都是 fulfilled,promise.all返回的實例才變成 fulfilled 態
resolve(result);
}
}, err => {
reject(err);
return;
});
}
}
});
}
可使用 MDN 上的代碼進行測試
考慮 iterable 對象
Promise.all = function (promises) {
/** promises 是一個可迭代對象,省略對參數類型的判斷 */
return new Promise((resolve, reject) => {
if (promises.length === 0) {
//如果傳入的參數是空的可迭代對象
return resolve([]);
} else {
let result = [];
let index = 0;
let iterator = promises[Symbol.iterator]();
function next() {
try {
var { value, done } = iterator.next();
if(!done) {
Promise.resolve(value).then(data => {
result[index] = data;
index++;
next();
}, err => {
//某個promise失敗
reject(err);
return;
});
}else {
//迭代完成
resolve(result);
}
} catch (e) {
return reject(e);
}
}
next(); //執行一次next
}
});
}
測試代碼:
let p2 = Promise.all({
a: 1,
[Symbol.iterator]() {
let index = 0;
return {
next() {
index++;
if (index == 1) {
return {
value: new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'foo');
}), done: false
}
} else if (index == 2) {
return {
value: new Promise((resolve, reject) => {
resolve(222);
}), done: false
}
} else if(index === 3) {
return {
value: 3, done: false
}
}else {
return { done: true }
}
}
}
}
});
setTimeout(() => {
console.log(p2)
}, 200);
17. 異步加載 js 腳本的方法有哪些?
<script>
標籤中增加 async
(html5) 或者 defer
(html4) 屬性,腳本就會異步加載。
<script src="../XXX.js" defer></script>
defer
和 async
的區別在於:
-
defer
要等到整個頁面在內存中正常渲染結束(DOM 結構完全生成,以及其他腳本執行完成),在window.onload 之前執行; -
async
一旦下載完,渲染引擎就會中斷渲染,執行這個腳本以後,再繼續渲染。 - 如果有多個
defer
腳本,會按照它們在頁面出現的順序加載 - 多個
async
腳本不能保證加載順序
動態創建 script
標籤
動態創建的 script
,設置 src
並不會開始下載,而是要添加到文檔中,JS文件纔會開始下載。
let script = document.createElement('script');
script.src = 'XXX.js';
// 添加到html文件中才會開始下載
document.body.append(script);
XHR 異步加載JS
let xhr = new XMLHttpRequest();
xhr.open("get", "js/xxx.js",true);
xhr.send();
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
eval(xhr.responseText);
}
}
18. 請實現一個 flattenDeep 函數,把嵌套的數組扁平化
利用 Array.prototype.flat
ES6 爲數組實例新增了 flat
方法,用於將嵌套的數組“拉平”,變成一維的數組。該方法返回一個新數組,對原數組沒有影響。
flat
默認只會 “拉平” 一層,如果想要 “拉平” 多層的嵌套數組,需要給 flat
傳遞一個整數,表示想要拉平的層數。
function flattenDeep(arr, deepLength) {
return arr.flat(deepLength);
}
console.log(flattenDeep([1, [2, [3, [4]], 5]], 3));
當傳遞的整數大於數組嵌套的層數時,會將數組拉平爲一維數組,JS能表示的最大數字爲 Math.pow(2, 53) - 1
,因此我們可以這樣定義 flattenDeep
函數
function flattenDeep(arr) {
//當然,大多時候我們並不會有這麼多層級的嵌套
return arr.flat(Math.pow(2,53) - 1);
}
console.log(flattenDeep([1, [2, [3, [4]], 5]]));
利用 reduce 和 concat
function flattenDeep(arr){
return arr.reduce((acc, val) => Array.isArray(val) ? acc.concat(flattenDeep(val)) : acc.concat(val), []);
}
console.log(flattenDeep([1, [2, [3, [4]], 5]]));
使用 stack 無限反嵌套多層嵌套數組
function flattenDeep(input) {
const stack = [...input];
const res = [];
while (stack.length) {
// 使用 pop 從 stack 中取出並移除值
const next = stack.pop();
if (Array.isArray(next)) {
// 使用 push 送回內層數組中的元素,不會改動原始輸入 original input
stack.push(...next);
} else {
res.push(next);
}
}
// 使用 reverse 恢復原數組的順序
return res.reverse();
}
console.log(flattenDeep([1, [2, [3, [4]], 5]]));
19. 可迭代對象有什麼特點
ES6 規定,默認的 Iterator
接口部署在數據結構的 Symbol.iterator
屬性,換個角度,也可以認爲,一個數據結構只要具有 Symbol.iterator
屬性(Symbol.iterator
方法對應的是遍歷器生成函數,返回的是一個遍歷器對象),那麼就可以其認爲是可迭代的。
可迭代對象的特點
- 具有
Symbol.iterator
屬性,Symbol.iterator()
返回的是一個遍歷器對象 - 可以使用
for ... of
進行循環
let arry = [1, 2, 3, 4];
let iter = arry[Symbol.iterator]();
console.log(iter.next()); //{ value: 1, done: false }
console.log(iter.next()); //{ value: 2, done: false }
console.log(iter.next()); //{ value: 3, done: false }
原生具有 Iterator
接口的數據結構:
- Array
- Map
- Set
- String
- TypedArray
- 函數的 arguments 對象
- NodeList 對象
自定義一個可迭代對象
上面我們說,一個對象只有具有正確的 Symbol.iterator
屬性,那麼其就是可迭代的,因此,我們可以通過給對象新增 Symbol.iterator
使其可迭代。
let obj = {
name: "Yvette",
age: 18,
job: 'engineer',
*[Symbol.iterator]() {
const self = this;
const keys = Object.keys(self);
for (let index = 0; index < keys.length; index++) {
yield self[keys[index]];//yield表達式僅能使用在 Generator 函數中
}
}
};
for (var key of obj) {
console.log(key); //Yvette 18 engineer
}
參考文章:
[1] 珠峯架構課(牆裂推薦)
[1] MDN Promise.all
[2] Promise
[3] Iterator
謝謝各位小夥伴願意花費寶貴的時間閱讀本文,如果本文給了您一點幫助或者是啓發,請不要吝嗇你的贊和Star,您的肯定是我前進的最大動力。 https://github.com/YvetteLau/...
關注公衆號,加入技術交流羣。