互聯網寒冬之際,各大公司都縮減了HC,甚至是採取了「裁員」措施,在這樣的大環境之下,想要獲得一份更好的工作,必然需要付出更多的努力。

一年前,也許你搞清楚閉包,this,原型鏈,就能獲得認可。但是現在,很顯然是不行了。本文梳理出了一些面試中有一定難度的高頻原生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可以準確的判斷複雜數據類型,但是不能正確判斷基本數據類型。(正確判斷數據類型請戳:https://github.com/YvetteLau/...

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 循環

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


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/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綁定.

  1. 函數是否在 new 中調用(new綁定),如果是,那麼 this 綁定的是新創建的對象。
  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 查詢,都會在當前的作用域開始查找,如果沒有找到,就會向上級作用域繼續查找目標標識符,每次上升一個作用域,一直到全局作用域為止。


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

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 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:
  1. 創建一個新對象。
  2. 這個新對象會被執行[[原型]]連接。
  3. 將構造函數的作用域賦值給新對象,即this指向這個新對象.
  4. 如果函數沒有返回其他對象,那麼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);
}

其它繼承方式實現,可以參考《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;
}


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

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位表示一個數字。

圖片說明:

  • 第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 時回調完成。

  1. 如果參數中有一個promise失敗,那麼Promise.all返回的promise對象失敗
  2. 在任何情況下,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;
});
}
})
}
});
}

如果想了解更多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知識,請給我留言。您的問題將會出現在後續文章中~

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


推薦閱讀:
相关文章