面試--原生JS (一)

面試--原生JS
1.原始類型有哪幾種?null 是對象嗎?原始數據類型和複雜數據類型存儲有什麼區別?
原始類型有6種,分別是undefined,null,bool,string,number,symbol(ES6新增)。
雖然 typeof null 返回的值是 object,但是null不是對象,而是基本數據類型的一種。
原始數據類型存儲在棧內存,存儲的是值。
複雜數據類型存儲在堆內存,存儲的是地址。當我們把對象賦值給另外一個變量的時候,複製的是地址,指向同一塊內存空間,當其中一個對象改變時,另一個對象也會變化。

2. typeof 是否正確判斷類型? instance of呢? instance of 的實現原理是什麼?
首先 typeof 能夠正確的判斷基本數據類型,但是null, typeof null輸出的是對象。
對對象來說,typeof 不能正確的判斷其類型。
instance of可以準確的判斷複雜數據類型,但是不能正確判斷基本數據類型。 
instance of 是通過原型鏈判斷的,A instance of B, 在A的原型鏈中層層查找,是否有原型等於B.prototype,如果一直找到A的原型鏈的頂端(null;即Object.prototype.__proto__),仍然不等於B.prototype,那麼返回false,否則返回true.

instance of的實現代碼:
// L instanceof R
function instance_of(L, R) {//L 表示左表達式,R 表示右表達式
    var O = R.prototype;// 取 R 的顯式原型
    L = L.__proto__;    // 取 L 的隱式原型
    while (true) { 
        if (L === null) //已經找到頂層
            return false;  
        if (O === L)   //當 O 嚴格等於 L 時,返回 true
            return true; 
        L = L.__proto__;  //繼續向上一層原型鏈查找
    } 
}

3. for of , for in 和 forEach,map 的區別。
(1)for...of循環:具有 iterator 接口,就可以用for...of循環遍歷它的成員(屬性值)。for...of循環可以使用的範圍包括數組、Set 和 Map 結構、某些類似數組的對象、Generator 對象,以及字符串。for...of循環調用遍歷器接口,數組的遍歷器接口只返回具有數字索引的屬性。對於普通的對象for...of結構不能直接使用,會報錯,必須部署了 Iterator 接口後才能使用。可以中斷循環。
(2)for...in循環:遍歷對象自身的和繼承的可枚舉的屬性, 不能直接獲取屬性值。可以中斷循環。
(3)forEach: 只能遍歷數組,不能中斷,沒有返回值(或認爲返回值是undefined),不修改原數組。
(4)map: 只能遍歷數組,不能中斷,返回值是修改後的數組,不修改原數組。
PS: Object.keys():返回給定對象所有可枚舉屬性的字符串數組。

4. 如何判斷一個變量是不是數組?
使用 Array.isArray 判斷,如果返回 true, 說明是數組
使用 instanceof Array 判斷,如果返回true, 說明是數組
使用 Object.prototype.toString.call 判斷,如果值是 [object Array], 說明是數組
通過 constructor 來判斷,如果是數組,那麼 arr.constructor === Array. (不準確,因爲我們可以指定 obj.constructor = Array)
function fn() {
    console.log(Array.isArray(arguments));   //false; 因爲arguments是類數組,但不是數組
    console.log(Array.isArray([1,2,3,4]));   //true
    console.log(arguments instanceof Array); //fasle
    console.log([1,2,3,4] instanceof Array); //true
    console.log(Object.prototype.toString.call(arguments)); //[object Arguments]
    console.log(Object.prototype.toString.call([1,2,3,4])); //[object Array]
    console.log(arguments.constructor === Array); //false
    arguments.constructor = Array;
    console.log(arguments.constructor === Array); //true
    console.log(Array.isArray(arguments));        //false
}
fn(1,2,3,4);

5. 類數組和數組的區別是什麼?
類數組:
1)擁有length屬性,其它屬性(索引)爲非負整數(對象中的索引會被當做字符串來處理);
2)不具有數組所具有的方法;
類數組是一個普通對象,而真實的數組是Array類型。
常見的類數組有: 函數的參數 arugments, DOM 對象列表(比如通過 document.querySelectorAll 得到的列表), jQuery 對象 (比如 $("div")).

類數組可以轉換爲數組:
//第一種方法
Array.prototype.slice.call(arrayLike, start);
//第二種方法
[...arrayLike];
//第三種方法:
Array.from(arrayLike);
PS: 任何定義了遍歷器(Iterator)接口的對象,都可以用擴展運算符轉爲真正的數組。
Array.from方法用於將兩類對象轉爲真正的數組:類似數組的對象(array-like object)和可遍歷(iterable)的對象。

6. == 和 === 有什麼區別?
=== 不需要進行類型轉換,只有類型相同並且值相等時,才返回 true.
== 如果兩者類型不同,首先需要進行類型轉換。具體流程如下:
首先判斷兩者類型是否相同,如果相等,判斷值是否相等.
如果類型不同,進行類型轉換
判斷比較的是否是 null 或者是 undefined, 如果是, 返回 true .
判斷兩者類型是否爲 string 和 number, 如果是, 將字符串轉換成 number
判斷其中一方是否爲 boolean, 如果是, 將 boolean 轉爲 number 再進行判斷
判斷其中一方是否爲 object 且另一方爲 string、number 或者 symbol , 如果是, 將 object 轉爲原始類型再進行判斷
let person1 = {
    age: 25
}
let person2 = person1;
person2.ag e = 20;
console.log(person1 === person2); //true,注意複雜數據類型,比較的是引用地址

思考: [] == ![]
我們來分析一下: [] == ![] 是true還是false?
首先,我們需要知道 ! 優先級是高於 == (更多運算符優先級可查看: 運算符優先級)
![] 引用類型轉換成布爾值都是true,因此![]的是false
根據上面的比較步驟中的第五條,其中一方是 boolean,將 boolean 轉爲 number 再進行判斷,false轉換成 number,對應的值是 0.
根據上面比較步驟中的第六條,有一方是 number,那麼將object也轉換成Number,空數組轉換成數字,對應的值是0.(空數組轉換成數字,對應的值是0,如果數組中只有一個數字,那麼轉成number就是這個數字,其它情況,均爲NaN)
0 == 0; 爲true

7. ES6中的class和ES5的類有什麼區別?
ES6 class 內部所有定義的方法都是不可枚舉的;
ES6 class 必須使用 new 調用;
ES6 class 不存在變量提升;
ES6 class 默認即是嚴格模式;
ES6 class 子類必須在父類的構造函數中調用super(),這樣纔有this對象;ES5中類繼承的關係是相反的,先有子類的this,然後用父類的方法應用在this上。

8. 數組的哪些API會改變原數組?
修改原數組的API有:
splice/reverse/fill/copyWithin/sort/push/pop/unshift/shift
不修改原數組的API有:
slice/map/forEach/every/filter/reduce/entry/entries/find

9. let、const 以及 var 的區別是什麼?
let 和 const 定義的變量不會出現變量提升,而 var 定義的變量會提升。
let 和 const 是JS中的塊級作用域
let 和 const 不允許重複聲明(會拋出錯誤)
let 和 const 定義的變量在定義語句之前,如果使用會拋出錯誤(形成了暫時性死區),而 var 不會。
const 聲明一個只讀的常量。一旦聲明,常量的值就不能改變(如果聲明是一個對象,那麼不能改變的是對象的引用地址)

10. 在JS中什麼是變量提升?什麼是暫時性死區?
變量提升就是變量在聲明之前就可以使用,值爲undefined。
在代碼塊內,使用 let/const 命令聲明變量之前,該變量都是不可用的(會拋出錯誤)。這在語法上,稱爲“暫時性死區”。暫時性死區也意味着 typeof 不再是一個百分百安全的操作。

typeof x; // ReferenceError(暫時性死區,拋錯)
let x;
typeof y; // 值是undefined,不會報錯
暫時性死區的本質就是,只要一進入當前作用域,所要使用的變量就已經存在了,但是不可獲取,只有等到聲明變量的那一行代碼出現,纔可以獲取和使用該變量。

11. 如何正確的判斷this? 箭頭函數的this是什麼?
this的綁定規則有四種:默認綁定,隱式綁定,顯式綁定,new綁定.
函數是否在 new 中調用(new綁定),如果是,那麼 this 綁定的是新創建的對象。
函數是否通過 call,apply 調用,或者使用了 bind (即硬綁定),如果是,那麼this綁定的就是指定的對象。
函數是否在某個上下文對象中調用(隱式綁定),如果是的話,this 綁定的是那個上下文對象。一般是 obj.foo()
如果以上都不是,那麼使用默認綁定。如果在嚴格模式下,則綁定到 undefined,否則綁定到全局對象。
如果把 null 或者 undefined 作爲 this 的綁定對象傳入 call、apply 或者 bind, 這些值在調用時會被忽略,實際應用的是默認綁定規則。
箭頭函數沒有自己的 this, 它的this繼承於上一層代碼塊的this。
測試下是否已經成功Get了此知識點(瀏覽器執行環境):
var number = 5;
var obj = {
    number: 3,
    fn1: (function () {
        var number;
        this.number *= 2;
        number = number * 2;
        number = 3;
        return function () {
            var num = this.number;
            this.number *= 2;
            console.log(num);
            number *= 3;
            console.log(number);
        }
    })();
}
var fn1 = obj.fn1;
fn1.call(null);
obj.fn1();
console.log(window.number);

12. 詞法作用域和this的區別。
詞法作用域是由你在寫代碼時將變量和塊作用域寫在哪裏來決定的
this 是在調用時被綁定的,this 指向什麼,完全取決於函數的調用位置

13. 談談你對JS執行上下文棧和作用域鏈的理解。
執行上下文就是當前 JavaScript 代碼被解析和執行時所在環境, JS執行上下文棧可以認爲是一個存儲函數調用的棧結構,遵循先進後出的原則。
JavaScript執行在單線程上,所有的代碼都是排隊執行。
一開始瀏覽器執行全局的代碼時,首先創建全局的執行上下文,壓入執行棧的頂部。
每當進入一個函數的執行就會創建函數的執行上下文,並且把它壓入執行棧的頂部。當前函數執行-完成後,當前函數的執行上下文出棧,並等待垃圾回收。
瀏覽器的JS執行引擎總是訪問棧頂的執行上下文。
全局上下文只有唯一的一個,它在瀏覽器關閉時出棧。
作用域鏈: 無論是 LHS 還是 RHS 查詢,都會在當前的作用域開始查找,如果沒有找到,就會向上級作用域繼續查找目標標識符,每次上升一個作用域,一直到全局作用域爲止。

14. 什麼是閉包?閉包的作用是什麼?閉包有哪些使用場景?
閉包是指有權訪問另一個函數作用域中的變量的函數,創建閉包最常用的方式就是在一個函數內部創建另一個函數。
閉包的作用有:
封裝私有變量
模仿塊級作用域(ES5中沒有塊級作用域)
實現JS的模塊

15. call、apply有什麼區別?call,aplly和bind的內部是如何實現的?
call 和 apply 的功能相同,區別在於傳參的方式不一樣:
fn.call(obj, arg1, arg2, ...),調用一個函數, 具有一個指定的this值和分別地提供的參數(參數的列表)。
fn.apply(obj, [argsArray]),調用一個函數,具有一個指定的this值,以及作爲一個數組(或類數組對象)提供的參數。

call 的核心:
將函數設爲傳入參數的屬性
指定this到函數並傳入給定參數執行函數
如果不傳入參數或者參數爲null,默認指向爲 window / global
刪除參數上的函數
Function.prototype.call = function (context) {
    /** 如果第一個參數傳入的是 null 或者是 undefined, 那麼指向this指向 window/global */
    /** 如果第一個參數傳入的不是null或者是undefined, 那麼必須是一個對象 */
    if (!context) {
        //context爲null或者是undefined
        context = typeof window === 'undefined' ? global : window;
    }
    context.fn = this; //this指向的是當前的函數(Function的實例)
    let args = [...arguments].slice(1);//獲取除了this指向對象以外的參數, 空數組slice後返回的仍然是空數組
    let result = context.fn(...args); //隱式綁定,當前函數的this指向了context.
    delete context.fn;
    return result;
}
//測試代碼
var foo = {
    name: 'Selina'
}
var name = 'Chirs';
function bar(job, age) {
    console.log(this.name);
    console.log(job, age);
}
bar.call(foo, 'programmer', 20);
// Selina programmer 20
bar.call(null, 'teacher', 25);
// 瀏覽器環境: Chirs teacher 25; node 環境: undefined teacher 25

apply:
apply的實現和call很類似,但是需要注意他們的參數是不一樣的,apply的第二個參數是數組或類數組。
Function.prototype.apply = function (context, rest) {
    if (!context) {
        //context爲null或者是undefined時,設置默認值
        context = typeof window === 'undefined' ? global : window;
    }
    context.fn = this;
    let result = context.fn(...rest);
    delete context.fn;
    return result;
}
var foo = {
    name: 'Selina'
}
var name = 'Chirs';
function bar(job, age) {
    console.log(this.name);
    console.log(job, age);
}
bar.apply(foo, ['programmer', 20]);
// Selina programmer 20
bar.apply(null, ['teacher', 25]);
// 瀏覽器環境: Chirs programmer 20; node 環境: undefined teacher 25
bind
bind 和 call/apply 有一個很重要的區別,一個函數被 call/apply 的時候,會直接調用,但是 bind 會創建一個新函數。當這個新函數被調用時,bind() 的第一個參數將作爲它運行時的 this,之後的一序列參數將會在傳遞的實參前傳入作爲它的參數。
Function.prototype.my_bind = function(context) {
    if(typeof this !== "function"){
        throw new TypeError("not a function");
    }
    let self = this;
    let args = [...arguments].slice(1);
    function Fn() {};
    Fn.prototype = this.prototype;
    let bound = function() {
        let res = [...args, ...arguments]; //bind傳遞的參數和函數調用時傳遞的參數拼接
        context = this instanceof Fn ? this : context || this;
        return self.apply(context, res);
    }
    //原型鏈
    bound.prototype = new Fn();
    return bound;
}
var name = 'Jack';
function person(age, job, gender){
    console.log(this.name , age, job, gender);
}
var Yve = {name : 'Yvette'};
let result = person.my_bind(Yve, 22, 'enginner')('female');

16. new的原理是什麼?通過new的方式創建對象和通過字面量創建有什麼區別?
new:
創建一個新對象。
這個新對象會被執行[[原型]]連接。
將構造函數的作用域賦值給新對象,即this指向這個新對象.
如果函數沒有返回其他對象,那麼new表達式中的函數調用會自動返回這個新對象。
function new(func) {
    lat target = {};
    target.__proto__ = func.prototype;
    let res = func.call(target);
    if (typeof(res) == "object" || typeof(res) == "function") {
        return res;
    }
    return target;
}
字面量創建對象,不會調用 Object構造函數, 簡潔且性能更好;
new Object() 方式創建對象本質上是方法調用,涉及到在proto鏈中遍歷該方法,當找到該方法後,又會生產方法調用必須的 堆棧信息,方法調用結束後,還要釋放該堆棧,性能不如字面量的方式。
通過對象字面量定義對象時,不會調用Object構造函數。

17. 談談你對原型的理解?
在 JavaScript 中,每當定義一個對象(函數也是對象)時候,對象中都會包含一些預定義的屬性。其中每個函數對象都有一個prototype 屬性,這個屬性指向函數的原型對象。使用原型對象的好處是所有對象實例共享它所包含的屬性和方法。

18. 什麼是原型鏈?【原型鏈解決的是什麼問題?】
原型鏈解決的主要是繼承問題。
每個對象擁有一個原型對象,通過 proto (讀音: dunder proto) 指針指向其原型對象,並從中繼承方法和屬性,同時原型對象也可能擁有原型,這樣一層一層,最終指向 null(Object.proptotype.__proto__ 指向的是null)。這種關係被稱爲原型鏈 (prototype chain),通過原型鏈一個對象可以擁有定義在其他對象中的屬性和方法。

構造函數 Parent、Parent.prototype 和 實例 p 的關係如下:(p.__proto__ === Parent.prototype)

 

19. prototype 和 __proto__ 區別是什麼?
prototype是構造函數的屬性。
__proto__ 是每個實例都有的屬性,可以訪問 [[prototype]] 屬性。
實例的__proto__ 與其構造函數的prototype指向的是同一個對象。
function Student(name) {
    this.name = name;
}
Student.prototype.setAge = function(){
    this.age=20;
}
let Jack = new Student('jack');
console.log(Jack.__proto__);
//console.log(Object.getPrototypeOf(Jack));;
console.log(Student.prototype);
console.log(Jack.__proto__ === Student.prototype);//true

20. 使用ES5實現一個繼承?
組合繼承(最常用的繼承方式)
function SuperType() {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function() {
    console.log(this.name);
}

function SubType(name, age) {
    SuperType.call(this, name);
    this.age = age;
}
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;

SubType.prototype.sayAge = function() {
    console.log(this.age);
}

21. 什麼是深拷貝?深拷貝和淺拷貝有什麼區別?
淺拷貝是指只複製第一層對象,但是當對象的屬性是引用類型時,實質複製的是其引用,當引用指向的值改變時也會跟着變化。
深拷貝複製變量值,對於非基本類型的變量,則遞歸至基本類型變量後,再複製。深拷貝後的對象與原來的對象是完全隔離的,互不影響,對一個對象的修改並不會影響另一個對象。
實現一個深拷貝:
function deepClone(obj) { //遞歸拷貝
    if(obj == null) return null;
    if(obj instanceof Date || obj instanceof RegExp) {
        return new Date(obj);
    }
    if(typeof obj !== 'object') {
        return obj;
    }
    let t = new obj.constructor();
    for(let key in obj) {
        t[key] = deepClone(obj[key]);
    }
    return t;
}

22. 防抖和節流的區別是什麼?防抖和節流的實現。
防抖和節流的作用都是防止函數多次調用。區別在於,假設一個用戶一直觸發這個函數,且每次觸發函數的間隔小於設置的時間,防抖的情況下只會調用一次,而節流的情況會每隔一定時間調用一次函數。

防抖(debounce): n秒內函數只會執行一次,如果n秒內高頻事件再次被觸發,則重新計算時間
function debounce(func, wait, immediate=true) {
    let timeout, context, args;
        // 延遲執行函數
        const later = () => setTimeout(() => {
            // 延遲函數執行完畢,清空定時器
            timeout = null
            // 延遲執行的情況下,函數會在延遲函數中執行
            // 使用到之前緩存的參數和上下文
            if (!immediate) {
                func.apply(context, args);
                context = args = null;
            }
        }, wait);
        let debounced = function (...params) {
            if (!timeout) {
                timeout = later();
                if (immediate) {
                    //立即執行
                    func.apply(this, params);
                } else {
                    //閉包
                    context = this;
                    args = params;
                }
            } else {
                clearTimeout(timeout);
                timeout = later();
            }
        }
    debounced.cancel = function () {
        clearTimeout(timeout);
        timeout = null;
    };
    return debounced;
};
防抖的應用場景:
每次 resize/scroll 觸發統計事件
文本輸入的驗證(連續輸入文字後發送 AJAX 請求進行驗證,驗證一次就好)

節流(throttle): 高頻事件在規定時間內只會執行一次,執行一次後,只有大於設定的執行週期後纔會執行第二次。
//underscore.js
function throttle(func, wait, options) {
    var timeout, context, args, result;
    var previous = 0;
    if (!options) options = {};

    var later = function () {
        previous = options.leading === false ? 0 : Date.now() || new Date().getTime();
        timeout = null;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
    };

    var throttled = function () {
        var now = Date.now() || new Date().getTime();
        if (!previous && options.leading === false) previous = now;
        var remaining = wait - (now - previous);
        context = this;
        args = arguments;
        if (remaining <= 0 || remaining > wait) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            result = func.apply(context, args);
            if (!timeout) context = args = null;
        } else if (!timeout && options.trailing !== false) {
            // 判斷是否設置了定時器和 trailing
            timeout = setTimeout(later, remaining);
        }
        return result;
    };

    throttled.cancel = function () {
        clearTimeout(timeout);
        previous = 0;
        timeout = context = args = null;
    };

    return throttled;
};

函數節流的應用場景有:
DOM 元素的拖拽功能實現(mousemove)
射擊遊戲的 mousedown/keydown 事件(單位時間只能發射一顆子彈)
計算鼠標移動的距離(mousemove)
Canvas 模擬畫板功能(mousemove)
搜索聯想(keyup)
監聽滾動事件判斷是否到頁面底部自動加載更多:給 scroll 加了 debounce 後,只有用戶停止滾動後,纔會判斷是否到了頁面底部;如果是 throttle 的話,只要頁面滾動就會間隔一段時間判斷一次


23. 取數組的最大值(ES5、ES6)
// ES5 的寫法
Math.max.apply(null, [14, 3, 77, 30]);
// ES6 的寫法
Math.max(...[14, 3, 77, 30]);
// reduce
[14,3,77,30].reduce((accumulator, currentValue)=>{
    return accumulator = accumulator > currentValue ? accumulator : currentValue
});

24. ES6新的特性有哪些?
新增了塊級作用域(let,const)
提供了定義類的語法糖(class)
新增了一種基本數據類型(Symbol)
新增了變量的解構賦值
函數參數允許設置默認值,引入了rest參數,新增了箭頭函數
數組新增了一些API,如 isArray / from / of 方法;數組實例新增了 entries(),keys() 和 values() 等方法
對象和數組新增了擴展運算符
ES6 新增了模塊化(import/export)
ES6 新增了 Set 和 Map 數據結構
ES6 原生提供 Proxy 構造函數,用來生成 Proxy 實例
ES6 新增了生成器(Generator)和遍歷器(Iterator)

25. setTimeout倒計時爲什麼會出現誤差?
setTimeout() 只是將事件插入了“任務隊列”,必須等當前代碼(執行棧)執行完,主線程纔會去執行它指定的回調函數。要是當前代碼消耗時間很長,也有可能要等很久,所以並沒辦法保證回調函數一定會在 setTimeout() 指定的時間執行。所以, setTimeout() 的第二個參數表示的是最少時間,並非是確切時間。
HTML5標準規定了 setTimeout() 的第二個參數的最小值不得小於4毫秒,如果低於這個值,則默認是4毫秒。在此之前。老版本的瀏覽器都將最短時間設爲10毫秒。另外,對於那些DOM的變動(尤其是涉及頁面重新渲染的部分),通常是間隔16毫秒執行。這時使用 requestAnimationFrame() 的效果要好於 setTimeout();

26. 爲什麼 0.1 + 0.2 != 0.3 ?
0.1 + 0.2 != 0.3 是因爲在進制轉換和進階運算的過程中出現精度損失。
下面是詳細解釋:
JavaScript使用 Number 類型表示數字(整數和浮點數),使用64位表示一個數字。

 

圖片說明:
第0位:符號位,0表示正數,1表示負數(s)
第1位到第11位:儲存指數部分(e)
第12位到第63位:儲存小數部分(即有效數字)f
計算機無法直接對十進制的數字進行運算, 需要先對照 IEEE 754 規範轉換成二進制,然後對階運算。

1.進制轉換

0.1和0.2轉換成二進制後會無限循環

0.1 -> 0.0001100110011001...(無限循環)
0.2 -> 0.0011001100110011...(無限循環)

但是由於IEEE 754尾數位數限制,需要將後面多餘的位截掉,這樣在進制之間的轉換中精度已經損失。

2.對階運算

由於指數位數不相同,運算時需要對階運算 這部分也可能產生精度損失。

按照上面兩步運算(包括兩步的精度損失),最後的結果是

0.0100110011001100110011001100110011001100110011001100

結果轉換成十進制之後就是 0.30000000000000004。

27. promise 有幾種狀態, Promise 有什麼優缺點 ?
promise有三種狀態: fulfilled, rejected, resolved.
Promise 的優點:
一旦狀態改變,就不會再變,任何時候都可以得到這個結果
可以將異步操作以同步操作的流程表達出來,避免了層層嵌套的回調函數
Promise 的缺點:
無法取消 Promise
當處於pending狀態時,無法得知目前進展到哪一個階段

28. Promise構造函數是同步還是異步執行,then呢 ?promise如何實現then處理 ?
Promise的構造函數是同步執行的。then 是異步執行的。
promise的then實現,詳見:Promise源碼實現。

29. Promise和setTimeout的區別 ?
Promise 是微任務,setTimeout 是宏任務,同一個事件循環中,promise.then總是先於 setTimeout 執行。同一個事件循環中,promise.then 先於 setTimeout 執行。

30. 如何實現 Promise.all ?
要實現 Promise.all,首先我們需要知道 Promise.all 的功能:
如果傳入的參數是一個空的可迭代對象,那麼此promise對象回調完成(resolve),只有此情況,是同步執行的,其它都是異步返回的。
如果傳入的參數不包含任何 promise,則返回一個異步完成。
promises 中所有的promise都promise都“完成”時或參數中不包含 promise 時回調完成。

如果參數中有一個promise失敗,那麼Promise.all返回的promise對象失敗。
在任何情況下,Promise.all 返回的 promise 的完成狀態的結果都是一個數組。
Promise.all = function (promises) {
    return new Promise((resolve, reject) => {
        let index = 0;
        let result = [];
        if (promises.length === 0) {
            resolve(result);
        } else {
            setTimeout(() => {
                function processValue(i, data) {
                    result[i] = data;
                    if (++index === promises.length) {
                        resolve(result);
                    }
                }
                for (let i = 0; i < promises.length; i++) {
                    //promises[i] 可能是普通值
                    Promise.resolve(promises[i]).then((data) => {
                        processValue(i, data);
                    }, (err) => {
                        reject(err);
                        return;
                    });
                }
            })
        }
    });
}

31.如何實現 Promise.finally ?
不管成功還是失敗,都會走到finally中,並且finally之後,還可以繼續then。並且會將值原封不動的傳遞給後面的then。
Promise.prototype.finally = function (callback) {
    return this.then((value) => {
        return Promise.resolve(callback()).then(() => {
            return value;
        });
    }, (err) => {
        return Promise.resolve(callback()).then(() => {
            throw err;
        });
    });
}

32. 什麼是函數柯里化?實現 sum(1)(2)(3) 返回結果是1,2,3之和
函數柯里化是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,並且返回接受餘下的參數而且返回結果的新函數的技術。
function sum(a) {
    return function(b) {
        return function(c) {
            return a+b+c;
        }
    }
}
console.log(sum(1)(2)(3)); // 6
引申:實現一個curry函數,將普通函數進行柯里化:
function curry(fn, args = []) {
    return function(){
        let rest = [...args, ...arguments];
        if (rest.length < fn.length) {
            return curry.call(this,fn,rest);
        }else{
            return fn.apply(this,rest);
        }
    }
}
//test
function sum(a,b,c) {
    return a+b+c;
}
let sumFn = curry(sum);
console.log(sumFn(1)(2)(3)); //6
console.log(sumFn(1)(2, 3)); //6
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章