我理解的ES6

yvonneit發表於2019-04-08

前言

我是通過阮一峰老師的ES6教程入門的,基本上是把ES6的幾個核心特性過了一遍,但是面試官一問深我就???了,還是實際運用的太少。

本篇文章也偏總結類,結合我親身經歷的高頻面試題,建議大家必須要對箭頭函式、Promise、Generator、async等內容深入理解。

工具:Babel是一個 ES6 轉碼器,可以將 ES6 程式碼轉為 ES5 程式碼,以便相容那些還沒支援ES6的平臺。

String字串優化

新增了字串模板,在拼接大段字串時,用反斜槓取代以往的字串相加的形式,能保留所有空格和換行,使得字串拼接看起來更加直觀,更加優雅。

新增了includes()方法,用於取代傳統的只能用indexOf查詢包含字元的方法, 此外還新增了startsWith(), endsWith(), padStart(),padEnd(),repeat()等方法,可方便的用於查詢,補全字串。

Array陣列優化

陣列解構賦值: 如ES6可以直接以let [a,b,c] = [1,2,3]形式進行變數賦值,對映關係更清晰。

擴充套件運算子

  • 可以將一個陣列轉為用逗號分隔的引數序列。
console.log(...[1, 2, 3])  // 1 2 3
複製程式碼
  • 可以實現陣列的複製和解構賦值 (let a = [2,3,4]; let b = [...a])

  • 可以取代arguments物件和apply方法,輕鬆獲取未知引數個數情況下的引數集合。

使用擴充套件運算子替代函式的apply方法:

// ES5 的寫法
function f(x, y, z) {
  // ...
}
var args = [0, 1, 2];
f.apply(null, args);
// ES6 寫法
let args = [0, 1, 2];
f(...args);
複製程式碼

JS中遍歷陣列的方法:

  1. for迴圈
  2. forEach
myArray.forEach(function(value){
    console.log(value);
})
複製程式碼

無法中途跳出forEach迴圈,break命令或return命令都不能奏效。

  1. for... in

for…in主要是為遍歷物件而設計的,不適用於遍歷陣列。

遍歷陣列的缺點:

  • 陣列的鍵名是數字,但是for…in迴圈是以字串作為鍵名的。
  • 某些情況下,for…in迴圈會以任意順序遍歷鍵名。
  1. for...of (ES6)
for(let value of myArray){
    console.log(value);
}
複製程式碼
  • 不同用於forEach方法,它可以與break、continue和return配合使用。
  • 提供了遍歷所有資料結構的統一操作介面。

for of 和 for in 的總結:

  • 推薦在迴圈物件屬性的時候,使用for...in, 在遍歷陣列的時候的時候使用for...of。
  • for...in迴圈出的是key,for...of迴圈出的是value
  • 注意,for...of是ES6新引入的特性。修復了ES5引入的for...in的不足
  • for...of不能迴圈普通的物件,需要通過和Object.keys()搭配使用

Object型別優化

  1. 物件屬性變數式宣告:

ES6可以直接以變數形式宣告物件屬性或者方法。比傳統的鍵值對形式宣告更加簡潔,更加方便,語義更加清晰。

let [apple, orange] = ['red appe', 'yellow orange'];
let myFruits = {apple, orange};    
// let myFruits = {apple: 'red appe', orange: 'yellow orange'};
複製程式碼
  1. 物件的擴充套件運算子(...)

可將一個陣列轉為用逗號分隔的引數序列,主要用於函式呼叫。 console.log(...[1, 2, 3]) // 1 2 3

  1. super 關鍵字

ES6在Class類裡新增了類似this的關鍵字super。同this總是指向當前函式所在的物件不同,super關鍵字總是指向當前函式所在物件的原型物件。

箭頭函式

基本使用:

如果 return 值就只有一行表示式,可以省去 return,預設表示該行是返回值,否則需要加一個大括號和 return。如果引數只有一個,也可以省去括號,兩個則需要加上括號。

var f = v => v*2;
// 等價於
var f = function(v){
  return v*2;
}

// 判斷偶數
var isEven = n => n % 2 == 0;

// 需要加 return
var = (a, b) => {
  if(a >= b)
    return a;
  return b;
}
複製程式碼

ES6 引入 rest 引數(形式為...變數名),用於獲取函式的多餘引數,這樣就不需要使用arguments物件了。

rest 引數搭配的變數是一個陣列,該變數將多餘的引數放入陣列中。

//利用 rest 引數,可以向該函式傳入任意數目的引數。
function add(...values) {
  let sum = 0;
  for (var val of values) {
    sum += val;
  }
  return sum;
}
add(2, 5, 3) // 10
複製程式碼

面試問題:箭頭函式和普通函式的區別

  1. 箭頭函式沒有自己的this物件

函式中的 this 始終是指向函式執行時所在的物件。比如全域性函式執行時,this 指向 window,物件的方法執行時,this 指向該物件,這就是函式 this 的可變。

而箭頭函式中的 this 是固定的,箭頭函式繼承自己作用域的上一層的this,就是上一級外部函式的 this 的指向。任何方法都改變不了其指向,如call(), bind(), apply()。

一個例子:

function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}

var id = 21;
foo.call({ id: 42 });  // id: 42
複製程式碼

執行的結果是 42 而不是全域性的 21,表示 setTimeout 函式執行的時候,this 指向的不是 window。因此箭頭函式很好地解決了匿名函式和setTimeoutsetInterval的this指向問題,不用再去給其用that變數儲存this。

對箭頭函式中關於 this 的總結:在物件的方法中直接使用箭頭函式,會指向 window,其他箭頭函式 this 會指向上一層的 this,箭頭函式並沒有儲存 this。

var obj = {
  id: 1,
  foo: ()=>{
    return this.id;
  }
}
var id = 2;
obj.foo(); // 2
複製程式碼
  1. 箭頭函式不能當做建構函式,不能使用new,因為它沒有自己的this,無法例項化。
  2. 箭頭函式不繫結arguments, 取而代之用rest引數(形式為...變數名)。也沒有supernew.target
  3. 不可以使用yield命令,箭頭函式不可用作Generator函式。
  4. 箭頭函式沒有原型屬性。

Set 和 Map

  1. Set

ES6引入的一種類似Array的新的資料結構,Set例項的成員類似於陣列item成員,區別是Set例項的成員都是唯一,不重複的。這個特性可以輕鬆地實現陣列去重

Set本身是一個建構函式,用來生成 Set 資料結構。

const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));

for (let i of s) {
  console.log(i);
}
// 2 3 5 4
複製程式碼
  1. Map

JavaScript 的物件(Object),本質上是鍵值對的集合,但是傳統上只能用字串當作鍵。這給它的使用帶來了很大的限制。

Map 是ES6引入的一種類似Object的新的資料結構,也是鍵值對的集合,但是“鍵”的範圍不限於字串,各種型別的值(包括物件)都可以當作鍵。

const m = new Map();
const o = {p: 'Hello World'};

m.set(o, 'content')
m.get(o) // "content"
複製程式碼

let 和 const

沒有塊級作用域回來帶很多難以理解的問題,比如for迴圈var變數洩露,變數覆蓋等問題。

let 宣告的變數擁有自己的塊級作用域,形如for (let x...)的迴圈在每次迭代時都為x建立新的繫結。且修復了var宣告變數帶來的變數提升問題。(必須宣告 'use strict' 後才能使用let宣告變數,否則瀏覽並不能顯示結果)

“變數提升”現象:即變數可以在宣告之前使用,值為undefined。為了糾正這種現象,let命令改變了語法行為,它所宣告的變數一定要在宣告後使用,否則報錯。

ES5 只有全域性作用域和函式作用域,沒有塊級作用域。

塊級作用域的出現,實際上使得獲得廣泛應用的立即執行函式表示式(IIFE)不再必要了。

// IIFE 寫法
(function () {
  var tmp = ...;
  ...
}());

// 塊級作用域寫法
{
  let tmp = ...;
  ...
}
複製程式碼

const 宣告一個只讀的常量。一旦宣告,常量的值就不能改變。 const 宣告的變數不得改變值,這意味著,const一旦宣告變數,就必須立即初始化。

const的作用域與let命令相同:只在宣告所在的塊級作用域內有效。

總結: 使用var宣告的變數,其作用域為該語句所在的函式內,且存在變數提升現象; 使用let宣告的變數,其作用域為該語句所在的程式碼塊內,不存在變數提升; 使用const宣告的是常量,在後面出現的程式碼中不能再修改該常量的值。

Promise

主要作用是用來解決JS回撥機制產生的“回撥地獄”。 回撥地獄帶來的負面作用有以下幾點:

  • 程式碼臃腫, 可讀性差, 複用性差, 容易滋生 bug。
  • 耦合度過高,可維護性差。
  • 只能在回撥裡處理異常。

Promise它不是新的語法功能,而是一種新的寫法,將回撥函式的巢狀,改成鏈式呼叫。

new Promise(請求1)
    .then(請求2(請求結果1))
    .then(請求3(請求結果2))
    .catch(處理異常(異常資訊))
複製程式碼

Promise 使用總結:

  1. 可以通過兩種方式初始化一個 Promise 物件,都會返回一個 Promise 物件。
  • new Promise(fn)
  • Promise.resolve(fn)
  1. 然後呼叫上一步返回的 promise 物件的 then 方法,註冊回撥函式。 then 中的回撥函式可以有一個引數,也可以不帶引數。如果 then 中的回撥函式依賴上一步的返回結果,那麼要帶上引數。

  2. 最後註冊 catch 異常處理函式,處理前面回撥中可能丟擲的異常。

簡單例子:

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, 'done');
  });
}

timeout(100).then((value) => {
  console.log(value);
});
複製程式碼

timeout方法返回一個Promise例項,表示一段時間以後才會發生的結果。過了指定的時間(ms引數)以後,Promise例項的狀態變為resolved,就會觸發then方法繫結的回撥函式。

一些常用API:

Promise.race

類方法,多個 Promise 任務同時執行,返回最先執行結束的 Promise 任務的結果,不管這個 Promise 結果是成功還是失敗。

Promise.all

類方法,多個 Promise 任務同時執行。 如果全部成功執行,則以陣列的方式返回所有 Promise 任務的執行結果。 如果有一個 Promise 任務 rejected,則只返回 rejected 任務的結果。

如果後續任務是非同步任務的話,必須return 一個 新的 promise 物件。如果後續任務是同步任務,只需 return 一個結果即可。

new Promise(買菜)
//用買好的菜做飯
.then((買好的菜)=>{
    return new Promise(做飯);
})
複製程式碼

一個 Promise 物件有三個狀態,並且狀態一旦改變,便不能再被更改為其他狀態:

  • pending,非同步任務正在進行。
  • resolved (也可以叫fulfilled),非同步任務執行成功。
  • rejected,非同步任務執行失敗。

generator 以及 async/await 語法使非同步處理更加接近同步程式碼寫法,可讀性更好,同時異常捕獲和同步程式碼的書寫趨於一致。

(async ()=>{
    let 蔬菜 = await 買菜();
    let 飯菜 = await 做飯(蔬菜);
    let 送飯結果 = await 送飯(飯菜);
    let 通知結果 = await 通知我(送飯結果);
})();
複製程式碼

Generator

Generator 函式會返回一個遍歷器物件,可以依次遍歷 Generator 函式內部的每一個狀態。

形式上,Generator 函式是一個普通函式,但是有兩個特徵。

  • function關鍵字與函式名之間有一個星號;
  • 函式體內部使用yield表示式,定義不同的內部狀態。
function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();

hw.next()
// { value: 'hello', done: false }
複製程式碼

Generator 函式的呼叫方法與普通函式一樣,也是在函式名後面加上一對圓括號。不同的是,呼叫 Generator 函式後,該函式並不執行,返回的也不是函式執行結果,而是一個指向內部狀態的指標物件,也就是上一章介紹的遍歷器物件。

下一步,必須呼叫遍歷器物件的next方法,使得指標移向下一個狀態。也就是說,每次呼叫next方法,內部指標就從函式頭部或上一次停下來的地方開始執行,直到遇到下一個yield表示式(或return語句)為止。換言之,Generator 函式是分段執行的,yield表示式是暫停執行的標記,而next方法可以恢復執行。

async和await

ES2017 標準引入了 async 函式,使得非同步操作變得更加方便。

async 函式就是 Generator 函式的語法糖。

async函式就是將 Generator 函式的星號(*)替換成async,將yield替換成await。並且返回一個 Promise,可以使用then方法新增回撥函式。 當函式執行的時候,一旦遇到await就會先返回,等到非同步操作完成,再接著執行函式體內後面的語句。

async函式對 Generator 函式的改進:

  1. 內建執行器 async函式自帶執行器,不像 Generator 函式,需要呼叫next方法,或者用co模組,才能真正執行。
  2. 更好的語義
  3. 更好的適用性
  4. 返回值是 Promise 進一步說,async函式完全可以看作多個非同步操作,包裝成的一個 Promise 物件,而await命令就是內部then命令的語法糖。

例子:getJSON函式返回一個promise,這個promise成功resolve時會返回一個json物件。我們只是呼叫這個函式,列印返回的JSON物件,然後返回”done”。

// promise
const makeRequest = () =>
  getJSON()
    .then(data => {
      console.log(data)
      return "done"
    })
makeRequest()
複製程式碼
//使用Async/Await
const makeRequest = async () => {
  console.log(await getJSON())
  return "done"
}
makeRequest()

//async函式會隱式地返回一個promise,該promise的reosolve值就是函式return的值。(示例中reosolve值就是字串”done”)
複製程式碼

Async的優缺點:

優勢: 處理 then 的呼叫鏈能夠更清晰準確的寫出程式碼。

缺點: 濫用 await 可能會導致效能問題,因為 await 會阻塞程式碼,也許之後的非同步程式碼並不依賴於前者,但仍然需要等待前者完成,導致程式碼失去了併發性。

Iterator

是ES6中一個很重要概念,它並不是物件,也不是任何一種資料型別。為Set、Map、Array、Object新增一個統一的遍歷API。部署了Iterator介面的物件(可遍歷物件)都可以通過for...of去遍歷。

class

ES6 的class可以看作只是一個語法糖,它的絕大部分功能,ES5 都可以做到,新的class寫法只是讓物件原型的寫法更加清晰、更像物件導向程式設計的語法而已,可以看做是建構函式的另一種寫法。

function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')';
};

var p = new Point(1, 2);

//ES6的class改寫
class Point {
  //構造方法
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}
複製程式碼

上面程式碼定義了一個“類”,可以看到裡面有一個constructor方法,這就是構造方法,而this關鍵字則代表例項物件。也就是說,ES5 的建構函式Point,對應 ES6 的Point類的構造方法。

Point類除了構造方法,還定義了一個toString方法。注意,定義“類”的方法的時候,前面不需要加上function這個關鍵字,直接把函式定義放進去了就可以了。另外,方法之間不需要逗號分隔,加了會報錯。

class實現繼承: Class 可以通過extends關鍵字實現繼承,這比 ES5 的通過修改原型鏈實現繼承,要清晰和方便很多。

總結

日常前端程式碼開發中,有哪些值得用ES6去改進的程式設計優化或者規範:

  • 常用箭頭函式來取代var self = this;的做法。
  • 常用let取代var命令。
  • 常用陣列/物件的結構賦值來命名變數,結構更清晰,語義更明確,可讀性更好。
  • 在長字串多變數組合場合,用模板字串來取代字串累加,能取得更好地效果和閱讀體驗。
  • 用Class類取代傳統的建構函式,來生成例項化物件。
  • 在大型應用開發中,要保持module模組化開發思維,分清模組之間的關係,常用import、export方法。

相關文章