通過 20 個棘手的ES6面試問題來提高咱們的 JS 技能
- 2019 年 11 月 12 日
- 筆記
作者:Alex 譯者:前端小智 來源:dev.to
ECMAScript 6(以下簡稱ES6)是 JS 語言的下一代標準,已經在2015年6月正式發布了。它的目標,是使得 JS 語言可以用來編寫複雜的大型應用程式,成為企業級開發語言。接下來咱們來看看 20 道棘手的面試題,通過做題,順帶提升一下咱們的 JS 的技能。
問題1:可以解釋一下 ES5 和ES6的區別嗎?
主題: JavaScript 難度: ⭐⭐⭐
ECMAScript 5 (ES5):ECMAScript 的第五版,於2009年標準化,該標準已在所有現代瀏覽器中完全支援。
ECMAScript 6 (ES6)/ ECMAScript 2015 (ES2015):ECMAscript 第 6 版,2015 年標準化。這個標準已經在大多數現代瀏覽器中部分實現。
以下是ES5和ES6之間的一些主要區別:
箭頭函數和字元串插值
const greetings = (name) => { return `hello ${name}`; }也可以這樣寫:
const greetings = name => `hello ${name}`; const:const 表示無法修改變數的原始值。需要注意的是,const表示對值的常量引用,咱們可以改變被引用的對象的屬性值,但不能改變引用本身。
const NAMES = []; NAMES.push("Jim"); console.log(NAMES.length === 1); // true NAMES = ["Steve", "John"]; // error 塊作用域:ES6 中 let, const 會創建塊級作用域,不會像 var 聲明變數一樣會被提升。
默認參數:默認參數使咱們可以使用默認值初始化函數。當參數省略或 undefined 時使用默認參數值。
function multiply (a, b = 2) { return a * b; } multiply(5); // 10 類定義與繼承
ES6 引入了對類(class關鍵字)、構造函數(constructor關鍵字)和 extend 關鍵字(用於繼承)的語言支援。
for-of 運算符
for...of 語句創建一個遍歷可迭代對象的循環。
展開操作符
const obj1 = { a: 1, b: 2 } const obj2 = { a: 2, c: 3, d: 4} const obj3 = {...obj1, ...obj2} Promises: Promises 提供了一種機制來處理非同步操作的結果和錯誤。可以使用回調來完成相同的事情,但是Promises 通過方法鏈接和簡潔的錯誤處理來提高可讀性。
const isGreater = (a, b) => { return new Promise ((resolve, reject) => { if(a > b) { resolve(true) } else { reject(false) } }) } isGreater(1, 2) .then(result => { console.log('greater') }) .catch(result => { console.log('smaller') }) 模組導出
const myModule = { x: 1, y: () => { console.log('This is ES5') }} export default myModule; 和導入
import myModule from './myModule';
問題 2:什麼是 IIFE (立即調用的函數表達式)
主題: JavaScript 難度: ⭐⭐⭐
IIFE是一個立即調用的函數表達式,它在創建後立即執行
(function IIFE(){ console.log( "Hello!" ); })(); // "Hello!"常常使用此模式來避免污染全局命名空間,因為在IIFE中使用的所有變數(與任何其他普通函數一樣)在其作用域之外都是不可見的。
問題 3:何時在 ES6 中使用箭頭函數?
主題: JavaScript 難度: ⭐⭐⭐
以下是一些經驗分享:
- 在全局作用域內和
Object.prototype屬性中使用function。 - 為對象構造函數使用
class。 - 其它情況使用箭頭函數。
為啥大多數情況都使用箭頭函數?
- 作用域安全性:當箭頭函數被一致使用時,所有東西都保證使用與根對象相同的
thisObject。如果一個標準函數回調與一堆箭頭函數混合在一起,那麼作用域就有可能變得混亂。 - 緊湊性:箭頭函數更容易讀寫。
- 清晰度:使用箭頭函數可明確知道當前
this指向。
問題 4:將 Symbol 引入ES6 的目的是什麼?
主題: JavaScript 難度: ⭐⭐⭐
Symbol 是一種新的、特殊的對象,可以用作對象中惟一的屬性名。使用 Symbol 替換string 可以避免不同的模組屬性的衝突。還可以將Symbol設置為私有,以便尚無直接訪問Symbol許可權的任何人都不能訪問它們的屬性。
Symbol 是JS新的基本數據類型。與number、string和boolean 原始類型一樣,Symbol 也有一個用於創建它們的函數。與其他原始類型不同,Symbol沒有字面量語法。創建它們的唯一方法是使用以下方法中的Symbol構造函數
let symbol = Symbol();
問題 5: 在 ES6 中使用展開(spread)語法有什麼好處? 它與剩餘(rest)語法有什麼不同?
主題: JavaScript 難度: ⭐⭐⭐
ES6 的展開語法在以函數形式進行編碼時非常有用,因為咱們可以輕鬆地創建數組或對象的副本,而無需求助於Object.create,slice或庫函數。Redux 和rx.js項目中經常使用此特性。
function putDookieInAnyArray(arr) { return [...arr, 'dookie']; } const result = putDookieInAnyArray(['I', 'really', "don't", 'like']); // ["I", "really", "don't", "like", "dookie"] const person = { name: 'Todd', age: 29, }; const copyOfTodd = { ...person }; ES6 的 rest 語法提供了一種捷徑,其中包括要傳遞給函數的任意數量的參數。
就像展開語法的逆過程一樣,它將數據放入並填充到數組中而不是展開數組,並且它在函數變數以及數組和對象解構分中也經常用到。
function addFiveToABunchOfNumbers(...numbers) { return numbers.map(x => x + 5); } const result = addFiveToABunchOfNumbers(4, 5, 6, 7, 8, 9, 10); // [9, 10, 11, 12, 13, 14, 15] const [a, b, ...rest] = [1, 2, 3, 4]; // a: 1, b: 2, rest: [3, 4] const { e, f, ...others } = { e: 1, f: 2, g: 3, h: 4, }; // e: 1, f: 2, others: { g: 3, h: 4 } 問題 6: ES6 類和 ES5 函數構造函數有什麼區別?
主題: JavaScript 難度: ⭐⭐⭐
// ES5 Function Constructor function Person(name) { this.name = name; } // ES6 Class class Person { constructor(name) { this.name = name; } } 對於簡單的構造函數,它們看起來非常相似。
構造函數的主要區別在於使用繼承。如果咱們創建一個繼承Person類的Student子類並添加一個studentId欄位,以下是兩種方式的使用:
// ES5 Function Constructor function Student(name, studentID) { // 調用你類的構造函數以初始化你類派生的成員。 Person.call(this. name) // 初始化子類的成員。 this.studentId = studentId } Student.prototype = Object.create(Person.prototype) Student.prototype.constructor = Student // ES6 Class class Student extends Person { constructor(name, studentId) { super(name) this.studentId = studentId } }在 ES5 中使用繼承要複雜得多,而且 ES6 版本更容易理解和記住。
問題 7: .call 和 .apply 區別是啥?
主題: JavaScript 難度: ⭐⭐⭐
.call和.apply均用於調用函數,並且第一個參數將用作函數中this的值。但是,.call將逗號分隔的參數作為下一個參數,而.apply將參數數組作為下一個參數。簡單記憶法:C用於call和逗號分隔,A用於apply和參數數組。
function add(a, b) { return a + b; } console.log(add.call(null, 1, 2)); // 3 console.log(add.apply(null, [1, 2])); // 3 問題 8: 為什麼要使用 ES6 類?
主題: JavaScript 難度: ⭐⭐⭐
選擇使用類的一些原因:
- 語法更簡單,更不容易出錯。
- 使用新語法比使用舊語法更容易(而且更不易出錯)地設置繼承層次結構。
-
class可以避免構造函數中使用new的常見錯誤(如果構造函數不是有效的對象,則使構造函數拋出異常)。 - 用新語法調用父原型方法的版本比舊語法要簡單得多,用
super.method()代替ParentConstructor.prototype.method.call(this)或Object.getPrototypeOf(Object.getPrototypeOf(this)).method.call(this)
問題 9: 在 JS 中定義枚舉的首選語法是什麼
主題: JavaScript 難度: ⭐⭐⭐
可以 Object.freeze 來實現枚舉
var DaysEnum = Object.freeze({ "monday": 1, "tuesday": 2, "wednesday": 3, ... })或者
var DaysEnum = { "monday": 1, "tuesday": 2, "wednesday": 3, ... } Object.freeze(DaysEnum)但是,這阻止咱們把值分配給變數:
let day = DaysEnum.tuesday day = 298832342 // 不會報錯
問題 10: 解釋一下 Object.freeze() 和 const 的區別
主題: JavaScript 難度: ⭐⭐⭐
const和Object.freeze是兩個完全不同的概念。
const 聲明一個只讀的變數,一旦聲明,常量的值就不可改變:
const person = { name: "Leonardo" }; let animal = { species: "snake" }; person = animal; // ERROR "person" is read-only Object.freeze適用於值,更具體地說,適用於對象值,它使對象不可變,即不能更改其屬性。
let person = { name: "Leonardo" }; let animal = { species: "snake" }; Object.freeze(person); person.name = "Lima"; //TypeError: Cannot assign to read only property 'name' of object console.log(person); 問題 11: JS 的提升是什麼
主題: JavaScript 難度: ⭐⭐⭐⭐
提升是指 JS 解釋器將所有變數和函數聲明移動到當前作用域頂部的操作,提升有兩種類型
- 變數提升
- 函數提升
只要一個var(或函數聲明)出現在一個作用域內,這個聲明就被認為屬於整個作用域,並且可以在任何地方訪問。
var a = 2 foo() // 正常運行, foo 已被提升 function foo() { a = 3 console.log(a) // 3 var a } console.log( a ) // 2問題 12: 解釋一下原型設計模式(Prototype Pattern)
主題: JavaScript 難度: ⭐⭐⭐⭐
原型模式會創建新的對象,而不是創建未初始化的對象,它會返回使用從原型或樣本對象複製的值進行初始化的對象。原型模式也稱為屬性模式。
原型模式有用的一個例子是使用與資料庫中的默認值匹配的值初始化業務對象。原型對象保留默認值,這些默認值將被複制到新創建的業務對象中。
傳統語言很少使用原型模式,但是JavaScript作為一種原型語言,在構建新對象及其原型時使用這種模式。
問題 13: ES6 中的臨時死區是什麼
主題: JavaScript 難度: ⭐⭐⭐⭐
在 ES6 中,let 和const 跟 var、class和function一樣也會被提升,只是在進入作用域和被聲明之間有一段時間不能訪問它們,這段時間是臨時死區(TDZ)。
//console.log(aLet) // would throw ReferenceError let aLet; console.log(aLet); // undefined aLet = 10; console.log(aLet); // 10
問題 14: 什麼時候不使用箭頭函數? 說出三個或更多的例子
主題: JavaScript 難度: ⭐⭐⭐⭐
不應該使用箭頭函數一些情況:
- 當想要函數被提升時(箭頭函數是匿名的)
- 要在函數中使用
this/arguments時,由於箭頭函數本身不具有this/arguments,因此它們取決於外部上下文 - 使用命名函數(箭頭函數是匿名的)
- 使用函數作為構造函數時(箭頭函數沒有構造函數)
- 當想在對象字面是以將函數作為屬性添加並在其中使用對象時,因為咱們無法訪問
this即對象本身。
問題 15: ES6 中的 WeakMa p的實際用途是什麼?
主題: JavaScript 難度: ⭐⭐⭐⭐
WeakMaps 提供了一種從外部擴展對象而不影響垃圾收集的方法。當咱們想要擴展一個對象,但是因為它是封閉的或者來自外部源而不能擴展時,可以應用WeakMap。
WeakMap只適用於 ES6 或以上版本。WeakMap是鍵和值對的集合,其中鍵必須是對象。
var map = new WeakMap(); var pavloHero = { first: "Pavlo", last: "Hero" }; var gabrielFranco = { first: "Gabriel", last: "Franco" }; map.set(pavloHero, "This is Hero"); map.set(gabrielFranco, "This is Franco"); console.log(map.get(pavloHero)); //This is HeroWeakMaps的有趣之處在於,它包含了對map內部鍵的弱引用。弱引用意味著如果對象被銷毀,垃圾收集器將從WeakMap中刪除整個條目,從而釋放記憶體。
問題 16: 說明下列方法為何不能用作 IIFE,要使其成為 IIFE,需要進行哪些更改?
主題: JavaScript 難度: ⭐⭐⭐⭐
function foo(){ }();IIFE 代表立即調用的函數表達式。JS解析器讀取函數foo(){}();作為函數foo(){}和();,前者是一個函數聲明,後者(一對括弧)是嘗試調用一個函數,但沒有指定名稱,因此它拋出Uncaught SyntaxError: Unexpected token 異常。
咱們可以使用void操作符:void function foo(){ }();。不幸的是,這種方法有一個問題。給定表達式的求值總是undefined的,所以如果IIFE 函數有返回值,則不能使用它,如下所示:
const foo = void function bar() { console.log('前端小智') return 'foo'; }(); console.log(foo); // undefined問題 17: 能否比較模組模式與構造函數/原型模式的用法?
主題: JavaScript 難度: ⭐⭐⭐⭐
模組模式通常用於命名空間,在該模式中,使用單個實例作為存儲來對相關函數和對象進行分組。這是一個不同於原型設計的用例,它們並不是相互排斥,咱們可以同時使用它們(例如,將一個構造函數放在一個模組中,並使用new MyNamespace.MyModule.MyClass(arguments) )。
構造函數和原型是實現類和實例的合理方法之一。它們與模型並不完全對應,因此通常需要選擇一個特定的scheme或輔助方法來實現原型中的類。
問題 18: ES6 Map 和 WeakMap 有什麼區別?
主題: JavaScript 難度: ⭐⭐⭐⭐⭐
當它們的鍵/值引用的對象被刪除時,它們的行為都不同,以下面的程式碼為例:
var map = new Map() var weakmap = new WeakMap() (function() { var a = { x: 12 }; var b = { y: 12 }; map.set(a, 1); weakmap.set(b, 2); })()執行上面的 IIFE,就無法再引用{x:12}和{y:12}。垃圾收集器繼續運行,並從 WeakMa中刪除鍵b指針,還從記憶體中刪除了{y:12}。
但在使用 Map的情況下,垃圾收集器不會從Map中刪除指針,也不會從記憶體中刪除{x:12}。
WeakMap 允許垃圾收集器執行其回收任務,但Map不允許。對於手動編寫的 Map,數組將保留對鍵對象的引用,以防止被垃圾回收。但在WeakMap中,對鍵對象的引用被「弱」保留,這意味著在沒有其他對象引用的情況下,它們不會阻止垃圾回收。
問題 19: 舉一個柯里化函數的例子,並說明柯里化的好處?
主題: JavaScript 難度: ⭐⭐⭐⭐⭐
柯里化是一種模式,其中一個具有多個參數的函數被分解成多個函數,當被串聯調用時,這些函數將一次累加一個所需的所有參數。這種技術有助於使用函數式編寫的程式碼更容易閱讀和編寫。需要注意的是,要實現一個函數,它需要從一個函數開始,然後分解成一系列函數,每個函數接受一個參數。
function curry(fn) { if (fn.length === 0) { return fn; } function _curried(depth, args) { return function(newArgument) { if (depth - 1 === 0) { return fn(...args, newArgument); } return _curried(depth - 1, [...args, newArgument]); }; } return _curried(fn.length, []); } function add(a, b) { return a + b; } var curriedAdd = curry(add); var addFive = curriedAdd(5); var result = [0, 1, 2, 3, 4, 5].map(addFive); // [5, 6, 7, 8, 9, 10]問題 20: 如何在 JS 中「深凍結」對象
主題: JavaScript 難度: ⭐⭐⭐⭐⭐
如果咱們想要確保對象被深凍結,就必須創建一個遞歸函數來凍結對象類型的每個屬性:
沒有深凍結
let person = { name: "Leonardo", profession: { name: "developer" } }; Object.freeze(person); person.profession.name = "doctor"; console.log(person); //output { name: 'Leonardo', profession: { name: 'doctor' } }深凍結
function deepFreeze(object) { let propNames = Object.getOwnPropertyNames(object); for (let name of propNames) { let value = object[name]; object[name] = value && typeof value === "object" ? deepFreeze(value) : value; } return Object.freeze(object); } let person = { name: "Leonardo", profession: { name: "developer" } }; deepFreeze(person); person.profession.name = "doctor"; // TypeError: Cannot assign to read only property 'name' of object

