《前端面試手記》之ES6重難點整理

👇 內容速覽 👇

  • let和const
  • Set和Map
  • Generator和yield
  • Promise、async/await介紹
  • Proxy代理器
  • ...

🔍查看全部教程 / 閱讀原文🔍

let和const

ES6新增了letconst,它們聲明的變量,都處於“塊級作用域”。並且不存在“變量提升”,不允許重複聲明。

同時,const聲明的變量所指向的內存地址保存的數據不得改變:

  • 對於簡單類型的數據(數值、字符串、布爾值),值就保存在變量指向的那個內存地址,因此等同於常量。
  • 對於複合類型的數據(主要是對象和數組),變量指向的內存地址,保存的只是一個指向實際數據的指針,const只能保證這個指針是固定的(即總是指向另一個固定的地址),不能保證指向的數據結構不可變。

如果要保證指向的數據結構也不可變,需要自行封裝:

/**
 * 凍結對象
 * @param {Object} obj 
 * @return {Object}
 */
function constantize(obj) {
  if(Object.isFrozen(obj)) {
    return obj
  }

  Reflect.ownKeys(obj).forEach(key => {
    // 如果屬性是對象,遞歸凍結
    typeof obj[key] === 'object' && (obj[key] = constantize(obj[key]))
  });

  return Object.freeze(obj)
}

/********測試代碼 **********/

const obj = {
  a: 1,
  b: {
    c: 2,
    d: {
      a: 1
    }
  },
  d: [
    1,
    2
  ]
}

const fronzenObj = constantize(obj)
try {
  fronzenObj.d = []
  fronzenObj.b.c = 3
} catch(error) {
  console.log(error.message)
}

Set和Map

題目:解釋下SetMap
  • Set元素不允許重複
  • Map類似對象,但是它的鍵(key)可以是任意數據類型

①Set常用方法

// 實例化一個set
const set = new Set([1, 2, 3, 4]);

// 遍歷set
for (let item of set) {
  console.log(item);
}

// 添加元素,返回Set本身
set.add(5).add(6);

// Set大小
console.log(set.size);

// 檢查元素存在
console.log(set.has(0));

// 刪除指定元素,返回bool
let success = set.delete(1);
console.log(success);

set.clear();

其他遍歷方法:由於沒有鍵名,values()keys()返回同樣結果。

for (let item of set.keys()) {
  console.log(item);
}

for (let item of set.values()) {
  console.log(item);
}

for (let item of set.entries()) {
  console.log(item);
}

②Map常用方法

Map接口基本和Set一致。不同的是增加新元素的API是:set(key, value)

const map = new Map();

// 以任意對象爲 Key 值
// 這裏以 Date 對象爲例
let key = new Date(); 
map.set(key, "today");

console.log(map.get(key));

Generator與yield

generator函數是es6提供的新特性,它的最大特點是:控制函數的執行。讓我們從網上最火的一個例子來看:

function* foo(x) {
  var y = 2 * (yield x + 1);
  var z = yield y / 3;
  return x + y + z;
}

var b = foo(5);
b.next(); // { value:6, done:false }
b.next(12); // { value:8, done:false }
b.next(13); // { value:42, done:true }

通俗的解釋下爲什麼會有這種輸出:

  1. 給函數foo傳入參數5,但由於它是generator,所以執行到第一個yield前就停止了。
  2. 第一次調用next(),這次傳入的參數會被忽略暫停**。
  3. 第二次調用next(12),傳入的參數會被當作上一個yield表達式的返回值。因此,y = 2 * 12 = 24。執行到第二個yield,返回其後的表達式的值 24 / 3 = 8。然後函數在此處暫停。
  4. 第三次調用next(13),沒有yield,只剩return了,按照正常函數那樣返回return的表達式的值,並且donetrue

難點:在於爲什麼最後的value是42呢?

首先,x的值是剛開始調用foo函數傳入的5。而最後傳入的13被當作第二個yield的返回值,所以z的值是13。對於y的值,我們在前面第三步中已經計算出來了,就是24。

所以,x + y + z = 5 + 24 + 13 = 42

看懂了上面的分析,再看下面這段代碼就很好理解了:

function* foo(x) {
  var y = 2 * (yield x + 1);
  var z = yield y / 3;
  return x + y + z;
}

var a = foo(5);
a.next(); // Object{value:6, done:false}
a.next(); // Object{value:NaN, done:false}
a.next(); // Object{value:NaN, done:true}

只有第一次調用next函數的時候,輸出的value是6。其他時候由於沒有給next傳入參數,因此yield的返回值都是undefined,進行運算後自然是NaN

Promise介紹

簡單歸納下 Promise:三個狀態、兩個過程、一個方法

  • 三個狀態:pendingfulfilledrejected
  • 兩個過程(單向不可逆):

    • pending->fulfilled
    • pending->rejected
  • 一個方法thenPromise本質上只有一個方法,catchall方法都是基於then方法實現的。

請看下面這段代碼:

// 構造 Promise 時候, 內部函數立即執行
new Promise((resolve, reject) => {
  console.log("new Promise");
  resolve("success");
});
console.log("finifsh");

//  then 中 使用了 return,那麼 return 的值會被 Promise.resolve() 包裝
Promise.resolve(1)
  .then(res => {
    console.log(res); // => 1
    return 2; // 包裝成 Promise.resolve(2)
  })
  .then(res => {
    console.log(res); // => 2
  });

async/await介紹

async函數返回一個Promise對象,可以使用then方法添加回調函數。

當函數執行的時候,一旦遇到await就會先返回,等到異步操作完成,再接着執行函數體內後面的語句。

這也是它最受歡迎的地方:能讓異步代碼寫起來像同步代碼,並且方便控制順序

可以利用它實現一個sleep函數阻塞進程:

function sleep(millisecond) {
  return new Promise(resolve => {
    setTimeout(() => resolve, millisecond)
  })
}

/**
 * 以下是測試代碼
 */
async function test() {
  console.log('start')
  await sleep(1000) // 睡眠1秒
  console.log('end')
}

test() // 執行測試函數

雖然方便,但是它也不能取代Promise,尤其是我們可以很方便地用Promise.all()來實現併發,而async/await只能實現串行。

function sleep(second) {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log(Math.random());
      resolve();
    }, second);
  });
}

async function chuanXingDemo() {
  await sleep(1000);
  await sleep(1000);
  await sleep(1000);
}


async function bingXingDemo() {
  var tasks = [];
  for (let i = 0; i < 3; ++i) {
    tasks.push(sleep(1000));
  }

  await Promise.all(tasks);
}

運行bingXingDemo(),幾乎同時輸出,它是併發執行;運行chuanXingDemo(),每個輸出間隔1s,它是串行執行。

ES6對象和ES5對象

題目:es6 class 的new實例和es5的new實例有什麼區別?

ES6中(和ES5相比),classnew實例有以下特點:

  • class的構造參數必須是new來調用,不可以將其作爲普通函數執行
  • es6class不存在變量提升
  • 最重要的是:es6內部方法不可以枚舉。es5的prototype上的方法可以枚舉。

爲此我做了以下測試代碼進行驗證:

console.log(ES5Class()) // es5:可以直接作爲函數運行
// console.log(new ES6Class()) // 會報錯:不存在變量提升

function ES5Class(){
  console.log("hello")
}

ES5Class.prototype.func = function(){ console.log("Hello world") }

class ES6Class{
  constructor(){}
  func(){
    console.log("Hello world")
  }
}

let es5 = new ES5Class()
let es6 = new ES6Class()

// 推薦在循環對象屬性的時候,使用for...in
// 在遍歷數組的時候的時候,使用for...of
console.log("ES5 :")
for(let _ in es5){
  console.log(_)
}

// es6:不可枚舉
console.log("ES6 :")
for(let _ in es6){
  console.log(_)
}

參考/推薦《JavaScript創建對象—從es5到es6》

Proxy代理器

他可以實現js中的“元編程”:在目標對象之前架設攔截,可以過濾和修改外部的訪問。

它支持多達13種攔截操作,例如下面代碼展示的setget方法,分別可以在設置對象屬性和訪問對象屬性時候進行攔截。

const handler = {
  // receiver 指向 proxy 實例
  get(target, property, receiver) {
    console.log(`GET: target is ${target}, property is ${property}`)
    return Reflect.get(target, property, receiver)
  },
  set(target, property, value, receiver) {
    console.log(`SET: target is ${target}, property is ${property}`)
    return Reflect.set(target, property, value)
  }
}

const obj = { a: 1 , b: {c: 0, d: {e: -1}}}
const newObj = new Proxy(obj, handler)

/**
 * 以下是測試代碼
 */

newObj.a // output: GET...
newObj.b.c // output: GET...

newObj.a = 123 // output: SET...
newObj.b.c = -1 // output: GET...

運行這段代碼,會發現最後一行的輸出是 GET ...。也就是說它觸發的是get攔截器,而不是期望的set攔截器。這是因爲對於對象的深層屬性,需要專門對其設置Proxy

更多請見《阮一峯ES6入門:Proxy》

EsModule和CommonJS的比較

目前js社區有4種模塊管理規範:AMD、CMD、CommonJS和EsModule。 ES Module 是原生實現的模塊化方案,與 CommonJS 有以下幾個區別:

  • CommonJS 支持動態導入,也就是 require(${path}/xx.js),後者目前不支持,但是已有提案:import(xxx)
  • CommonJS 是同步導入,因爲用於服務端,文件都在本地,同步導入即使卡住主線程影響也不大。而後者是異步導入,因爲用於瀏覽器,需要下載文件,如果也採用同步導入會對渲染有很大影響
  • commonJs輸出的是值的淺拷貝,esModule輸出值的引用
  • ES Module 會編譯成 require/exports 來執行的

更多系列文章

⭐在GitHub上收藏/訂閱⭐

《前端知識體系》

《設計模式手冊》

《Webpack4漸進式教程》

⭐在GitHub上收藏/訂閱⭐

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