【面試篇】寒冬求職季之你必須要懂的原生JS(上)

前端小姐姐發表於2019-04-09
網際網路寒冬之際,各大公司都縮減了HC,甚至是採取了“裁員”措施,在這樣的大環境之下,想要獲得一份更好的工作,必然需要付出更多的努力。

一年前,也許你搞清楚閉包,this,原型鏈,就能獲得認可。但是現在,很顯然是不行了。本文梳理出了一些面試中有一定難度的高頻原生JS問題,部分知識點可能你之前從未關注過,或者看到了,卻沒有仔細研究,但是它們卻非常重要。

本文將以真實的面試題的形式來呈現知識點,大家在閱讀時,建議不要先看我的答案,而是自己先思考一番。儘管,本文所有的答案,都是我在翻閱各種資料,思考並驗證之後,才給出的(絕非複製貼上而來)。但因水平有限,本人的答案未必是最優的,如果您有更好的答案,歡迎給我留言。

本文篇幅較長,但是滿滿的都是乾貨!並且還埋伏了可愛的表情包,希望小夥伴們能夠堅持讀完。

衷心的祝願大家都能找到心儀的工作。

【面試篇】寒冬求職季之你必須要懂的原生JS(上)

1. 基本型別有哪幾種?null 是物件嗎?基本資料型別和複雜資料型別儲存有什麼區別?

  • 基本型別有6種,分別是undefined,null,bool,string,number,symbol(ES6新增)。
  • 雖然 typeof null 返回的值是 object,但是null不是物件,而是基本資料型別的一種。
  • 基本資料型別儲存在棧記憶體,儲存的是值。
  • 複雜資料型別的值儲存在堆記憶體,地址(指向堆中的值)儲存在棧記憶體。當我們把物件賦值給另外一個變數的時候,複製的是地址,指向同一塊記憶體空間,當其中一個物件改變時,另一個物件也會變化。

2. typeof 是否正確判斷型別? instanceof呢? instanceof 的實現原理是什麼?

首先 typeof 能夠正確的判斷基本資料型別,但是除了 null, typeof null輸出的是物件。

但是物件來說,typeof 不能正確的判斷其型別, typeof 一個函式可以輸出 'function',而除此之外,輸出的全是 object,這種情況下,我們無法準確的知道物件的型別。

instanceof可以準確的判斷複雜資料型別,但是不能正確判斷基本資料型別。(正確判斷資料型別請戳:github.com/YvetteLau/B…)

instanceof 是通過原型鏈判斷的,A instanceof B, 在A的原型鏈中層層查詢,是否有原型等於B.prototype,如果一直找到A的原型鏈的頂端(null;即Object.prototype.__proto__),仍然不等於B.prototype,那麼返回false,否則返回true.

instanceof的實現程式碼:

// 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 的區別。

  • for...of迴圈:具有 iterator 介面,就可以用for...of迴圈遍歷它的成員(屬性值)。for...of迴圈可以使用的範圍包括陣列、Set 和 Map 結構、某些類似陣列的物件、Generator 物件,以及字串。for...of迴圈呼叫遍歷器介面,陣列的遍歷器介面只返回具有數字索引的屬性。對於普通的物件,for...of結構不能直接使用,會報錯,必須部署了 Iterator 介面後才能使用。可以中斷迴圈。
  • for...in迴圈:遍歷物件自身的和繼承的可列舉的屬性, 不能直接獲取屬性值。可以中斷迴圈。
  • forEach: 只能遍歷陣列,不能中斷,沒有返回值(或認為返回值是undefined)。
  • map: 只能遍歷陣列,不能中斷,返回值是修改後的陣列。

PS: Object.keys():返回給定物件所有可列舉屬性的字串陣列

關於forEach是否會改變原陣列的問題,有些小夥伴提出了異議,為此我寫了程式碼測試了下(注意陣列項是複雜資料型別的情況)。 除了forEach之外,map等API,也有同樣的問題。

let arry = [1, 2, 3, 4];

arry.forEach((item) => {
    item *= 10;
});
console.log(arry); //[1, 2, 3, 4]

arry.forEach((item) => {
    arry[1] = 10; //直接運算元組
});
console.log(arry); //[ 1, 10, 3, 4 ]

let arry2 = [
    { name: "Yve" },
    { age: 20 }
];
arry2.forEach((item) => {
    item.name = 10;
});
console.log(arry2);//[ { name: 10 }, { age: 20, name: 10 } ]
複製程式碼

如還不瞭解 iterator 介面或 for...of, 請先閱讀ES6文件: Iterator 和 for...of 迴圈

更多細節請戳: github.com/YvetteLau/B…


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.

== 如果兩者型別不同,首先需要進行型別轉換。具體流程如下:

  1. 首先判斷兩者型別是否相同,如果相等,判斷值是否相等.
  2. 如果型別不同,進行型別轉換
  3. 判斷比較的是否是 null 或者是 undefined, 如果是, 返回 true .
  4. 判斷兩者型別是否為 string 和 number, 如果是, 將字串轉換成 number
  5. 判斷其中一方是否為 boolean, 如果是, 將 boolean 轉為 number 再進行判斷
  6. 判斷其中一方是否為 object 且另一方為 string、number 或者 symbol , 如果是, 將 object 轉為原始型別再進行判斷
let person1 = {
    age: 25
}
let person2 = person1;
person2.gae = 20;
console.log(person1 === person2); //true,注意複雜資料型別,比較的是引用地址
複製程式碼

思考: [] == ![]

我們來分析一下: [] == ![] 是true還是false?

  1. 首先,我們需要知道 ! 優先順序是高於 == (更多運算子優先順序可檢視: 運算子優先順序)
  2. ![] 引用型別轉換成布林值都是true,因此![]的是false
  3. 根據上面的比較步驟中的第五條,其中一方是 boolean,將 boolean 轉為 number 再進行判斷,false轉換成 number,對應的值是 0.
  4. 根據上面比較步驟中的第六條,有一方是 number,那麼將object也轉換成Number,空陣列轉換成數字,對應的值是0.(空陣列轉換成數字,對應的值是0,如果陣列中只有一個數字,那麼轉成number就是這個數字,其它情況,均為NaN)
  5. 0 == 0; 為true

7. ES6中的class和ES5的類有什麼區別?

  1. ES6 class 內部所有定義的方法都是不可列舉的;
  2. ES6 class 必須使用 new 呼叫;
  3. ES6 class 不存在變數提升;
  4. ES6 class 預設即是嚴格模式;
  5. 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/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繫結.

  1. 函式是否在 new 中呼叫(new繫結),如果是,那麼 this 繫結的是新建立的物件【前提是建構函式中沒有返回物件或者是function,否則this指向返回的物件/function】。
  2. 函式是否通過 call,apply 呼叫,或者使用了 bind (即硬繫結),如果是,那麼this繫結的就是指定的物件。
  3. 函式是否在某個上下文物件中呼叫(隱式繫結),如果是的話,this 繫結的是那個上下文物件。一般是 obj.foo()
  4. 如果以上都不是,那麼使用預設繫結。如果在嚴格模式下,則繫結到 undefined,否則繫結到全域性物件。
  5. 如果把 null 或者 undefined 作為 this 的繫結物件傳入 call、apply 或者 bind, 這些值在呼叫時會被忽略,實際應用的是預設繫結規則。
  6. 箭頭函式沒有自己的 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);
複製程式碼

如果this的知識點,您還不太懂,請戳: 嗨,你真的懂this嗎?


12. 詞法作用域和this的區別。

  • 詞法作用域是由你在寫程式碼時將變數和塊作用域寫在哪裡來決定的
  • this 是在呼叫時被繫結的,this 指向什麼,完全取決於函式的呼叫位置(關於this的指向問題,本文已經有說明)

13. 談談你對JS執行上下文棧和作用域鏈的理解。

執行上下文就是當前 JavaScript 程式碼被解析和執行時所在環境, JS執行上下文棧可以認為是一個儲存函式呼叫的棧結構,遵循先進後出的原則。

  • JavaScript執行在單執行緒上,所有的程式碼都是排隊執行。
  • 一開始瀏覽器執行全域性的程式碼時,首先建立全域性的執行上下文,壓入執行棧的頂部。
  • 每當進入一個函式的執行就會建立函式的執行上下文,並且把它壓入執行棧的頂部。當前函式執行-完成後,當前函式的執行上下文出棧,並等待垃圾回收。
  • 瀏覽器的JS執行引擎總是訪問棧頂的執行上下文。
  • 全域性上下文只有唯一的一個,它在瀏覽器關閉時出棧。

作用域鏈: 無論是 LHS 還是 RHS 查詢,都會在當前的作用域開始查詢,如果沒有找到,就會向上級作用域繼續查詢目標識別符號,每次上升一個作用域,一直到全域性作用域為止。


題難不難?不難!繼續挑戰一下!難!知道難,就更要繼續了!

【面試篇】寒冬求職季之你必須要懂的原生JS(上)

14. 什麼是閉包?閉包的作用是什麼?閉包有哪些使用場景?

閉包是指有權訪問另一個函式作用域中的變數的函式,建立閉包最常用的方式就是在一個函式內部建立另一個函式。

閉包的作用有:

  1. 封裝私有變數
  2. 模仿塊級作用域(ES5中沒有塊級作用域)
  3. 實現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 rest = [...arguments].slice(1);//獲取除了this指向物件以外的引數, 空陣列slice後返回的仍然是空陣列
    let result = context.fn(...rest); //隱式繫結,當前函式的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;
    if(rest === undefined || rest === null) {
        //undefined 或者 是 null 不是 Iterator 物件,不能被 ...
        result = context.fn(rest);
    }else if(typeof rest === 'object') {
        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.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.bind(Yve, 22, 'enginner')('female');	
複製程式碼

16. new的原理是什麼?通過new的方式建立物件和通過字面量建立有什麼區別?

new:

  1. 建立一個新物件。
  2. 這個新物件會被執行[[原型]]連線。
  3. 屬性和方法被加入到 this 引用的物件中。並執行了建構函式中的方法.
  4. 如果函式沒有返回其他物件,那麼this指向這個新物件,否則this指向建構函式中返回的物件。
function new(func) {
    let 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)

【面試篇】寒冬求職季之你必須要懂的原生JS(上)


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(name) {
    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);
}
複製程式碼

其它繼承方式實現,可以參考《JavaScript高階程式設計》


21. 什麼是深拷貝?深拷貝和淺拷貝有什麼區別?

淺拷貝是指只複製第一層物件,但是當物件的屬性是引用型別時,實質複製的是其引用,當引用指向的值改變時也會跟著變化。

深拷貝複製變數值,對於非基本型別的變數,則遞迴至基本型別變數後,再複製。深拷貝後的物件與原來的物件是完全隔離的,互不影響,對一個物件的修改並不會影響另一個物件。

實現一個深拷貝:

function deepClone(obj) { //遞迴拷貝
    if(obj === null) return null; //null 的情況
    if(obj instanceof RegExp) return new RegExp(obj);
    if(obj instanceof Date) return new Date(obj);
    if(typeof obj !== 'object') {
        //如果不是複雜資料型別,直接返回
        return obj;
    }
    /**
     * 如果obj是陣列,那麼 obj.constructor 是 [Function: Array]
     * 如果obj是物件,那麼 obj.constructor 是 [Function: Object]
     */
    let t = new obj.constructor();
    for(let key in obj) {
        //如果 obj[key] 是複雜資料型別,遞迴
        t[key] = deepClone(obj[key]);
    }
    return t;
}
複製程式碼

看不下去了?別人的送分題會成為你的送命題

【面試篇】寒冬求職季之你必須要懂的原生JS(上)

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新的特性有哪些?

  1. 新增了塊級作用域(let,const)
  2. 提供了定義類的語法糖(class)
  3. 新增了一種基本資料型別(Symbol)
  4. 新增了變數的解構賦值
  5. 函式引數允許設定預設值,引入了rest引數,新增了箭頭函式
  6. 陣列新增了一些API,如 isArray / from / of 方法;陣列例項新增了 entries(),keys() 和 values() 等方法
  7. 物件和陣列新增了擴充套件運算子
  8. ES6 新增了模組化(import/export)
  9. ES6 新增了 Set 和 Map 資料結構
  10. ES6 原生提供 Proxy 建構函式,用來生成 Proxy 例項
  11. 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位表示一個數字。

【面試篇】寒冬求職季之你必須要懂的原生JS(上)


圖片說明:

  • 第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, pending.

Promise 的優點:

  1. 一旦狀態改變,就不會再變,任何時候都可以得到這個結果
  2. 可以將非同步操作以同步操作的流程表達出來,避免了層層巢狀的回撥函式

Promise 的缺點:

  1. 無法取消 Promise
  2. 當處於pending狀態時,無法得知目前進展到哪一個階段

28. Promise建構函式是同步還是非同步執行,then中的方法呢 ?promise如何實現then處理 ?

Promise的建構函式是同步執行的。then 中的方法是非同步執行的。

promise的then實現,詳見: Promise原始碼實現


29. Promise和setTimeout的區別 ?

Promise 是微任務,setTimeout 是巨集任務,同一個事件迴圈中,promise.then總是先於 setTimeout 執行。


30. 如何實現 Promise.all ?

要實現 Promise.all,首先我們需要知道 Promise.all 的功能:

  1. 如果傳入的引數是一個空的可迭代物件,那麼此promise物件回撥完成(resolve),只有此情況,是同步執行的,其它都是非同步返回的。
  2. 如果傳入的引數不包含任何 promise,則返回一個非同步完成. promises 中所有的promise都“完成”時或引數中不包含 promise 時回撥完成。
  3. 如果引數中有一個promise失敗,那麼Promise.all返回的promise物件失敗
  4. 在任何情況下,Promise.all 返回的 promise 的完成狀態的結果都是一個陣列
Promise.all = function (promises) {
    return new Promise((resolve, reject) => {
        let index = 0;
        let result = [];
        if (promises.length === 0) {
            resolve(result);
        } else {
            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;
                });
            }
        }
    });
}
複製程式碼

如果想了解更多Promise的原始碼實現,可以參考我的另一篇文章:Promise的原始碼實現(完美符合Promise/A+規範)


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

如果您在面試中遇到了更多的原生JS問題,或者有一些本文未涉及到且有一定難度的JS知識,請給我留言。您的問題將會出現在後續文章中~

【面試篇】寒冬求職季之你必須要懂的原生JS(上)

本文的寫成耗費了非常多的時間,在這個過程中,我也學習到了很多知識,謝謝各位小夥伴願意花費寶貴的時間閱讀本文,如果本文給了您一點幫助或者是啟發,請不要吝嗇你的贊和Star,您的肯定是我前進的最大動力。github.com/YvetteLau/B…

小姐姐的微信公眾號:前端宇宙。

【面試篇】寒冬求職季之你必須要懂的原生JS(上)

後續寫作計劃

1.《寒冬求職季之你必須要懂的原生JS》(中)(下)

2.《寒冬求職季之你必須要知道的CSS》

3.《寒冬求職季之你必須要懂的前端安全》

4.《寒冬求職季之你必須要懂的一些瀏覽器知識》

5.《寒冬求職季之你必須要知道的效能優化》

針對React技術棧:

1.《寒冬求職季之你必須要懂的React》系列

2.《寒冬求職季之你必須要懂的ReactNative》系列

參考文章:

  1. www.ibm.com/developerwo…
  2. juejin.im/post/5c7736…
  3. 選用了面試之道上的部分面試題
  4. 選用了木易楊說文中提及的部分面試題: juejin.im/post/5bc92e…
  5. 特別說明: 0.1 + 0.2 !== 0.3 此題答案大量使用了此篇文章的圖文: juejin.im/post/5b90e0…
  6. 選用了朋友面試大廠時遇到的一些面試題
  7. 《你不知道的JavaSctipt》
  8. 《JavaScript高階程式設計》
  9. github.com/hanzichi/un…

相關文章