【Step-By-Step】高頻面試題深入解析 / 週刊04

劉小夕發表於2019-06-17

本週面試題一覽:

  • 什麼是閉包?閉包的作用是什麼?
  • 實現 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();
複製程式碼

閉包的作用

  1. 能夠訪問函式定義時所在的詞法作用域(阻止其被回收)。

  2. 私有化變數

function base() {
    let x = 10; //私有變數
    return {
        getX: function() {
            return x;
        }
    }
}
let obj = base();
console.log(obj.getX()); //10
複製程式碼
  1. 模擬塊級作用域
var a = [];
for (var i = 0; i < 10; i++) {
    a[i] = (function(j){
        return function () {
            console.log(j);
        }
    })(i);
}
a[6](); // 6
複製程式碼
  1. 建立模組
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 引數內所有的 promisefulfilled 或者引數中不包含 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 j = 0;
            for (let value of promises) {
                (function (i) {
                    Promise.resolve(value).then(data => {
                        result[i] = data; //保證順序
                        index++;
                        if (index === j) {
                            //此時的j是length.
                            resolve(result);
                        }
                    }, err => {
                        //某個promise失敗
                        reject(err);
                        return;
                    });
                })(j)
                j++; //length
            }
        }
    });
}
複製程式碼

測試程式碼:

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>
複製程式碼

deferasync 的區別在於:

  • 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] MDN Promise.all

[2] Promise

[3] Iterator

謝謝各位小夥伴願意花費寶貴的時間閱讀本文,如果本文給了您一點幫助或者是啟發,請不要吝嗇你的贊和Star,您的肯定是我前進的最大動力。 github.com/YvetteLau/B…

關注公眾號,加入技術交流群

【Step-By-Step】高頻面試題深入解析 / 週刊04

相關文章