2022高頻前端面試題合集之JavaScript篇(上)
theme: fancy highlight: atom-one-dark
近期整理了一下高頻的前端面試題,分享給大家一起來學習。如有問題,歡迎指正!
前端面試題系列文章:
JavaScript 面試題彙總(上篇)
1. 根據下面 ES6 建構函式的書寫方式,要求寫出 ES5 的
class Example {
constructor(name) {
this.name = name;
}
init() {
const fun = () => { console.log(this.name) }
fun();
}
}
const e = new Example('Hello');
e.init();
參考答案:
``` function Example(name) { 'use strict'; if (!new.target) { throw new TypeError('Class constructor cannot be invoked without new'); } this.name = name; }
Object.defineProperty(Example.prototype, 'init', { enumerable: false, value: function () { 'use strict'; if (new.target) { throw new TypeError('init is not a constructor'); } var fun = function () { console.log(this.name); } fun.call(this); } }) ```
解析:
此題的關鍵在於是否清楚 ES6 的 class 和普通建構函式的區別,記住它們有以下區別,就不會有遺漏:
ES6 中的 class 必須通過 new 來呼叫,不能當做普通函式呼叫,否則報錯
因此,在答案中,加入了 new.target 來判斷呼叫方式
ES6 的 class 中的所有程式碼均處於嚴格模式之下
因此,在答案中,無論是建構函式本身,還是原型方法,都使用了嚴格模式
ES6 中的原型方法是不可被列舉的
因此,在答案中,定義原型方法使用了屬性描述符,讓其不可列舉
原型上的方法不允許通過 new 來呼叫
因此,在答案中,原型方法中加入了 new.target 來判斷呼叫方式
2. 陣列去重有哪些方法?(美團 19 年)
參考答案:
``` // 數字或字串陣列去重,效率高 function unique(arr) { var result = {}; // 利用物件屬性名的唯一性來保證不重複 for (var i = 0; i < arr.length; i++) { if (!result[arr[i]]) { result[arr[i]] = true; } } return Object.keys(result); // 獲取物件所有屬性名的陣列 }
// 任意陣列去重,適配範圍光,效率低 function unique(arr) { var result = []; // 結果陣列 for (var i = 0; i < arr.length; i++) { if (!result.includes(arr[i])) { result.push(arr[i]); } } return result; }
// 利用ES6的Set去重,適配範圍廣,效率一般,書寫簡單 function unique(arr) { return [...new Set(arr)] } ```
3. 描述下列程式碼的執行結果
foo(typeof a);
function foo(p) {
console.log(this);
console.log(p);
console.log(typeof b);
let b = 0;
}
參考答案:
報錯,報錯的位置在
console.log(typeof b);
報錯原因:ReferenceError: Cannot access 'b' before initialization
解析:
這道題考查的是 ES6 新增的宣告變數關鍵字 let 以及暫時性死區的知識。let 和以前的 var 關鍵字不一樣,無法在 let 宣告變數之前訪問到該變數,所以在 typeof b 的地方就會報錯。
4. 描述下列程式碼的執行結果
class Foo {
constructor(arr) {
this.arr = arr;
}
bar(n) {
return this.arr.slice(0, n);
}
}
var f = new Foo([0, 1, 2, 3]);
console.log(f.bar(1));
console.log(f.bar(2).splice(1, 1));
console.log(f.arr);
參考答案:
[ 0 ]
[ 1 ]
[ 0, 1, 2, 3 ]解析:
主要考察的是陣列相關的知識。 f 物件上面有一個屬性 arr,arr 的值在初始化的時候會被初始化為 [0, 1, 2, 3] ,之後就完全是考察陣列以及陣列方法的使用了。
5. 描述下列程式碼的執行結果
01 function f(count) {
02 console.log(`foo${count}`);
03 setTimeout(() => { console.log(`bar${count}`); });
04 }
05 f(1);
06 f(2);
07 setTimeout(() => { f(3); });
參考答案:
foo1
foo2
bar1
bar2
foo3
bar3解析:
這個完全是考察的非同步的知識。呼叫 f(1) 的時候,會執行同步程式碼,打印出 foo1,然後 03 行的 setTimeout 被放入到非同步執行佇列,接下來呼叫 f(2) 的時候,打印出 foo2,後面 03 行的 setTimeout 又被放入到非同步執行佇列。然後執行 07 行的語句,被放入到非同步執行佇列。至此,所有同步程式碼就都執行完畢了。
接下來開始執行非同步程式碼,那麼大家時間沒寫,就都是相同的,所以誰先被放入到非同步佇列,誰就先執行,所以先打印出 bar1、然後是 bar2,接下來執行之前 07 行放入到非同步佇列裡面的 setTimeout,先執行 f 函式裡面的同步程式碼,打印出 foo3,然後是最後一個非同步,打印出 bar3
6. 描述下列程式碼的執行結果
var a = 2;
var b = 5;
console.log(a === 2 || 1 && b === 3 || 4);
參考答案:
true
考察的是邏輯運算子。在 || 裡面,只要有一個為真,後面的直接短路,都不用去計算。所以 a === 2 得到 true 之後直接短路了,返回 true。
7. 描述下列程式碼的執行結果
export class ButtonWrapper {
constructor(domBtnEl, hash) {
this.domBtnEl = domBtnEl;
this.hash = hash;
this.bindEvent();
}
bindEvent() {
this.domBtnEl.addEventListener('click', this.clickEvent, false);
}
detachEvent() {
this.domBtnEl.removeEventListener('click', this.clickEvent);
}
clickEvent() {
console.log(`The hash of the button is: ${this.hash}`);
}
}
參考答案:
上面的程式碼匯出了一個 ButtonWrapper 類,該類在被例項化的時候,例項化物件上面有兩個屬性,分別是 domBtnEl 和 hash,domBtnEl 是一個 DOM 節點,之後為這個 domBtnEl 綁定了點選事件,點選後打印出 The hash of the button is: hash 那句話。detachEvent 是移除點選事件,當呼叫例項化物件的 detachEvent 方法時,點選事件就會被移除。
8. 箭頭函式有哪些特點
參考答案:
更簡潔的語法,例如
- 只有一個形參就不需要用括號括起來
- 如果函式體只有一行,就不需要放到一個塊中
- 如果 return 語句是函式體內唯一的語句,就不需要 return 關鍵字
箭頭函式沒有自己的 this,arguments,super
箭頭函式 this 只會從自己的作用域鏈的上一層繼承 this。
9. 說一說類的繼承
參考答案:
繼承是面向物件程式設計中的三大特性之一。
JavaScript 中的繼承經過不斷的發展,從最初的物件冒充慢慢發展到了今天的聖盃模式繼承。
其中最需要掌握的就是偽經典繼承和聖盃模式的繼承。
很長一段時間,JS 繼承使用的都是組合繼承。這種繼承也被稱之為偽經典繼承,該繼承方式綜合了原型鏈和盜用建構函式的方式,將兩者的優點集中了起來。
組合繼承彌補了之前原型鏈和盜用建構函式這兩種方式各自的不足,是 JavaScript 中使用最多的繼承方式。
組合繼承最大的問題就是效率問題。最主要就是父類的建構函式始終會被呼叫兩次:一次是在建立子類原型時呼叫,另一次是在子類建構函式中呼叫。
本質上,子類原型最終是要包含超類物件的所有例項屬性,子類建構函式只要在執行時重寫自己的原型就行了。
聖盃模式的繼承解決了這一問題,其基本思路就是不通過呼叫父類建構函式來給子類原型賦值,而是取得父類原型的一個副本,然後將返回的新物件賦值給子類原型。
解析:該題主要考察就是對 js 中的繼承是否瞭解,以及常見的繼承的形式有哪些。最常用的繼承就是組合繼承(偽經典繼承)和聖盃模式繼承。下面附上 js 中這兩種繼承模式的詳細解析。
下面是一個組合繼承的例子:
``` // 基類 var Person = function (name, age) { this.name = name; this.age = age; } Person.prototype.test = "this is a test"; Person.prototype.testFunc = function () { console.log('this is a testFunc'); }
// 子類 var Student = function (name, age, gender, score) { Person.apply(this, [name, age]); // 盜用建構函式 this.gender = gender; this.score = score; } Student.prototype = new Person(); // 改變 Student 建構函式的原型物件 Student.prototype.testStuFunc = function () { console.log('this is a testStuFunc'); }
// 測試 var zhangsan = new Student("張三", 18, "男", 100); console.log(zhangsan.name); // 張三 console.log(zhangsan.age); // 18 console.log(zhangsan.gender); // 男 console.log(zhangsan.score); // 100 console.log(zhangsan.test); // this is a test zhangsan.testFunc(); // this is a testFunc zhangsan.testStuFunc(); // this is a testStuFunc ```
在上面的例子中,我們使用了組合繼承的方式來實現繼承,可以看到無論是基類上面的屬性和方法,還是子類自己的屬性和方法,都得到了很好的實現。
但是在組合繼承中存在效率問題,比如在上面的程式碼中,我們其實呼叫了兩次 Person,產生了兩組 name 和 age 屬性,一組在原型上,一組在例項上。
也就是說,我們在執行 Student.prototype = new Person( ) 的時候,我們是想要 Person 原型上面的方法,屬性是不需要的,因為屬性之後可以通過 Person.apply(this, [name, age]) 拿到,但是當你 new Person( ) 的時候,會例項化一個 Person 物件出來,這個物件上面,屬性和方法都有。
聖盃模式的繼承解決了這一問題,其基本思路就是不通過呼叫父類建構函式來給子類原型賦值,而是取得父類原型的一個副本,然後將返回的新物件賦值給子類原型。
下面是一個聖盃模式的示例:
``` // target 是子類,origin 是基類 // target ---> Student, origin ---> Person function inherit(target, origin) { function F() { }; // 沒有任何多餘的屬性
// origin.prototype === Person.prototype, origin.prototype.constructor === Person 建構函式 F.prototype = origin.prototype; // 假設 new F() 出來的物件叫小 f // 那麼這個 f 的原型物件 === F.prototype === Person.prototype // 那麼 f.constructor === Person.prototype.constructor === Person 的建構函式 target.prototype = new F(); // 而 f 這個物件又是 target 物件的原型物件 // 這意味著 target.prototype.constructor === f.constructor // 所以 target 的 constructor 會指向 Person 建構函式 // 我們要讓子類的 constructor 重新指向自己 // 若不修改則會發現 constructor 指向的是父類的建構函式 target.prototype.constructor = target;
}
// 基類 var Person = function (name, age) { this.name = name; this.age = age; } Person.prototype.test = "this is a test"; Person.prototype.testFunc = function () { console.log('this is a testFunc'); }
// 子類 var Student = function (name, age, gender, score) { Person.apply(this, [name, age]); this.gender = gender; this.score = score; } inherit(Student, Person); // 使用聖盃模式實現繼承 // 在子類上面新增方法 Student.prototype.testStuFunc = function () { console.log('this is a testStuFunc'); }
// 測試 var zhangsan = new Student("張三", 18, "男", 100);
console.log(zhangsan.name); // 張三 console.log(zhangsan.age); // 18 console.log(zhangsan.gender); // 男 console.log(zhangsan.score); // 100 console.log(zhangsan.test); // this is a test zhangsan.testFunc(); // this is a testFunc zhangsan.testStuFunc(); // this is a testStuFunc ```
在上面的程式碼中,我們在 inherit 方法中建立了一箇中間層,之後讓 F 的原型和父類的原型指向同一地址,再讓子類的原型指向這個 F 的例項化物件來實現了繼承。
這樣我們的繼承,屬性就不會像之前那樣例項物件上一份,原型物件上一份,擁有兩份。聖盃模式繼承是目前 js 繼承的最優解。
最後我再畫個圖幫助大家理解,如下圖:
組合模式(偽經典模式)下的繼承示意圖:
聖盃模式下的繼承示意圖:
10. new 操作符都做了哪些事?
參考答案:
new 運算子建立一個使用者定義的物件型別的例項或具有建構函式的內建物件的例項。
new 關鍵字會進行如下的操作:
步驟 1:建立一個空的簡單 JavaScript 物件,即 { } ;
步驟 2:連結該物件到另一個物件(即設定該物件的原型物件);
步驟 3:將步驟 1 新建立的物件作為 this 的上下文;
步驟 4:如果該函式沒有返回物件,則返回 this。
11. call、apply、bind 的區別 ?
參考答案:
call 和 apply 的功能相同,區別在於傳參的方式不一樣:
- fn.call(obj, arg1, arg2, ...) 呼叫一個函式, 具有一個指定的 this 值和分別地提供的引數(引數的列表)。
- fn.apply(obj, [argsArray]) 呼叫一個函式,具有一個指定的 this 值,以及作為一個數組(或類陣列物件)提供的引數。
bind 和 call/apply 有一個很重要的區別,一個函式被 call/apply 的時候,會直接呼叫,但是 bind 會建立一個新函式。當這個新函式被呼叫時,bind( ) 的第一個引數將作為它執行時的 this,之後的一序列引數將會在傳遞的實參前傳入作為它的引數。
12. 事件迴圈機制(巨集任務、微任務)
參考答案:
在 js 中任務會分為同步任務和非同步任務。
如果是同步任務,則會在主執行緒(也就是 js 引擎執行緒)上進行執行,形成一個執行棧。但是一旦遇到非同步任務,則會將這些非同步任務交給非同步模組去處理,然後主執行緒繼續執行後面的同步程式碼。
當非同步任務有了執行結果以後,就會在任務佇列裡面放置一個事件,這個任務佇列由事件觸發執行緒來進行管理。
一旦執行棧中所有的同步任務執行完畢,就代表著當前的主執行緒(js 引擎執行緒)空閒了,系統就會讀取任務佇列,將可以執行的非同步任務新增到執行棧中,開始執行。
在 js 中,任務佇列中的任務又可以被分為 2 種類型:巨集任務(macrotask)與微任務(microtask)
巨集任務可以理解為每次執行棧所執行的程式碼就是一個巨集任務,包括每次從事件佇列中獲取一個事件回撥並放到執行棧中所執行的任務。
微任務可以理解為當前巨集任務執行結束後立即執行的任務。
13. 你瞭解 node 中的事件迴圈機制嗎?node11 版本以後有什麼改變
參考答案:
Node.js 在主執行緒裡維護了一個事件佇列,當接到請求後,就將該請求作為一個事件放入這個佇列中,然後繼續接收其他請求。當主執行緒空閒時(沒有請求接入時),就開始迴圈事件佇列,檢查佇列中是否有要處理的事件,這時要分兩種情況:如果是非 I/O 任務,就親自處理,並通過回撥函式返回到上層呼叫;如果是 I/O 任務,就從執行緒池中拿出一個執行緒來處理這個事件,並指定回撥函式,然後繼續迴圈佇列中的其他事件。
當執行緒中的 I/O 任務完成以後,就執行指定的回撥函式,並把這個完成的事件放到事件佇列的尾部,等待事件迴圈,當主執行緒再次迴圈到該事件時,就直接處理並返回給上層呼叫。 這個過程就叫 事件迴圈 (Event Loop)。
無論是 Linux 平臺還是 Windows 平臺,Node.js 內部都是通過執行緒池來完成非同步 I/O 操作的,而 LIBUV 針對不同平臺的差異性實現了統一呼叫。因此,Node.js 的單執行緒僅僅是指 JavaScript 執行在單執行緒中,而並非 Node.js 是單執行緒。
Node.JS 的事件迴圈分為 6 個階段:
- timers 階段:這個階段執行 timer( setTimeout、setInterval )的回撥
- I/O callbacks 階段:處理一些上一輪迴圈中的少數未執行的 I/O 回撥
- idle、prepare 階段:僅 Node.js 內部使用
- poll 階段:獲取新的 I/O 事件, 適當的條件下 Node.js 將阻塞在這裡
- check 階段:執行 setImmediate( ) 的回撥
- close callbacks 階段:執行 socket 的 close 事件回撥
事件迴圈的執行順序為:
外部輸入資料 –-> 輪詢階段( poll )-–> 檢查階段( check )-–> 關閉事件回撥階段( close callback )–-> 定時器檢測階段( timer )–-> I/O 事件回撥階段( I/O callbacks )-–>閒置階段( idle、prepare )–->輪詢階段(按照該順序反覆執行)...
瀏覽器和 Node.js 環境下,微任務任務佇列的執行時機不同
- Node.js 端,微任務在事件迴圈的各個階段之間執行
- 瀏覽器端,微任務在事件迴圈的巨集任務執行完之後執行
Node.js v11.0.0 版本於 2018 年 10 月,主要有以下變化:
- V8 引擎更新至版本 7.0
- http、https 和 tls 模組預設使用 WHESWG URL 解析器。
- 隱藏子程序的控制檯視窗預設改為了 true。
- FreeBSD 10不再支援。
- 增加了多執行緒 Worker Threads
14. 什麼是函式柯里化?
參考答案:
柯里化(currying)又稱部分求值。一個柯里化的函式首先會接受一些引數,接受了這些引數之後,該函式並不會立即求值,而是繼續返回另外一個函式,剛才傳入的引數在函式形成的閉包中被儲存起來。待到函式被真正需要求值的時候,之前傳入的所有引數都會被一次性用於求值。
舉個例子,就是把原本:
function(arg1,arg2) 變成 function(arg1)(arg2)
function(arg1,arg2,arg3) 變成 function(arg1)(arg2)(arg3)
function(arg1,arg2,arg3,arg4) 變成 function(arg1)(arg2)(arg3)(arg4)總而言之,就是將:
function(arg1,arg2,…,argn) 變成 function(arg1)(arg2)…(argn)
15. promise.all 方法的使用場景?陣列中必須每一項都是 promise 物件嗎?不是 promise 物件會如何處理 ?
參考答案:
promise.all(promiseArray) * 方法是 promise 物件上的靜態方法,該方法的作用是將多個 promise 物件例項包裝,生成並返回一個新的 promise* 例項。
此方法在集合多個 promise 的返回結果時很有用。
返回值將會按照引數內的 promise 順序排列,而不是由呼叫 promise 的完成順序決定。
promise.all 的特點
接收一個Promise例項的陣列或具有Iterator介面的物件
如果元素不是Promise物件,則使用Promise.resolve轉成Promise物件
如果全部成功,狀態變為resolved,返回值將組成一個數組傳給回撥
只有有一個失敗,狀態就變為 rejected,返回值將直接傳遞給回撥 all( )的返回值,也是新的 promise 物件
16. this 的指向哪幾種 ?
參考答案:
總結起來,this 的指向規律有如下幾條:
- 在函式體中,非顯式或隱式地簡單呼叫函式時,在嚴格模式下,函式內的 this 會被繫結到 undefined 上,在非嚴格模式下則會被繫結到全域性物件 window/global 上。
- 一般使用 new 方法呼叫建構函式時,建構函式內的 this 會被繫結到新建立的物件上。
- 一般通過 call/apply/bind 方法顯式呼叫函式時,函式體內的 this 會被繫結到指定引數的物件上。
- 一般通過上下文物件呼叫函式時,函式體內的 this 會被繫結到該物件上。
- 在箭頭函式中,this 的指向是由外層(函式或全域性)作用域來決定的。
17. JS 中繼承實現的幾種方式
參考答案:
JS 的繼承隨著語言的發展,從最早的物件冒充到現在的聖盃模式,湧現出了很多不同的繼承方式。每一種新的繼承方式都是對前一種繼承方式不足的一種補充。
原型鏈繼承
重點:讓新例項的原型等於父類的例項。
特點:例項可繼承的屬性有:例項的建構函式的屬性,父類建構函式屬性,父類原型的屬性。(新例項不會繼承父類例項的屬性!)
缺點:
- 1、新例項無法向父類建構函式傳參。
- 2、繼承單一。
- 3、所有新例項都會共享父類例項的屬性。(原型上的屬性是共享的,一個例項修改了原型屬性,另一個例項的原型屬性也會被修改!)
借用建構函式繼承
重點:用 call( ) 和 apply( ) 將父類建構函式引入子類函式(在子類函式中做了父類函式的自執行(複製))
特點:
- 1、只繼承了父類建構函式的屬性,沒有繼承父類原型的屬性。
- 2、解決了原型鏈繼承缺點1、2、3。
- 3、可以繼承多個建構函式屬性(call多個)。
- 4、在子例項中可向父例項傳參。
缺點:
- 1、只能繼承父類建構函式的屬性。
- 2、無法實現建構函式的複用。(每次用每次都要重新呼叫)
- 3、每個新例項都有父類建構函式的副本,臃腫。
組合模式(又被稱之為偽經典模式)
重點:結合了兩種模式的優點,傳參和複用
- 特點:
- 1、可以繼承父類原型上的屬性,可以傳參,可複用。
- 2、每個新例項引入的建構函式屬性是私有的。缺點:呼叫了兩次父類建構函式(耗記憶體),子類的建構函式會代替原型上的那個父類建構函式。
寄生組合式繼承(聖盃模式)
重點:修復了組合繼承的問題
18. 什麼是事件監聽
參考答案:
首先需要區別清楚事件監聽和事件監聽器。
在繫結事件的時候,我們需要對應的書寫一個事件處理程式,來應對事件發生時的具體行為。
這個事件處理程式我們也稱之為事件監聽器。
當事件繫結好後,程式就會對事件進行監聽,當用戶觸發事件時,就會執行對應的事件處理程式。
關於事件監聽,W3C 規範中定義了 3 個事件階段,依次是捕獲階段、目標階段、冒泡階段。
- 捕獲階段:在事件物件到達事件目標之前,事件物件必須從 window 經過目標的祖先節點傳播到事件目標。 這個階段被我們稱之為捕獲階段。在這個階段註冊的事件監聽器在事件到達其目標前必須先處理事件。
- 目標 階段:事件物件到達其事件目標。 這個階段被我們稱為目標階段。一旦事件物件到達事件目標,該階段的事件監聽器就要對它進行處理。如果一個事件物件型別被標誌為不能冒泡。那麼對應的事件物件在到達此階段時就會終止傳播。
- 冒泡 階段:事件物件以一個與捕獲階段相反的方向從事件目標傳播經過其祖先節點傳播到 window。這個階段被稱之為冒泡階段。在此階段註冊的事件監聽器會對相應的冒泡事件進行處理。
19. 什麼是 js 的閉包?有什麼作用?
參考答案:
一個函式和對其周圍狀態(lexical environment,詞法環境)的引用捆綁在一起(或者說函式被引用包圍),這樣的組合就是閉包(closure)。也就是說,閉包讓你可以在一個內層函式中訪問到其外層函式的作用域。在 JavaScript 中,每當建立一個函式,閉包就會在函式建立的同時被創建出來。
閉包的用處:
- 匿名自執行函式
- 結果快取
- 封裝
- 實現類和繼承
20. 事件委託以及冒泡原理
參考答案:
事件委託,又被稱之為事件代理。在 JavaScript 中,新增到頁面上的事件處理程式數量將直接關係到頁面整體的執行效能。導致這一問題的原因是多方面的。
首先,每個函式都是物件,都會佔用記憶體。記憶體中的物件越多,效能就越差。其次,必須事先指定所有事件處理程式而導致的 DOM 訪問次數,會延遲整個頁面的互動就緒時間。
對事件處理程式過多問題的解決方案就是事件委託。
事件委託利用了事件冒泡,只指定一個事件處理程式,就可以管理某一型別的所有事件。例如,click 事件會一直冒泡到 document 層次。也就是說,我們可以為整個頁面指定一個 onclick 事件處理程式,而不必給每個可單擊的元素分別新增事件處理程式。
事件冒泡(event bubbling),是指事件開始時由最具體的元素(文件中巢狀層次最深的那個節點)接收,然後逐級向上傳播到較為不具體的節點(文件)。
21. let const var 的區別?什麼是塊級作用域?如何用?
參考答案:
- var 定義的變數,沒有塊的概念,可以跨塊訪問, 不能跨函式訪問,有變數提升。
- let 定義的變數,只能在塊作用域裡訪問,不能跨塊訪問,也不能跨函式訪問,無變數提升,不可以重複宣告。
- const 用來定義常量,使用時必須初始化(即必須賦值),只能在塊作用域裡訪問,而且不能修改,無變數提升,不可以重複宣告。
最初在 JS 中作用域有:全域性作用域、函式作用域。沒有塊作用域的概念。
ES6 中新增了塊級作用域。塊作用域由 { } 包括,if 語句和 for 語句裡面的 { } 也屬於塊作用域。
在以前沒有塊作用域的時候,在 if 或者 for 迴圈中宣告的變數會洩露成全域性變數,其次就是 { } 中的內層變數可能會覆蓋外層變數。塊級作用域的出現解決了這些問題。
22. ES5 的方法實現塊級作用域(立即執行函式) ES6 呢?
參考答案:
ES6 原生支援塊級作用域。塊作用域由 { } 包括,if 語句和 for 語句裡面的 { } 也屬於塊作用域。
使用 let 宣告的變數或者使用 const 宣告的常量,只能在塊作用域裡訪問,不能跨塊訪問。
23. ES6 箭頭函式的特性
參考答案:
更簡潔的語法,例如
- 只有一個形參就不需要用括號括起來
- 如果函式體只有一行,就不需要放到一個塊中
- 如果 return 語句是函式體內唯一的語句,就不需要 return 關鍵字
箭頭函式沒有自己的 this,arguments,super
箭頭函式 this 只會從自己的作用域鏈的上一層繼承 this。
24. 箭頭函式與普通函式的區別 ?
參考答案:
外形不同。箭頭函式使用箭頭定義,普通函式中沒有
普通函式可以有匿名函式,也可以有具體名函式,但是箭頭函式都是匿名函式。
箭頭函式不能用於建構函式,不能使用 new,普通函式可以用於建構函式,以此建立物件例項。
箭頭函式中 this 的指向不同,在普通函式中,this 總是指向呼叫它的物件,如果用作建構函式,this 指向建立的物件例項。
箭頭函式本身不建立 this,也可以說箭頭函式本身沒有 this,但是它在宣告時可以捕獲其所在上下文的 this 供自己使用。每一個普通函式呼叫後都具有一個 arguments 物件,用來儲存實際傳遞的引數。
但是箭頭函式並沒有此物件。取而代之用rest引數來解決。
箭頭函式不能用於 Generator 函式,不能使用 yeild 關鍵字。
箭頭函式不具有 prototype 原型物件。而普通函式具有 prototype 原型物件。
箭頭函式不具有 super。
箭頭函式不具有 new.target。
25. JS 的基本資料型別有哪些?基本資料型別和引用資料型別的區別
參考答案:
在 JavaScript 中,資料型別整體上來講可以分為兩大類:基本型別和引用資料型別
基本資料型別,一共有 6 種:
string,symbol,number,boolean,undefined,null
其中 symbol 型別是在 ES6 裡面新新增的基本資料型別。
引用資料型別,就只有 1 種:
object
基本資料型別的值又被稱之為原始值或簡單值,而引用資料型別的值又被稱之為複雜值或引用值。
兩者的區別在於:
原始值是表示 JavaScript 中可用的資料或資訊的最底層形式或最簡單形式。簡單型別的值被稱為原始值,是因為它們是不可細化的。
也就是說,數字是數字,字元是字元,布林值是 true 或 false,null 和 undefined 就是 null 和 undefined。這些值本身很簡單,不能夠再進行拆分。由於原始值的資料大小是固定的,所以原始值的資料是儲存於記憶體中的棧區裡面的。
在 JavaScript 中,物件就是一個引用值。因為物件可以向下拆分,拆分成多個簡單值或者複雜值。引用值在記憶體中的大小是未知的,因為引用值可以包含任何值,而不是一個特定的已知值,所以引用值的資料都是儲存於堆區裡面。
最後總結一下兩者的區別:
訪問方式
- 原始值:訪問到的是值
- 引用值:訪問到的是引用地址
比較方式
- 原始值:比較的是值
- 引用值:比較的是地址
動態屬性
- 原始值:無法新增動態屬性
- 引用值:可以新增動態屬性
變數賦值
- 原始值:賦值的是值
- 引用值:賦值的是地址
26. NaN 是什麼的縮寫
參考答案:
NaN 的全稱為 Not a Number,表示非數,或者說不是一個數。雖然 NaN 表示非數,但是它卻屬於 number 型別。
NaN 有兩個特點:
- 任何涉及 NaN 的操作都會返回 NaN
- NaN 和任何值都不相等,包括它自己本身
27. JS 的作用域型別
參考答案:
在 JavaScript 裡面,作用域一共有 4 種:全域性作用域,區域性作用域、函式作用域以及 eval 作用域。
全域性作用域:這個是預設的程式碼執行環境,一旦程式碼被載入,引擎最先進入的就是這個環境。
區域性作用域:當使用 let 或者 const 宣告變數時,這些變數在一對花括號中存在區域性作用域,只能夠在花括號內部進行訪問使用。
函式作用域:當進入到一個函式的時候,就會產生一個函式作用域。函式作用域裡面所宣告的變數只在函式中提供訪問使用。
eval 作用域:當呼叫 eval( ) 函式的時候,就會產生一個 eval 作用域。
28. undefined==null 返回的結果是什麼?undefined 與 null 的區別在哪?
參考答案:
返回 true。
這兩個值都表示“無”的意思。
通常情況下, 當我們試圖訪問某個不存在的或者沒有賦值的變數時,就會得到一個 undefined 值。Javascript 會自動將宣告是沒有進行初始化的變數設為 undifined。
而 null 值表示空,null 不能通過 Javascript 來自動賦值,也就是說必須要我們自己手動來給某個變數賦值為 null。
解析:
那麼為什麼 JavaScript 要設定兩個表示"無"的值呢?這其實是歷史原因。
1995 年 JavaScript 誕生時,最初像 Java 一樣,只設置了 null 作為表示"無"的值。根據 C 語言的傳統,null 被設計成可以自動轉為0。
但是,JavaScript 的設計者,覺得這樣做還不夠,主要有以下兩個原因。
- null 像在 Java 裡一樣,被當成一個物件。但是,JavaScript 的資料型別分成原始型別(primitive)和合成型別(complex)兩大類,作者覺得表示"無"的值最好不是物件。
- JavaScript 的最初版本沒有包括錯誤處理機制,發生資料型別不匹配時,往往是自動轉換型別或者默默地失敗。作者覺得,如果 null 自動轉為 0,很不容易發現錯誤。
因此,作者又設計了一個 undefined。
這裡注意:先有 null 後有 undefined 出來,undefined 是為了填補之前的坑。
JavaScript 的最初版本是這樣區分的:
null 是一個表示"無"的物件(空物件指標),轉為數值時為 0;
典型用法是:
- 作為函式的引數,表示該函式的引數不是物件。
- 作為物件原型鏈的終點。
undefined 是一個表示"無"的原始值,轉為數值時為 NaN。
典型用法是:
- 變數被聲明瞭,但沒有賦值時,就等於 undefined。
- 呼叫函式時,應該提供的引數沒有提供,該引數等於 undefined。
- 物件沒有賦值的屬性,該屬性的值為 undefined。
- 函式沒有返回值時,預設返回 undefined。
29. 寫一個函式判斷變數型別
參考答案:
function getType(data){ let type = typeof data; if(type !== "object"){ return type } return Object.prototype.toString.call(data).replace(/^[object (\S+)]$/,'$1') } function Person(){} console.log(getType(1)); // number console.log(getType(true)); // boolean console.log(getType([1,2,3])); // Array console.log(getType(/abc/)); // RegExp console.log(getType(new Date)); // Date console.log(getType(new Person)); // Object console.log(getType({})); // Object
30. js 的非同步處理函式
參考答案:
在最早期的時候,JavaScript 中要實現非同步操作,使用的就是 Callback 回撥函式。
但是回撥函式會產生回撥地獄(Callback Hell)
之後 ES6 推出了 Promise 解決方案來解決回撥地獄的問題。不過,雖然 Promise 作為 ES6 中提供的一種新的非同步程式設計解決方案,但是它也有問題。比如,程式碼並沒有因為新方法的出現而減少,反而變得更加複雜,同時理解難度也加大。
之後,就出現了基於 Generator 的非同步解決方案。不過,這種方式需要編寫外部的執行器,而執行器的程式碼寫起來一點也不簡單。當然也可以使用一些外掛,比如 co 模組來簡化執行器的編寫。
ES7 提出的 async 函式,終於讓 JavaScript 對於非同步操作有了終極解決方案。
實際上,async 只是生成器的一種語法糖而已,簡化了外部執行器的程式碼,同時利用 await 替代 yield,async 替代生成器的
*
號。
31. defer 與 async 的區別
參考答案:
按照慣例,所有 script 元素都應該放在頁面的 head 元素中。這種做法的目的就是把所有外部檔案(CSS 檔案和 JavaScript 檔案)的引用都放在相同的地方。可是,在文件的 head 元素中包含所有 JavaScript 檔案,意味著必須等到全部 JavaScript 程式碼都被下載、解析和執行完成以後,才能開始呈現頁面的內容(瀏覽器在遇到 body 標籤時才開始呈現內容)。
對於那些需要很多 JavaScript 程式碼的頁面來說,這無疑會導致瀏覽器在呈現頁面時出現明顯的延遲,而延遲期間的瀏覽器視窗中將是一片空白。為了避免這個問題,現在 Web 應用程式一般都全部 JavaScript 引用放在 body 元素中頁面的內容後面。這樣一來,在解析包含的 JavaScript 程式碼之前,頁面的內容將完全呈現在瀏覽器中。而使用者也會因為瀏覽器視窗顯示空白頁面的時間縮短而感到開啟頁面的速度加快了。
有了 defer 和 async 後,這種局面得到了改善。
defer (延遲指令碼)
延遲指令碼:defer 屬性只適用於外部指令碼檔案。
如果給 script 標籤定義了defer 屬性,這個屬性的作用是表明指令碼在執行時不會影響頁面的構造。也就是說,指令碼會被延遲到整個頁面都解析完畢後再執行。因此,如果 script 元素中設定了 defer 屬性,相當於告訴瀏覽器立即下載,但延遲執行。
async(非同步指令碼)
非同步指令碼:async 屬性也只適用於外部指令碼檔案,並告訴瀏覽器立即下載檔案。
但與 defer 不同的是:標記為 async 的指令碼並不保證按照指定它們的先後順序執行。
所以總結起來,兩者之間最大的差異就是在於指令碼下載完之後何時執行,顯然 defer 是最接近我們對於應用指令碼載入和執行的要求的。
defer 是立即下載但延遲執行,載入後續文件元素的過程將和指令碼的載入並行進行(非同步),但是指令碼的執行要在所有元素解析完成之後,DOMContentLoaded 事件觸發之前完成。async 是立即下載並執行,載入和渲染後續文件元素的過程將和 js 指令碼的載入與執行並行進行(非同步)。
32. 瀏覽器事件迴圈和任務佇列
參考答案:
JavaScript 的非同步機制由事件迴圈和任務佇列構成。
JavaScript 本身是單執行緒語言,所謂非同步依賴於瀏覽器或者作業系統等完成。JavaScript 主執行緒擁有一個執行棧以及一個任務佇列,主執行緒會依次執行程式碼,當遇到函式時,會先將函式入棧,函式執行完畢後再將該函數出棧,直到所有程式碼執行完畢。
遇到非同步操作(例如:setTimeout、Ajax)時,非同步操作會由瀏覽器(OS)執行,瀏覽器會在這些任務完成後,將事先定義的回撥函式推入主執行緒的任務佇列(task queue)中,當主執行緒的執行棧清空之後會讀取任務佇列中的回撥函式,當任務佇列被讀取完畢之後,主執行緒接著執行,從而進入一個無限的迴圈,這就是事件迴圈。
33. 原型與原型鏈 (美團 19年)
參考答案:
- 每個物件都有一個
__proto__
屬性,該屬性指向自己的原型物件- 每個建構函式都有一個
prototype
屬性,該屬性指向例項物件的原型物件- 原型物件裡的
constructor
指向建構函式本身如下圖:
每個物件都有自己的原型物件,而原型物件本身,也有自己的原型物件,從而形成了一條原型鏈條。
當試圖訪問一個物件的屬性時,它不僅僅在該物件上搜尋,還會搜尋該物件的原型,以及該物件的原型的原型,依次層層向上搜尋,直到找到一個名字匹配的屬性或到達原型鏈的末尾。
34. 作用域與作用域鏈 (美團 19年)
參考答案:
作用域是在執行時程式碼中的某些特定部分中變數,函式和物件的可訪問性。換句話說,作用域決定了程式碼區塊中變數和其他資源的可見性。ES6 之前 JavaScript 沒有塊級作用域,只有全域性作用域和函式作用域。ES6 的到來,為我們提供了塊級作用域。
作用域鏈指的是作用域與作用域之間形成的鏈條。當我們查詢一個當前作用域沒有定義的變數(自由變數)的時候,就會向上一級作用域尋找,如果上一級也沒有,就再一層一層向上尋找,直到找到全域性作用域還是沒找到,就宣佈放棄。這種一層一層的關係,就是作用域鏈 。
35. 閉包及應用場景以及閉包缺點 (美團 19年)
參考答案:
閉包的應用場景:
- 匿名自執行函式
- 結果快取
- 封裝
- 實現類和繼承
閉包的缺點:
因為閉包的作用域鏈會引用包含它的函式的活動物件,導致這些活動物件不會被銷燬,因此會佔用更多的記憶體。
36. 繼承方式 (美團 19年)
參考答案:
參閱前面第 9 題以及第 18 題答案。
37. 原始值與引用值 (美團 19年)
參考答案:
原始值是表示 JavaScript 中可用的資料或資訊的最底層形式或最簡單形式。簡單型別的值被稱為原始值,是因為它們是不可細化的。
也就是說,數字是數字,字元是字元,布林值是 true 或 false,null 和 undefined 就是 null 和 undefined。這些值本身很簡單,不能夠再進行拆分。由於原始值的資料大小是固定的,所以原始值的資料是儲存於記憶體中的棧區裡面的。
在 JavaScript 中,物件就是一個引用值。因為物件可以向下拆分,拆分成多個簡單值或者複雜值。引用值在記憶體中的大小是未知的,因為引用值可以包含任何值,而不是一個特定的已知值,所以引用值的資料都是儲存於堆區裡面。
最後總結一下兩者的區別:
訪問方式
- 原始值:訪問到的是值
- 引用值:訪問到的是引用地址
比較方式
- 原始值:比較的是值
- 引用值:比較的是地址
動態屬性
- 原始值:無法新增動態屬性
- 引用值:可以新增動態屬性
變數賦值
- 原始值:賦值的是值
- 引用值:賦值的是地址
38. 描述下列程式碼的執行結果
const first = () => (new Promise((resolve, reject) => {
console.log(3);
let p = new Promise((resolve, reject) => {
console.log(7);
setTimeout(() => {
console.log(1);
}, 0);
setTimeout(() => {
console.log(2);
resolve(3);
}, 0)
resolve(4);
});
resolve(2);
p.then((arg) => {
console.log(arg, 5); // 1 bb
});
setTimeout(() => {
console.log(6);
}, 0);
}))
first().then((arg) => {
console.log(arg, 7); // 2 aa
setTimeout(() => {
console.log(8);
}, 0);
});
setTimeout(() => {
console.log(9);
}, 0);
console.log(10);
參考答案:
3
7
10
4 5
2 7
1
2
6
9
8
39. 如何判斷陣列或物件(美團 19年)
參考答案:
- 通過 instanceof 進行判斷
var arr = [1,2,3,1]; console.log(arr instanceof Array) // true
- 通過物件的 constructor 屬性
var arr = [1,2,3,1]; console.log(arr.constructor === Array) // true
- Object.prototype.toString.call(arr)
console.log(Object.prototype.toString.call({name: "jerry"}));//[object Object] console.log(Object.prototype.toString.call([]));//[object Array]
- 可以通過 ES6 新提供的方法 Array.isArray( )
Array.isArray([]) //true
40. 物件深拷貝與淺拷貝,單獨問了 Object.assign(美團 19年)
參考答案:
淺拷貝:只是拷貝了基本型別的資料,而引用型別資料,複製後也是會發生引用,我們把這種拷貝叫做淺拷貝(淺複製)
淺拷貝只複製指向某個物件的指標,而不復制物件本身,新舊物件還是共享同一塊記憶體。
深拷貝:在堆中重新分配記憶體,並且把源物件所有屬性都進行新建拷貝,以保證深拷貝的物件的引用圖不包含任何原有物件或物件圖上的任何物件,拷貝後的物件與原來的物件是完全隔離,互不影響。
Object.assign 方法可以把任意多個的源物件自身的可列舉屬性拷貝給目標物件,然後返回目標物件。但是 Object.assign 方法進行的是淺拷貝,拷貝的是物件的屬性的引用,而不是物件本身。
42. 說說 instanceof 原理,並回答下面的題目(美團 19年)
function A(){}
function B(){}
A.prototype = new B();
let a = new A();
console.log(a instanceof B) // true of false ?
參考答案:
答案為 true。
instanceof 原理:
instanceof 用於檢測一個物件是否為某個建構函式的例項。
例如:A instanceof B
instanceof 用於檢測物件 A 是不是 B 的例項,而檢測是基於原型鏈進行查詢的,也就是說 B 的 prototype 有沒有在物件 A 的__proto__ 原型鏈上,如果有就返回 true,否則返回 false
43. 記憶體洩漏(美團 19 年)
參考答案:
記憶體洩漏(Memory Leak)是指程式中己動態分配的堆記憶體由於某種原因程式未釋放或無法釋放,造成系統記憶體的浪費,導致程式執行速度減慢甚至系統崩潰等嚴重後果。
Javascript 是一種高階語言,它不像 C 語言那樣要手動申請記憶體,然後手動釋放,Javascript 在宣告變數的時候自動會分配記憶體,普通的型別比如 number,一般放在棧記憶體裡,物件放在堆記憶體裡,宣告一個變數,就分配一些記憶體,然後定時進行垃圾回收。垃圾回收的任務由 JavaScript 引擎中的垃圾回收器來完成,它監視所有物件,並刪除那些不可訪問的物件。
基本的垃圾回收演算法稱為“標記-清除”,定期執行以下“垃圾回收”步驟:
- 垃圾回收器獲取根並“標記”(記住)它們。
- 然後它訪問並“標記”所有來自它們的引用。
- 然後它訪問標記的物件並標記它們的引用。所有被訪問的物件都被記住,以便以後不再訪問同一個物件兩次。
- 以此類推,直到有未訪問的引用(可以從根訪問)為止。
- 除標記的物件外,所有物件都被刪除。
44. ES6 新增哪些東西?讓你自己說(美團 19 年)
參考答案:
ES6 新增內容眾多,這裡列舉出一些關鍵的以及平時常用的新增內容:
- 箭頭函式
- 字串模板
- 支援模組化(import、export)
- 類(class、constructor、extends)
- let、const 關鍵字
- 新增一些陣列、字串等內建建構函式方法,例如 Array.from、Array.of 、Math.sign、Math.trunc 等
- 新增一些語法,例如擴充套件操作符、解構、函式預設引數等
- 新增一種基本資料型別 Symbol
- 新增超程式設計相關,例如 proxy、Reflect
- Set 和 Map 資料結構
- Promise
- Generator 生成器
45. weakmap、weakset(美團 19 年)
參考答案:
WeakSet 物件是一些物件值的集合, 並且其中的每個物件值都只能出現一次。在 WeakSet 的集合中是唯一的
它和 Set 物件的區別有兩點:
- 與 Set 相比,WeakSet 只能是物件的集合,而不能是任何型別的任意值。
- WeakSet 持弱引用:集合中物件的引用為弱引用。 如果沒有其他的對 WeakSet 中物件的引用,那麼這些物件會被當成垃圾回收掉。 這也意味著 WeakSet 中沒有儲存當前物件的列表。 正因為這樣,WeakSet 是不可列舉的。
WeakMap 物件也是鍵值對的集合。它的鍵必須是物件型別,值可以是任意型別。它的鍵被弱保持,也就是說,當其鍵所指物件沒有其他地方引用的時候,它會被 GC 回收掉。WeakMap 提供的介面與 Map 相同。
與 Map 物件不同的是,WeakMap 的鍵是不可列舉的。不提供列出其鍵的方法。列表是否存在取決於垃圾回收器的狀態,是不可預知的。
46. 為什麼 ES6 會新增 Promise(美團 19年)
參考答案:
在 ES6 以前,解決非同步的方法是回撥函式。但是回撥函式有一個最大的問題就是回撥地獄(callback hell),當我們的回撥函式巢狀的層數過多時,就會導致程式碼橫向發展。
Promise 的出現就是為了解決回撥地獄的問題。
47. ES5 實現繼承?(蝦皮)
參考答案:
- 借用建構函式實現繼承
function Parent1(){ this.name = "parent1" } function Child1(){ Parent1.call(this); this.type = "child1"; }
缺點:Child1 無法繼承 Parent1 的原型物件,並沒有真正的實現繼承 (部分繼承)。
- 借用原型鏈實現繼承
function Parent2(){ this.name = "parent2"; this.play = [1,2,3]; } function Child2(){ this.type = "child2"; } Child2.prototype = new Parent2();
缺點:原型物件的屬性是共享的。
- 組合式繼承
function Parent3(){ this.name = "parent3"; this.play = [1,2,3]; } function Child3(){ Parent3.call(this); this.type = "child3"; } Child3.prototype = Object.create(Parent3.prototype); Child3.prototype.constructor = Child3;
48. 科裡化?(搜狗)
參考答案:
柯里化,英語全稱 Currying,是把接受多個引數的函式變換成接受一個單一引數(最初函式的第一個引數)的函式,並且返回接受餘下的引數而且返回結果的新函式的技術。
舉個例子,就是把原本:
function(arg1,arg2) 變成 function(arg1)(arg2)
function(arg1,arg2,arg3) 變成 function(arg1)(arg2)(arg3)
function(arg1,arg2,arg3,arg4) 變成 function(arg1)(arg2)(arg3)(arg4)總而言之,就是將:
function(arg1,arg2,…,argn) 變成 function(arg1)(arg2)…(argn)
49. 防抖和節流?(蝦皮)
參考答案:
我們在平時開發的時候,會有很多場景會頻繁觸發事件,比如說搜尋框實時發請求,onmousemove、resize、onscroll 等,有些時候,我們並不能或者不想頻繁觸發事件,這時候就應該用到函式防抖和函式節流。
函式防抖(debounce),指的是短時間內多次觸發同一事件,只執行最後一次,或者只執行最開始的一次,中間的不執行。
函式節流(throttle),指連續觸發事件但是在 n 秒中只執行一次函式。即 2n 秒內執行 2 次... 。節流如字面意思,會稀釋函式的執行頻率。
50. 閉包?(好未來---探討了 40 分鐘)
參考答案:
請參閱前面第 20 題以及第 36 題答案。
51. 原型和原型鏈?(位元組)
參考答案:
請參閱前面第 34 題答案。
52. 排序演算法---(時間複雜度、空間複雜度)
參考答案:
演算法(Algorithm)是指用來操作資料、解決程式問題的一組方法。對於同一個問題,使用不同的演算法,也許最終得到的結果是一樣的,但在過程中消耗的資源和時間卻會有很大的區別。
主要還是從演算法所佔用的「時間」和「空間」兩個維度去考量。
- 時間維度:是指執行當前演算法所消耗的時間,我們通常用「時間複雜度」來描述。
- 空間維度:是指執行當前演算法需要佔用多少記憶體空間,我們通常用「空間複雜度」來描述。
因此,評價一個演算法的效率主要是看它的時間複雜度和空間複雜度情況。然而,有的時候時間和空間卻又是「魚和熊掌」,不可兼得的,那麼我們就需要從中去取一個平衡點。
排序也稱排序演算法(Sort Algorithm),排序是將一組資料,依指定的順序進行排列的過程。
排序的分類分為內部排序和外部排序法。
- 內部排序:指將需要處理的所有資料都載入到內部儲存器(記憶體)中進行排序。
- 外部排序:資料量過大,無法全部載入到記憶體中,需要藉助外部儲存(檔案等)進行排序。
53. 瀏覽器事件迴圈和 node 事件迴圈(搜狗)
參考答案:
- 瀏覽器中的 Event Loop
事件迴圈中的非同步佇列有兩種:macro(巨集任務)佇列和 micro(微任務)佇列。巨集任務佇列可以有多個,微任務佇列只有一個。
- 常見的 macro-task 比如:setTimeout、setInterval、 setImmediate、script(整體程式碼)、 I/O 操作、UI 渲染等。
- 常見的 micro-task 比如: process.nextTick、new Promise( ).then(回撥)、MutationObserver(html5 新特性) 等。
當某個巨集任務執行完後,會檢視是否有微任務佇列。如果有,先執行微任務佇列中的所有任務,如果沒有,會讀取巨集任務佇列中排在最前的任務,執行巨集任務的過程中,遇到微任務,依次加入微任務佇列。棧空後,再次讀取微任務佇列裡的任務,依次類推。
- Node 中的事件迴圈
Node 中的 Event Loop 和瀏覽器中的是完全不相同的東西。Node.js 採用 V8 作為 js 的解析引擎,而 I/O 處理方面使用了自己設計的 libuv,libuv 是一個基於事件驅動的跨平臺抽象層,封裝了不同作業系統一些底層特性,對外提供統一的 API,事件迴圈機制也是它裡面的實現。
Node.JS 的事件迴圈分為 6 個階段:
- timers 階段:這個階段執行 timer( setTimeout、setInterval )的回撥
- I/O callbacks 階段:處理一些上一輪迴圈中的少數未執行的 I/O 回撥
- idle、prepare 階段:僅 Node.js 內部使用
- poll 階段:獲取新的 I/O 事件, 適當的條件下 Node.js 將阻塞在這裡
- check 階段:執行 setImmediate( ) 的回撥
- close callbacks 階段:執行 socket 的 close 事件回撥
Node.js 的執行機制如下:
- V8 引擎解析 JavaScript 指令碼。
- 解析後的程式碼,呼叫 Node API。
- libuv 庫負責 Node API 的執行。它將不同的任務分配給不同的執行緒,形成一個 Event Loop(事件迴圈),以非同步的方式將任務的執行結果返回給 V8 引擎。
- V8 引擎再將結果返回給使用者。
54. 閉包的好處
參考答案:
請參閱前面第 20 題以及第 36 題答案。
55. let、const、var 的區別
參考答案:
- var 定義的變數,沒有塊的概念,可以跨塊訪問, 不能跨函式訪問,有變數提升。
- let 定義的變數,只能在塊作用域裡訪問,不能跨塊訪問,也不能跨函式訪問,無變數提升,不可以重複宣告。
- const 用來定義常量,使用時必須初始化(即必須賦值),只能在塊作用域裡訪問,而且不能修改,無變數提升,不可以重複宣告。
56. 閉包、作用域(可以擴充到作用域鏈)
參考答案:
什麼是作業域?
ES5 中只存在兩種作用域:全域性作用域和函式作用域。在 JavaScript 中,我們將作用域定義為一套規則,這套規則用來管理引擎如何在當前作用域以及巢狀子作用域中根據識別符號名稱進行變數(變數名或者函式名)查詢。
什麼是作用域鏈?
當訪問一個變數時,編譯器在執行這段程式碼時,會首先從當前的作用域中查詢是否有這個識別符號,如果沒有找到,就會去父作用域查詢,如果父作用域還沒找到繼續向上查詢,直到全域性作用域為止,,而作用域鏈,就是有當前作用域與上層作用域的一系列變數物件組成,它保證了當前執行的作用域對符合訪問許可權的變數和函式的有序訪問。
閉包產生的本質
當前環境中存在指向父級作用域的引用
什麼是閉包
閉包是一種特殊的物件,它由兩部分組成:執行上下文(代號 A),以及在該執行上下文中建立的函式 (代號 B),當 B 執行時,如果訪問了 A 中變數物件的值,那麼閉包就會產生,且在 Chrome 中使用這個執行上下文 A 的函式名代指閉包。
一般如何產生閉包
- 返回函式
- 函式當做引數傳遞
閉包的應用場景
- 柯里化 bind
- 模組
57. Promise
參考答案:
Promise 是非同步程式設計的一種解決方案,比傳統的解決方案——回撥函式和事件——更合理且更強大。它最早由社群提出並實現,ES6將其寫進了語言標準,統一了用法,並原生提供了Promise物件。
特點
物件的狀態不受外界影響 (3 種狀態)
- Pending 狀態(進行中)
- Fulfilled 狀態(已成功)
- Rejected 狀態(已失敗)
一旦狀態改變就不會再變 (兩種狀態改變:成功或失敗)
- Pending -> Fulfilled
- Pending -> Rejected
用法
``` var promise = new Promise(function(resolve, reject){ // ... some code
if (/* 非同步操作成功 */) { resolve(value); } else { reject(error); }
}) ```
58. 實現一個函式,對一個url進行請求,失敗就再次請求,超過最大次數就走失敗回撥,任何一次成功都走成功回撥
參考答案:
示例程式碼如下:
``` /* @params url: 請求介面地址; @params body: 設定的請求體; @params succ: 請求成功後的回撥 @params error: 請求失敗後的回撥 @params maxCount: 設定請求的數量 / function request(url, body, succ, error, maxCount = 5) { return fetch(url, body) .then(res => succ(res)) .catch(err => { if (maxCount <= 0) return error('請求超時'); return request(url, body, succ, error, --maxCount); }); }
// 呼叫請求函式 request('http://java.some.com/pc/reqCount', { method: 'GET', headers: {} }, (res) => { console.log(res.data); }, (err) => { console.log(err); }) ```
59. 氣泡排序
參考答案:
氣泡排序的核心思想是:
- 比較相鄰的兩個元素,如果前一個比後一個大或者小(取決於排序的順序是小到大還是大到小),則交換位置。
- 比較完第一輪的時候,最後一個元素是最大或最小的元素。
- 這時候最後一個元素已經是最大或最小的了,所以下一次冒泡的時候最後一個元素不需要參與比較。
示例程式碼:
``` function bSort(arr) { var len = arr.length; // 外層 for 迴圈控制冒泡的次數 for (var i = 0; i < len - 1; i++) { for (var j = 0; j < len - 1 - i; j++) { // 內層 for 迴圈控制每一次冒泡需要比較的次數 // 因為之後每一次冒泡的兩兩比較次數會越來越少,所以 -i if (arr[j] > arr[j + 1]) { var temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } return arr; }
//舉個數組 myArr = [20, -1, 27, -7, 35]; //使用函式 console.log(bSort(myArr)); // [ -7, -1, 20, 27, 35 ] ```
60. 陣列降維
參考答案:
陣列降維就是將一個巢狀多層的陣列進行降維操作,也就是對陣列進行扁平化。在 ES5 時代我們需要自己手寫方法或者藉助函式庫來完成,但是現在可以使用 ES6 新提供的陣列方法 flat 來完成陣列降維操作。
解析:使用 flat 方法會接收一個引數,這個引數是數值型別,是要處理扁平化陣列的深度,生成後的新陣列是獨立存在的,不會對原陣列產生影響。
flat 方法的語法如下:
var newArray = arr.flat([depth])
其中 depth 指定要提取巢狀陣列結構的深度,預設值為 1。
示例如下:
var arr = [1, 2, [3, 4, [5, 6]]]; console.log(arr.flat()); // [1, 2, 3, 4, [5, 6]] console.log(arr.flat(2)); // [1, 2, 3, 4, 5, 6]
上面的程式碼定義了一個層巢狀的陣列,預設情況下只會拍平一層陣列,也就是把原來的三維陣列降低到了二維陣列。在傳入的引數為 2 時,則會降低兩維,成為一個一維陣列。
使用 Infinity,可展開任意深度的巢狀陣列,示例如下:
var arr = [1, 2, [3, 4, [5, 6, [7, 8]]]]; console.log(arr.flat(Infinity)); // [1, 2, 3, 4, 5, 6, 7, 8]
在陣列中有空項的時候,使用 flat 方法會將中的空項進行移除。
var arr = [1, 2, , 4, 5]; console.log(arr.flat()); // [1, 2, 4, 5]
上面的程式碼中,陣列中第三項是空值,在使用 flat 後會對空項進行移除。
61. call apply bind
參考答案:
請參閱前面第 11 題答案。
62. promise 程式碼題
new Promise((resolve, reject) => {
reject(1);
console.log(2);
resolve(3);
console.log(4);
}).then((res) => { console.log(res) })
.catch(res => { console.log('reject1') })
try {
new Promise((resolve, reject) => {
throw 'error'
}).then((res) => { console.log(res) })
.catch(res => { console.log('reject2') })
} catch (err) {
console.log(err)
}
參考答案:
2
4
reject1
reject2直播課或者錄播課進行解析。
63. proxy 是實現代理,可以改變 js 底層的實現方式, 然後說了一下和 Object.defineProperty 的區別
參考答案:
兩者的區別總結如下:
- 代理原理:Object.defineProperty的原理是通過將資料屬性轉變為存取器屬性的方式實現的屬性讀寫代理。而Proxy則是因為這個內建的Proxy物件內部有一套監聽機制,在傳入handler物件作為引數構造代理物件後,一旦代理物件的某個操作觸發,就會進入handler中對應註冊的處理函式,此時我們就可以有選擇的使用Reflect將操作轉發被代理物件上。
- 代理侷限性:Object.defineProperty始終還是侷限於屬性層面的讀寫代理,對於物件層面以及屬性的其它操作代理它都無法實現。鑑於此,由於陣列物件push、pop等方法的存在,它對於陣列元素的讀寫代理實現的並不完全。而使用Proxy則可以很方便的監視陣列操作。
- 自我代理:Object.defineProperty方式可以代理到自身(代理之後使用物件本身即可),也可以代理到別的物件身上(代理之後需要使用代理物件)。Proxy方式只能代理到Proxy例項物件上。這一點在其它說法中是Proxy物件不需要侵入物件就可以實現代理,實際上Object.defineProperty方式也可以不侵入。
64. 使用 ES5 與 ES6 分別實現繼承
參考答案:
如果是使用 ES5 來實現繼承,那麼現在的最優解是使用聖盃模式。聖盃模式的核心思想就是不通過呼叫父類建構函式來給子類原型賦值,而是取得父類原型的一個副本,然後將返回的新物件賦值給子類原型。具體程式碼可以參閱前面第 9 題的解析。
ES6 新增了 extends 關鍵字,直接使用該關鍵字就能夠實現繼承。
65. 深拷貝
參考答案:
有深拷貝就有淺拷貝。
淺拷貝就是隻拷貝物件的引用,而不深層次的拷貝物件的值,多個物件指向堆記憶體中的同一物件,任何一個修改都會使得所有物件的值修改,因為它們共用一條資料。
深拷貝不是單純的拷貝一份引用資料型別的引用地址,而是將引用型別的值全部拷貝一份,形成一個新的引用型別,這樣就不會發生引用錯亂的問題,使得我們可以多次使用同樣的資料,而不用擔心資料之間會起衝突。
解析:
「深拷貝」就是在拷貝資料的時候,將資料的所有引用結構都拷貝一份。簡單的說就是,在記憶體中存在兩個資料結構完全相同又相互獨立的資料,將引用型型別進行復制,而不是隻複製其引用關係。
分析下怎麼做「深拷貝」:
- 首先假設深拷貝這個方法已經完成,為 deepClone
- 要拷貝一個數據,我們肯定要去遍歷它的屬性,如果這個物件的屬性仍是物件,繼續使用這個方法,如此往復
function deepClone(o1, o2) { for (let k in o2) { if (typeof o2[k] === 'object') { o1[k] = {}; deepClone(o1[k], o2[k]); } else { o1[k] = o2[k]; } } } // 測試用例 let obj = { a: 1, b: [1, 2, 3], c: {} }; let emptyObj = Object.create(null); deepClone(emptyObj, obj); console.log(emptyObj.a == obj.a); console.log(emptyObj.b == obj.b);
遞迴容易造成爆棧,尾部呼叫可以解決遞迴的這個問題,Chrome 的 V8 引擎做了尾部呼叫優化,我們在寫程式碼的時候也要注意尾部呼叫寫法。遞迴的爆棧問題可以通過將遞迴改寫成列舉的方式來解決,就是通過 for 或者 while 來代替遞迴。
66. async 與 await 的作用
參考答案:
async 是一個修飾符,async 定義的函式會預設的返回一個 Promise 物件 resolve 的值,因此對 async 函式可以直接進行 then 操作,返回的值即為 then 方法的傳入函式。
await 關鍵字只能放在 async 函式內部, await 關鍵字的作用就是獲取 Promise 中返回的內容, 獲取的是 Promise 函式中 resolve 或者 reject 的值。
67. 資料的基礎型別(原始型別)有哪些
參考答案:
JavaScript 中的基礎資料型別,一共有 6 種:
string,symbol,number,boolean,undefined,null
其中 symbol 型別是在 ES6 裡面新新增的基本資料型別。
68. typeof null 返回結果
參考答案:
返回 object
解析:至於為什麼會返回 object,這實際上是來源於 JavaScript 從第一個版本開始時的一個 bug,並且這個 bug 無法被修復。修復會破壞現有的程式碼。
原理這是這樣的,不同的物件在底層都表現為二進位制,在 JavaScript 中二進位制前三位都為 0 的話會被判斷為 object 型別,null 的二進位制全部為 0,自然前三位也是 0,所以執行 typeof 值會返回 object。
69. 對變數進行型別判斷的方式有哪些
參考答案:
常用的方法有 4 種:
- typeof
typeof 是一個操作符,其右側跟一個一元表示式,並返回這個表示式的資料型別。返回的結果用該型別的字串(全小寫字母)形式表示,包括以下 7 種:number、boolean、symbol、string、object、undefined、function 等。
- instanceof
instanceof 是用來判斷 A 是否為 B 的例項,表示式為:A instanceof B,如果 A 是 B 的例項,則返回 true,否則返回 false。 在這裡需要特別注意的是:instanceof 檢測的是原型。
- constructor
當一個函式 F 被定義時,JS 引擎會為 F 新增 prototype 原型,然後再在 prototype 上新增一個 constructor 屬性,並讓其指向 F 的引用。
- toString
toString( ) 是 Object 的原型方法,呼叫該方法,預設返回當前物件的 Class 。這是一個內部屬性,其格式為 [object Xxx] ,其中 Xxx 就是物件的型別。
對於 Object 物件,直接呼叫 toString( ) 就能返回 [object Object] 。而對於其他物件,則需要通過 call / apply 來呼叫才能返回正確的型別資訊。例如:
Object.prototype.toString.call('') ; // [object String] Object.prototype.toString.call(1) ; // [object Number] Object.prototype.toString.call(true) ;// [object Boolean] Object.prototype.toString.call(Symbol());//[object Symbol] Object.prototype.toString.call(undefined) ;// [object Undefined] Object.prototype.toString.call(null) ;// [object Null]
70. typeof 與 instanceof 的區別? instanceof 是如何實現?
參考答案:
- typeof
typeof 是一個操作符,其右側跟一個一元表示式,並返回這個表示式的資料型別。返回的結果用該型別的字串(全小寫字母)形式表示,包括以下 7 種:number、boolean、symbol、string、object、undefined、function 等。
- instanceof
instanceof 是用來判斷 A 是否為 B 的例項,表示式為:A instanceof B,如果 A 是 B 的例項,則返回 true,否則返回 false。 在這裡需要特別注意的是:instanceof 檢測的是原型。
用一段虛擬碼來模擬其內部執行過程:
instanceof (A,B) = { varL = A.__proto__; varR = B.prototype; if(L === R) { // A的內部屬性 __proto__ 指向 B 的原型物件 return true; } return false; }
從上述過程可以看出,當 A 的 proto 指向 B 的 prototype 時,就認為 A 就是 B 的例項。
需要注意的是,instanceof 只能用來判斷兩個物件是否屬於例項關係, 而不能判斷一個物件例項具體屬於哪種型別。
例如: [ ] instanceof Object 返回的也會是 true。
71. 引用型別有哪些,有什麼特點
參考答案:
JS 中七種內建型別(null,undefined,boolean,number,string,symbol,object)又分為兩大型別
兩大型別:
- 基本型別:
null
,undefined
,boolean
,number
,string
,symbol
- 引用型別Object:
Array
,Function
,Date
,RegExp
等基本型別和引用型別的主要區別有以下幾點:
存放位置:
- 基本資料型別:基本型別值在記憶體中佔據固定大小,直接儲存在棧記憶體中的資料
- 引用資料型別:引用型別在棧中儲存了指標,這個指標指向堆記憶體中的地址,真實的資料存放在堆記憶體裡。
值的可變性:
- 基本資料型別: 值不可變,javascript 中的原始值(undefined、null、布林值、數字和字串)是不可更改的
- 引用資料型別:引用型別是可以直接改變其值的
比較:
- 基本資料型別: 基本型別的比較是值的比較,只要它們的值相等就認為他們是相等的
- 引用資料型別: 引用資料型別的比較是引用的比較,看其的引用是否指向同一個物件
72. 如何得到一個變數的型別---指函式封裝實現
參考答案:
請參閱前面第 30 題答案。
73. 什麼是作用域、閉包
參考答案:
請參閱前面第 56 題。
74. 閉包的缺點是什麼?閉包的應用場景有哪些?怎麼銷燬閉包?
參考答案:
閉包是指有權訪問另外一個函式作用域中的變數的函式。
因為閉包引用著另一個函式的變數,導致另一個函式已經不使用了也無法銷燬,所以閉包使用過多,會佔用較多的記憶體,這也是一個副作用,記憶體洩漏。
如果要銷燬一個閉包,可以 把被引用的變數設定為null,即手動清除變數,這樣下次 js 垃圾回收機制回收時,就會把設為 null 的量給回收了。
閉包的應用場景:
- 匿名自執行函式
- 結果快取
- 封裝
- 實現類和繼承
75. JS的垃圾回收站機制
參考答案:
JS 具有自動垃圾回收機制。垃圾收集器會按照固定的時間間隔週期性的執行。
JS 常見的垃圾回收方式:標記清除、引用計數方式。
1、標記清除方式:
- 工作原理:當變數進入環境時,將這個變數標記為“進入環境”。當變數離開環境時,則將其標記為“離開環境”。標記“離開環境”的就回收記憶體。
- 工作流程:
- 垃圾回收器,在執行的時候會給儲存在記憶體中的所有變數都加上標記;
- 去掉環境中的變數以及被環境中的變數引用的變數的標記;
- 被加上標記的會被視為準備刪除的變數;
- 垃圾回收器完成記憶體清理工作,銷燬那些帶標記的值並回收他們所佔用的記憶體空間。
2、引用計數方式:
- 工作原理:跟蹤記錄每個值被引用的次數。
- 工作流程:
- 聲明瞭一個變數並將一個引用型別的值賦值給這個變數,這個引用型別值的引用次數就是 1;
- 同一個值又被賦值給另一個變數,這個引用型別值的引用次數加1;
- 當包含這個引用型別值的變數又被賦值成另一個值了,那麼這個引用型別值的引用次數減 1;
- 當引用次數變成 0 時,說明沒辦法訪問這個值了;
- 當垃圾收集器下一次執行時,它就會釋放引用次數是0的值所佔的記憶體。
76. 什麼是作用域鏈、原型鏈
參考答案:
什麼是作用域鏈?
當訪問一個變數時,編譯器在執行這段程式碼時,會首先從當前的作用域中查詢是否有這個識別符號,如果沒有找到,就會去父作用域查詢,如果父作用域還沒找到繼續向上查詢,直到全域性作用域為止,,而作用域鏈,就是有當前作用域與上層作用域的一系列變數物件組成,它保證了當前執行的作用域對符合訪問許可權的變數和函式的有序訪問。
什麼原型鏈?
每個物件都可以有一個原型__proto__,這個原型還可以有它自己的原型,以此類推,形成一個原型鏈。查詢特定屬性的時候,我們先去這個物件裡去找,如果沒有的話就去它的原型物件裡面去,如果還是沒有的話再去向原型物件的原型物件裡去尋找。這個操作被委託在整個原型鏈上,這個就是我們說的原型鏈。
77. new 一個建構函式發生了什麼
參考答案:
new 運算子建立一個使用者定義的物件型別的例項或具有建構函式的內建物件的例項。
new 關鍵字會進行如下的操作:
步驟 1:建立一個空的簡單 JavaScript 物件,即 { } ;
步驟 2:連結該物件到另一個物件(即設定該物件的原型物件);
步驟 3:將步驟 1 新建立的物件作為 this 的上下文;
步驟 4:如果該函式沒有返回物件,則返回 this。
78. 對一個建構函式例項化後. 它的原型鏈指向什麼
參考答案:
指向該建構函式例項化出來物件的原型物件。
對於建構函式來講,可以通過 prototype 訪問到該物件。
對於例項物件來講,可以通過隱式屬性 proto 來訪問到。
79. 什麼是變數提升
參考答案:
當 JavaScript 編譯所有程式碼時,所有使用 var 的變數宣告都被提升到它們的函式/區域性作用域的頂部(如果在函式內部宣告的話),或者提升到它們的全域性作用域的頂部(如果在函式外部宣告的話),而不管實際的宣告是在哪裡進行的。這就是我們所說的“提升”。
請記住,這種“提升”實際上並不發生在你的程式碼中,而只是一種比喻,與 JavaScript 編譯器如何讀取你的程式碼有關。記住當我們想到“提升”的時候,我們可以想象任何被提升的東西都會被移動到頂部,但是實際上你的程式碼並不會被修改。
函式宣告也會被提升,但是被提升到了最頂端,所以將位於所有變數宣告之上。
在編譯階段變數和函式宣告會被放入記憶體中,但是你在程式碼中編寫它們的位置會保持不變。
80. == 和 === 的區別是什麼
參考答案:
簡單來說: == 代表相同, === 代表嚴格相同(資料型別和值都相等)。
當進行雙等號比較時候,先檢查兩個運算元資料型別,如果相同,則進行===比較,如果不同,則願意為你進行一次型別轉換,轉換成相同型別後再進行比較,而 === 比較時,如果型別不同,直接就是false。
從這個過程來看,大家也能發現,某些情況下我們使用 === 進行比較效率要高些,因此,沒有歧義的情況下,不會影響結果的情況下,在 JS 中首選 === 進行邏輯比較。
81. Object.is 方法比較的是什麼
參考答案:
Object.is 方法是 ES6 新增的用來比較兩個值是否嚴格相等的方法,與 === (嚴格相等)的行為基本一致。不過有兩處不同:
- +0 不等於 -0。
- NaN 等於自身。
所以可以將Object.is 方法看作是加強版的嚴格相等。
82. 基礎資料型別和引用資料型別,哪個是儲存在棧記憶體中?哪個是在堆記憶體中?
參考答案:
在 ECMAScript 規範中,共定義了 7 種資料型別,分為 基本型別 和 引用型別 兩大類,如下所示:
- 基本型別:String、Number、Boolean、Symbol、Undefined、Null
- 引用型別:Object
基本型別也稱為簡單型別,由於其佔據空間固定,是簡單的資料段,為了便於提升變數查詢速度,將其儲存在棧中,即按值訪問。
引用型別也稱為複雜型別,由於其值的大小會改變,所以不能將其存放在棧中,否則會降低變數查詢速度,因此,其值儲存在堆(heap)中,而儲存在變數處的值,是一個指標,指向儲存物件的記憶體處,即按址訪問。引用型別除 Object 外,還包括 Function 、Array、RegExp、Date 等等。
83. 箭頭函式解決了什麼問題?
參考答案:
箭頭函式主要解決了 this 的指向問題。
解析:
在 ES5 時代,一旦物件的方法裡面又存在函式,則 this 的指向往往會讓開發人員抓狂。
例如:
//錯誤案例,this 指向會指向 Windows 或者 undefined var obj = { age: 18, getAge: function () { var a = this.age; // 18 var fn = function () { return new Date().getFullYear() - this.age; // this 指向 window 或 undefined }; return fn(); } }; console.log(obj.getAge()); // NaN
然而,箭頭函式沒有 this,箭頭函式的 this 是繼承父執行上下文裡面的 this
``` var obj = { age: 18, getAge: function () { var a = this.age; // 18 var fn = () => new Date().getFullYear() - this.age; // this 指向 obj 物件 return fn(); } };
console.log(obj.getAge()); // 2003 ```
84. new 一個箭頭函式後,它的 this 指向什麼?
參考答案:
我不知道這道題是出題人寫錯了還是故意為之。
箭頭函式無法用來充當建構函式,所以是無法 new 一個箭頭函式的。
當然,也有可能是面試官故意挖的一個坑,等著你往裡面跳。
85. promise 的其他方法有用過嗎?如 all、race。請說下這兩者的區別
參考答案:
promise.all 方法引數是一個 promise 的陣列,只有當所有的 promise 都完成並返回成功,才會呼叫 resolve,當有一個失敗,都會進catch,被捕獲錯誤,promise.all 呼叫成功返回的結果是每個 promise 單獨呼叫成功之後返回的結果組成的陣列,如果呼叫失敗的話,返回的則是第一個 reject 的結果
promise.race 也會呼叫所有的 promise,返回的結果則是所有 promise 中最先返回的結果,不關心是成功還是失敗。
86. class 是如何實現的
參考答案:
class 是 ES6 新推出的關鍵字,它是一個語法糖,本質上就是基於這個原型實現的。只不過在以前 ES5 原型實現的基礎上,添加了一些 _classCallCheck、_defineProperties、_createClass等方法來做出了一些特殊的處理。
例如:
class Hello { constructor(x) { this.x = x; } greet() { console.log("Hello, " + this.x) } }
``` "use strict";
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { console.log("Constructor::",Constructor); console.log("protoProps::",protoProps); console.log("staticProps::",staticProps); if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
var Hello = /#PURE/function () { function Hello(x) { _classCallCheck(this, Hello);
this.x = x;
}
_createClass(Hello, [{ key: "greet", value: function greet() { console.log("Hello, " + this.x); } }]);
return Hello; }(); ```
87. let、const、var 的區別
參考答案:
請參閱前面第 22 題答案。
88. ES6 中模組化匯入和匯出與 common.js 有什麼區別
參考答案:
CommonJs模組輸出的是值的拷貝,也就是說,一旦輸出一個值,模組內部的變化不會影響到這個值.
``` // common.js var count = 1;
var printCount = () =>{ return ++count; }
module.exports = { printCount: printCount, count: count }; // index.js let v = require('./common'); console.log(v.count); // 1 console.log(v.printCount()); // 2 console.log(v.count); // 1 ```
你可以看到明明common.js裡面改變了count,但是輸出的結果還是原來的。這是因為count是一個原始型別的值,會被快取。除非寫成一個函式,才能得到內部變動的值。將common.js裡面的module.exports 改寫成
module.exports = { printCount: printCount, get count(){ return count } };
這樣子的輸出結果是 1,2,2
而在ES6當中,寫法是這樣的,是利用export 和import匯入的
// es6.js export let count = 1; export function printCount() { ++count; } // main1.js import { count, printCount } from './es6'; console.log(count) console.log(printCount()); console.log(count)
ES6 模組是動態引用,並且不會快取,模組裡面的變數繫結其所有的模組,而是動態地去載入值,並且不能重新賦值,
ES6 輸入的模組變數,只是一個“符號連線符”,所以這個變數是隻讀的,對它進行重新賦值會報錯。如果是引用型別,變數指向的地址是隻讀的,但是可以為其新增屬性或成員。
另外還想說一個 export default
let count = 1; function printCount() { ++count; } export default { count, printCount} // main3.js import res form './main3.js' console.log(res.count)
export與export default的區別及聯絡:
- export與export default均可用於匯出常量、函式、檔案、模組等
- 你可以在其它檔案或模組中通過 import + (常量 | 函式 | 檔案 | 模組)名的方式,將其匯入,以便能夠對其進行使用
- 在一個檔案或模組中,export、import可以有多個,export default僅有一個
- 通過export方式匯出,在匯入時要加{ },export default則不需要。
89. 說一下普通函式和箭頭函式的區別
參考答案:
請參閱前面第 8、25、83 題答案。
90. 說一下 promise 和 async 和 await 什麼關係
參考答案:
await 表示式會造成非同步函式停止執行並且等待promise的解決,當值被resolved,非同步函式會恢復執行以及返回resolved值。如果該值不是一個promise,它將會被轉換成一個resolved後的promise。如果promise被rejected,await 表示式會丟擲異常值。
91. 說一下你學習過的有關 ES6 的知識點
參考答案:
這種題目是開放題,可以簡單列舉一下 ES6 的新增知識點。( ES6 的新增知識點參閱前面第 44 題)
然後說一下自己平時開發中用得比較多的是哪些即可。
一般面試官會針對你所說的內容進行二次提問。例如:你回答平時開發中箭頭函式用得比較多,那麼面試官極大可能針對箭頭函式展開二次提問,詢問你箭頭函式有哪些特性?箭頭函式 this 特點之類的問題。
92. 瞭解過 js 中 arguments 嗎?接收的是實參還是形參?
參考答案:
JS 中的 arguments 是一個偽陣列物件。這個偽陣列物件將包含呼叫函式時傳遞的所有的實參。
與之相對的,JS 中的函式還有一個 length 屬性,返回的是函式形參的個數。
93. ES6 相比於 ES5 有什麼變化
參考答案:
ES6 相比 ES5 新增了很多新特性,這裡可以自己簡述幾個。
具體的新增特性可以參閱前面第 44 題。
94. 強制型別轉換方法有哪些?
參考答案:
JavaScript 中的資料型別轉換,主要有三種方式:
- 轉換函式
js 提供了諸如 parseInt 和 parseFloat 這些轉換函式,通過這些轉換函式可以進行資料型別的轉換 。
- 強制型別轉換
還可使用強制型別轉換(type casting)處理轉換值的型別。
例如:
- Boolean(value) 把給定的值轉換成 Boolean 型;
- Number(value)——把給定的值轉換成數字(可以是整數或浮點數);
String(value)——把給定的值轉換成字串。
利用 js 變數弱型別轉換。
例如:
- 轉換字串:直接和一個空字串拼接,例如:
a = "" + 資料
- 轉換布林:!!資料型別,例如:
!!"Hello"
- 轉換數值:資料*1 或 /1,例如:
"Hello * 1"
95. 純函式
參考答案:
一個函式,如果符合以下兩個特點,那麼它就可以稱之為純函式:
- 對於相同的輸入,永遠得到相同的輸出
- 沒有任何可觀察到的副作用
解析:
針對上面的兩個特點,我們一個一個來看。
- 相同輸入得到相同輸出
我們先來看一個不純的反面典型:
``` let greeting = 'Hello'
function greet (name) { return greeting + ' ' + name }
console.log(greet('World')) // Hello World ```
上面的程式碼中,greet('World') 是不是永遠返回 Hello World ? 顯然不是,假如我們修改 greeting 的值,就會影響 greet 函式的輸出。即函式 greet 其實是 依賴外部狀態 的。
那我們做以下修改:
``` function greet (greeting, name) { return greeting + ' ' + name }
console.log(greet('Hi', 'Savo')) // Hi Savo ```
將 greeting 引數也傳入,這樣對於任何輸入引數,都有與之對應的唯一的輸出引數了,該函式就符合了第一個特點。
- 沒有副作用
副作用的意思是,這個函式的執行,不會修改外部的狀態。
下面再看反面典型:
``` const user = { username: 'savokiss' }
let isValid = false
function validate (user) { if (user.username.length > 4) { isValid = true } } ```
可見,執行函式的時候會修改到 isValid 的值(注意:如果你的函式沒有任何返回值,那麼它很可能就具有副作用!)
那麼我們如何移除這個副作用呢?其實不需要修改外部的 isValid 變數,我們只需要在函式中將驗證的結果 return 出來:
``` const user = { username: 'savokiss' }
function validate (user) { return user.username.length > 4; }
const isValid = validate(user) ```
這樣 validate 函式就不會修改任何外部的狀態了~
96. JS 模組化
參考答案:
模組化主要是用來抽離公共程式碼,隔離作用域,避免變數衝突等。
模組化的整個發展歷史如下:
IIFE: 使用自執行函式來編寫模組化,特點:在一個單獨的函式作用域中執行程式碼,避免變數衝突。
(function(){ return { data:[] } })()
AMD: 使用requireJS 來編寫模組化,特點:依賴必須提前宣告好。
define('./index.js',function(code){ // code 就是index.js 返回的內容 })
CMD: 使用seaJS 來編寫模組化,特點:支援動態引入依賴檔案。
define(function(require, exports, module) { var indexCode = require('./index.js'); });
CommonJS: nodejs 中自帶的模組化。
var fs = require('fs');
UMD:相容AMD,CommonJS 模組化語法。
webpack(require.ensure) :webpack 2.x 版本中的程式碼分割。
ES Modules: ES6 引入的模組化,支援import 來引入另一個 js 。
import a from 'a';
97. 看過 jquery 原始碼嗎?
參考答案:
開放題,但是需要注意的是,如果看過 jquery 原始碼,不要簡單的回答一個“看過”就完了,應該繼續乘勝追擊,告訴面試官例如哪個哪個部分是怎麼怎麼實現的,並針對這部分的原始碼實現,可以發表一些自己的看法和感想。
98. 說一下 js 中的 this
參考答案:
請參閱前面第 17 題答案。
99. apply call bind 區別,手寫
參考答案:
apply call bind 區別 ?
call 和 apply 的功能相同,區別在於傳參的方式不一樣:
- fn.call(obj, arg1, arg2, ...) 呼叫一個函式, 具有一個指定的 this 值和分別地提供的引數(引數的列表)。
- fn.apply(obj, [argsArray]) 呼叫一個函式,具有一個指定的 this 值,以及作為一個數組(或類陣列物件)提供的引數。
bind 和 call/apply 有一個很重要的區別,一個函式被 call/apply 的時候,會直接呼叫,但是 bind 會建立一個新函式。當這個新函式被呼叫時,bind( ) 的第一個引數將作為它執行時的 this,之後的一序列引數將會在傳遞的實參前傳入作為它的引數。
實現 call 方法:
``` Function.prototype.call2 = function (context) { //沒傳引數或者為 null 是預設是 window var context = context || (typeof window !== 'undefined' ? window : global) // 首先要獲取呼叫 call 的函式,用 this 可以獲取 context.fn = this var args = [] for (var i = 1; i < arguments.length; i++) { args.push('arguments[' + i + ']') } eval('context.fn(' + args + ')') delete context.fn }
// 測試 var value = 3 var foo = { value: 2 }
function bar(name, age) { console.log(this.value) console.log(name) console.log(age) } bar.call2(null) // 瀏覽器環境: 3 undefinde undefinde
// Node環境:undefinde undefinde undefindebar.call2(foo, 'cc', 18) // 2 cc 18 ```
實現 apply 方法:
``` Function.prototype.apply2 = function (context, arr) { var context = context || (typeof window !== 'undefined' ? window : global) context.fn = this;
var result; if (!arr) { result = context.fn(); } else { var args = []; for (var i = 0, len = arr.length; i < len; i++) { args.push('arr[' + i + ']'); } result = eval('context.fn(' + args + ')') }
delete context.fn return result; }
// 測試:
var value = 3 var foo = { value: 2 }
function bar(name, age) { console.log(this.value) console.log(name) console.log(age) } bar.apply2(null) // 瀏覽器環境: 3 undefinde undefinde
// Node環境:undefinde undefinde undefindebar.apply2(foo, ['cc', 18]) // 2 cc 18 ```
實現 bind 方法:
``` Function.prototype.bind2 = function (oThis) { if (typeof this !== "function") { // closest thing possible to the ECMAScript 5 internal IsCallable function throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); } var aArgs = Array.prototype.slice.call(arguments, 1), fToBind = this, fNOP = function () { }, fBound = function () { return fToBind.apply(this instanceof fNOP && oThis ? this : oThis || window, aArgs.concat(Array.prototype.slice.call(arguments))); };
fNOP.prototype = this.prototype; fBound.prototype = new fNOP();
return fBound; }
// 測試 var test = { name: "jack" } var demo = { name: "rose", getName: function () { return this.name; } }
console.log(demo.getName()); // 輸出 rose 這裡的 this 指向 demo
// 運用 bind 方法更改 this 指向 var another2 = demo.getName.bind2(test); console.log(another2()); // 輸出 jack 這裡 this 指向了 test 物件了 ```
100. 手寫 reduce flat
參考答案:
reduce 實現:
``` Array.prototype.my_reduce = function (callback, initialValue) { if (!Array.isArray(this) || !this.length || typeof callback !== 'function') { return [] } else { // 判斷是否有初始值 let hasInitialValue = initialValue !== undefined; let value = hasInitialValue ? initialValue : tihs[0]; for (let index = hasInitialValue ? 0 : 1; index < this.length; index++) { const element = this[index]; value = callback(value, element, index, this) } return value } }
let arr = [1, 2, 3, 4, 5] let res = arr.my_reduce((pre, cur, i, arr) => { console.log(pre, cur, i, arr) return pre + cur }, 10) console.log(res)//25 ```
flat 實現:
``` let arr = [1, [2, 3, [4, 5, [12, 3, "zs"], 7, [8, 9, [10, 11, [1, 2, [3, 4]]]]]]];
//萬能的型別檢測方法 const checkType = (arr) => { return Object.prototype.toString.call(arr).slice(8, -1); } //自定義flat方法,注意:不可以使用箭頭函式,使用後內部的this會指向window Array.prototype.myFlat = function (num) { //判斷第一層陣列的型別 let type = checkType(this); //建立一個新陣列,用於儲存拆分後的陣列 let result = []; //若當前物件非陣列則返回undefined if (!Object.is(type, "Array")) { return; } //遍歷所有子元素並判斷型別,若為陣列則繼續遞迴,若不為陣列則直接加入新陣列 this.forEach((item) => { let cellType = checkType(item); if (Object.is(cellType, "Array")) { //形參num,表示當前需要拆分多少層陣列,傳入Infinity則將多維直接降為一維 num--; if (num < 0) { let newArr = result.push(item); return newArr; } //使用三點運算子解構,遞迴函式返回的陣列,並加入新陣列 result.push(...item.myFlat(num)); } else { result.push(item); } }) return result; } console.time();
console.log(arr.flat(Infinity)); //[1, 2, 3, 4, 5, 12, 3, "zs", 7, 8, 9, 10, 11, 1, 2, 3, 4];
console.log(arr.myFlat(Infinity)); //[1, 2, 3, 4, 5, 12, 3, "zs", 7, 8, 9, 10, 11, 1, 2, 3, 4]; //自定義方法和自帶的flat返回結果一致!!!! console.timeEnd(); ``` 注: 由於字數限制,剩餘內容在下篇進行總結哦。🤞,大家認真看哦,奧利給!💪