ES6、ES7、ES8特性一鍋燉(ES6、ES7、ES8學習指南)

CrazyCodeBoy發表於2018-09-15

概述

ES全稱ECMAScript,ECMAScript是ECMA制定的標準化指令碼語言。目前JavaScript使用的ECMAScript版本為ECMAScript-262

ECMAScript 標準建立在一些原有的技術上,最為著名的是 JavaScript (網景) 和 JScript (微軟)。它最初由網景的 Brendan Eich 發明,第一次出現是在網景的 Navigator 2.0 瀏覽器上。Netscape 2.0 以及微軟 Internet Explorer 3.0 後序的所有瀏覽器上都有它的身影。

ECMAScript版本 釋出時間 新增特性
ECMAScript 2009(ES5) 2009年11月 擴充套件了Object、Array、Function的功能等
ECMAScript 2015(ES6) 2015年6月 類,模組化,箭頭函式,函式引數預設值等
ECMAScript 2016(ES7) 2016年3月 includes,指數操作符
ECMAScript 2017(ES8) 2017年6月 sync/await,Object.values(),Object.entries(),String padding等

瞭解這些特性,不僅能使我們的編碼更加的符合規範,而且能提高我們Coding的效率。

ES6的特性

ES6的特性比較多,在 ES5 釋出近 6 年(2009-11 至 2015-6)之後才將其標準化。兩個釋出版本之間時間跨度很大,所以ES6中的特性比較多。

在這裡列舉幾個常用的:

  • 模組化
  • 箭頭函式
  • 函式引數預設值
  • 模板字串
  • 解構賦值
  • 延展操作符
  • 物件屬性簡寫
  • Promise
  • Let與Const

1.類(class)

對熟悉Java,object-c,c#等純面嚮物件語言的開發者來說,都會對class有一種特殊的情懷。ES6 引入了class(類),讓JavaScript的物件導向程式設計變得更加簡單和易於理解。

  class Animal {
    // 建構函式,例項化的時候將會被呼叫,如果不指定,那麼會有一個不帶引數的預設建構函式.
    constructor(name,color) {
      this.name = name;
      this.color = color;
    }
    // toString 是原型物件上的屬性
    toString() {
      console.log('name:' + this.name + ',color:' + this.color);

    }
  }

 var animal = new Animal('dog','white');//例項化Animal
 animal.toString();

 console.log(animal.hasOwnProperty('name')); //true
 console.log(animal.hasOwnProperty('toString')); // false
 console.log(animal.__proto__.hasOwnProperty('toString')); // true

 class Cat extends Animal {
  constructor(action) {
    // 子類必須要在constructor中指定super 函式,否則在新建例項的時候會報錯.
    // 如果沒有置頂consructor,預設帶super函式的constructor將會被新增、
    super('cat','white');
    this.action = action;
  }
  toString() {
    console.log(super.toString());
  }
 }

 var cat = new Cat('catch')
 cat.toString();

 // 例項cat 是 Cat 和 Animal 的例項,和Es5完全一致。
 console.log(cat instanceof Cat); // true
 console.log(cat instanceof Animal); // true
複製程式碼

2.模組化(Module)

ES5不支援原生的模組化,在ES6中模組作為重要的組成部分被新增進來。模組的功能主要由 export 和 import 組成。每一個模組都有自己單獨的作用域,模組之間的相互呼叫關係是通過 export 來規定模組對外暴露的介面,通過import來引用其它模組提供的介面。同時還為模組創造了名稱空間,防止函式的命名衝突。

匯出(export)

ES6允許在一個模組中使用export來匯出多個變數或函式。

匯出變數

//test.js
export var name = 'Rainbow'
複製程式碼

心得:ES6不僅支援變數的匯出,也支援常量的匯出。 export const sqrt = Math.sqrt;//匯出常量

ES6將一個檔案視為一個模組,上面的模組通過 export 向外輸出了一個變數。一個模組也可以同時往外面輸出多個變數。

 //test.js
 var name = 'Rainbow';
 var age = '24';
 export {name, age};
複製程式碼

匯出函式

// myModule.js
export function myModule(someArg) {
  return someArg;
}  
複製程式碼

匯入(import)

定義好模組的輸出以後就可以在另外一個模組通過import引用。

import {myModule} from 'myModule';// main.js
import {name,age} from 'test';// test.js
複製程式碼

心得:一條import 語句可以同時匯入預設函式和其它變數。import defaultMethod, { otherMethod } from 'xxx.js';

3.箭頭(Arrow)函式

這是ES6中最令人激動的特性之一。=>不只是關鍵字function的簡寫,它還帶來了其它好處。箭頭函式與包圍它的程式碼共享同一個this,能幫你很好的解決this的指向問題。有經驗的JavaScript開發者都熟悉諸如var self = this;var that = this這種引用外圍this的模式。但藉助=>,就不需要這種模式了。

箭頭函式的結構

箭頭函式的箭頭=>之前是一個空括號、單個的引數名、或用括號括起的多個引數名,而箭頭之後可以是一個表示式(作為函式的返回值),或者是用花括號括起的函式體(需要自行通過return來返回值,否則返回的是undefined)。

// 箭頭函式的例子
()=>1
v=>v+1
(a,b)=>a+b
()=>{
    alert("foo");
}
e=>{
    if (e == 0){
        return 0;
    }
    return 1000/e;
}
複製程式碼

心得:不論是箭頭函式還是bind,每次被執行都返回的是一個新的函式引用,因此如果你還需要函式的引用去做一些別的事情(譬如解除安裝監聽器),那麼你必須自己儲存這個引用。

解除安裝監聽器時的陷阱

錯誤的做法

class PauseMenu extends React.Component{
    componentWillMount(){
        AppStateIOS.addEventListener('change', this.onAppPaused.bind(this));
    }
    componentWillUnmount(){
        AppStateIOS.removeEventListener('change', this.onAppPaused.bind(this));
    }
    onAppPaused(event){
    }
}
複製程式碼

正確的做法

class PauseMenu extends React.Component{
    constructor(props){
        super(props);
        this._onAppPaused = this.onAppPaused.bind(this);
    }
    componentWillMount(){
        AppStateIOS.addEventListener('change', this._onAppPaused);
    }
    componentWillUnmount(){
        AppStateIOS.removeEventListener('change', this._onAppPaused);
    }
    onAppPaused(event){
    }
}
複製程式碼

除上述的做法外,我們還可以這樣做:

class PauseMenu extends React.Component{
    componentWillMount(){
        AppStateIOS.addEventListener('change', this.onAppPaused);
    }
    componentWillUnmount(){
        AppStateIOS.removeEventListener('change', this.onAppPaused);
    }
    onAppPaused = (event) => {
        //把函式直接作為一個arrow function的屬性來定義,初始化的時候就繫結好了this指標
    }
}
複製程式碼

需要注意的是:不論是bind還是箭頭函式,每次被執行都返回的是一個新的函式引用,因此如果你還需要函式的引用去做一些別的事情(譬如解除安裝監聽器),那麼你必須自己儲存這個引用。

4.函式引數預設值

ES6支援在定義函式的時候為其設定預設值:

function foo(height = 50, color = 'red')
{
    // ...
}
複製程式碼

不使用預設值:

function foo(height, color)
{
    var height = height || 50;
    var color = color || 'red';
    //...
}
複製程式碼

這樣寫一般沒問題,但當引數的布林值為false時,就會有問題了。比如,我們這樣呼叫foo函式:

foo(0, "")
複製程式碼

因為0的布林值為false,這樣height的取值將是50。同理color的取值為‘red’。

所以說,函式引數預設值不僅能是程式碼變得更加簡潔而且能規避一些問題。

5.模板字串

ES6支援模板字串,使得字串的拼接更加的簡潔、直觀。

不使用模板字串:

var name = 'Your name is ' + first + ' ' + last + '.'
複製程式碼

使用模板字串:

var name = `Your name is ${first} ${last}.`
複製程式碼

在ES6中通過${}就可以完成字串的拼接,只需要將變數放在大括號之中。

6.解構賦值

解構賦值語法是JavaScript的一種表示式,可以方便的從陣列或者物件中快速提取值賦給定義的變數。

獲取陣列中的值

從陣列中獲取值並賦值到變數中,變數的順序與陣列中物件順序對應。

var foo = ["one", "two", "three", "four"];

var [one, two, three] = foo;
console.log(one); // "one"
console.log(two); // "two"
console.log(three); // "three"

//如果你要忽略某些值,你可以按照下面的寫法獲取你想要的值
var [first, , , last] = foo;
console.log(first); // "one"
console.log(last); // "four"

//你也可以這樣寫
var a, b; //先宣告變數

[a, b] = [1, 2];
console.log(a); // 1
console.log(b); // 2
複製程式碼

如果沒有從陣列中的獲取到值,你可以為變數設定一個預設值。

var a, b;

[a=5, b=7] = [1];
console.log(a); // 1
console.log(b); // 7
複製程式碼

通過解構賦值可以方便的交換兩個變數的值。

var a = 1;
var b = 3;

[a, b] = [b, a];
console.log(a); // 3
console.log(b); // 1

複製程式碼

獲取物件中的值

const student = {
  name:'Ming',
  age:'18',
  city:'Shanghai'  
};

const {name,age,city} = student;
console.log(name); // "Ming"
console.log(age); // "18"
console.log(city); // "Shanghai"
複製程式碼

7.延展操作符(Spread operator)

延展操作符...可以在函式呼叫/陣列構造時, 將陣列表示式或者string在語法層面展開;還可以在構造物件時, 將物件表示式按key-value的方式展開。

語法

函式呼叫:

myFunction(...iterableObj);
複製程式碼

陣列構造或字串:

[...iterableObj, '4', ...'hello', 6];
複製程式碼

構造物件時,進行克隆或者屬性拷貝(ECMAScript 2018規範新增特性):

let objClone = { ...obj };
複製程式碼

應用場景

在函式呼叫時使用延展操作符

function sum(x, y, z) {
  return x + y + z;
}
const numbers = [1, 2, 3];

//不使用延展操作符
console.log(sum.apply(null, numbers));

//使用延展操作符
console.log(sum(...numbers));// 6
複製程式碼

構造陣列

沒有展開語法的時候,只能組合使用 push,splice,concat 等方法,來將已有陣列元素變成新陣列的一部分。有了展開語法, 構造新陣列會變得更簡單、更優雅:

const stuendts = ['Jine','Tom']; 
const persons = ['Tony',... stuendts,'Aaron','Anna'];
conslog.log(persions)// ["Tony", "Jine", "Tom", "Aaron", "Anna"]
複製程式碼

和引數列表的展開類似, ... 在構造字陣列時, 可以在任意位置多次使用。

陣列拷貝

var arr = [1, 2, 3];
var arr2 = [...arr]; // 等同於 arr.slice()
arr2.push(4); 
console.log(arr2)//[1, 2, 3, 4]
複製程式碼

展開語法和 Object.assign() 行為一致, 執行的都是淺拷貝(只遍歷一層)。

連線多個陣列

var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
var arr3 = [...arr1, ...arr2];// 將 arr2 中所有元素附加到 arr1 後面並返回
//等同於
var arr4 = arr1.concat(arr2);
複製程式碼

在ECMAScript 2018中延展操作符增加了對物件的支援

var obj1 = { foo: 'bar', x: 42 };
var obj2 = { foo: 'baz', y: 13 };

var clonedObj = { ...obj1 };
// 克隆後的物件: { foo: "bar", x: 42 }

var mergedObj = { ...obj1, ...obj2 };
// 合併後的物件: { foo: "baz", x: 42, y: 13 }
複製程式碼

在React中的應用

通常我們在封裝一個元件時,會對外公開一些 props 用於實現功能。大部分情況下在外部使用都應顯示的傳遞 props 。但是當傳遞大量的props時,會非常繁瑣,這時我們可以使用 ...(延展操作符,用於取出引數物件的所有可遍歷屬性) 來進行傳遞。

一般情況下我們應該這樣寫

<CustomComponent name ='Jine' age ={21} />
複製程式碼

使用 ... ,等同於上面的寫法

const params = {
		name: 'Jine',
		age: 21
	}

<CustomComponent {...params} />
複製程式碼

配合解構賦值避免傳入一些不需要的引數

var params = {
	name: '123',
	title: '456',
	type: 'aaa'
}

var { type, ...other } = params;

<CustomComponent type='normal' number={2} {...other} />
//等同於
<CustomComponent type='normal' number={2} name='123' title='456' />
複製程式碼

8.物件屬性簡寫

在ES6中允許我們在設定一個物件的屬性的時候不指定屬性名。

不使用ES6

const name='Ming',age='18',city='Shanghai';
        
const student = {
    name:name,
    age:age,
    city:city
};
console.log(student);//{name: "Ming", age: "18", city: "Shanghai"}
複製程式碼

物件中必須包含屬性和值,顯得非常冗餘。

使用ES6

const name='Ming',age='18',city='Shanghai';
        
const student = {
    name,
    age,
    city
};
console.log(student);//{name: "Ming", age: "18", city: "Shanghai"}
複製程式碼

物件中直接寫變數,非常簡潔。

9.Promise

Promise 是非同步程式設計的一種解決方案,比傳統的解決方案callback更加的優雅。它最早由社群提出和實現的,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise物件。

不使用ES6

巢狀兩個setTimeout回撥函式:

setTimeout(function()
{
    console.log('Hello'); // 1秒後輸出"Hello"
    setTimeout(function()
    {
        console.log('Hi'); // 2秒後輸出"Hi"
    }, 1000);
}, 1000);
複製程式碼

使用ES6

var waitSecond = new Promise(function(resolve, reject)
{
    setTimeout(resolve, 1000);
});

waitSecond
    .then(function()
    {
        console.log("Hello"); // 1秒後輸出"Hello"
        return waitSecond;
    })
    .then(function()
    {
        console.log("Hi"); // 2秒後輸出"Hi"
    });
複製程式碼

上面的的程式碼使用兩個then來進行非同步程式設計序列化,避免了回撥地獄:

10.支援let與const

在之前JS是沒有塊級作用域的,const與let填補了這方便的空白,const與let都是塊級作用域。

使用var定義的變數為函式級作用域:

{
  var a = 10;
}

console.log(a); // 輸出10
複製程式碼

使用let與const定義的變數為塊級作用域:

{
  let a = 10;
}

console.log(a); //-1 or Error“ReferenceError: a is not defined”
複製程式碼

ES7的特性

在ES6之後,ES的釋出頻率更加頻繁,基本每年一次,所以自ES6之後,每個新版本的特性的數量就比較少。

  • includes()
  • 指數操作符

1. Array.prototype.includes()

includes() 函式用來判斷一個陣列是否包含一個指定的值,如果包含則返回 true,否則返回false

includes 函式與 indexOf 函式很相似,下面兩個表示式是等價的:

arr.includes(x)
arr.indexOf(x) >= 0
複製程式碼

接下來我們來判斷數字中是否包含某個元素:

在ES7之前的做法

使用indexOf()驗證陣列中是否存在某個元素,這時需要根據返回值是否為-1來判斷:

let arr = ['react', 'angular', 'vue'];

if (arr.indexOf('react') !== -1)
{
    console.log('react存在');
}
複製程式碼

使用ES7的includes()

使用includes()驗證陣列中是否存在某個元素,這樣更加直觀簡單:

let arr = ['react', 'angular', 'vue'];

if (arr.includes('react'))
{
    console.log('react存在');
}
複製程式碼

2.指數操作符

在ES7中引入了指數運算子****具有與Math.pow(..)等效的計算結果。

不使用指數操作符

使用自定義的遞迴函式calculateExponent或者Math.pow()進行指數運算:

function calculateExponent(base, exponent)
{
    if (exponent === 1)
    {
        return base;
    }
    else
    {
        return base * calculateExponent(base, exponent - 1);
    }
}

console.log(calculateExponent(2, 10)); // 輸出1024
console.log(Math.pow(2, 10)); // 輸出1024
複製程式碼

使用指數操作符

使用指數運算子**,就像+、-等操作符一樣:

console.log(2**10);// 輸出1024
複製程式碼

ES8的特性

  • async/await
  • Object.values()
  • Object.entries()
  • String padding
  • 函式引數列表結尾允許逗號
  • Object.getOwnPropertyDescriptors()

瀏覽器相容性

1.async/await

在ES8中加入了對async/await的支援,也就我們所說的非同步函式,這是一個很實用的功能。 async/await將我們從頭痛的回撥地獄中解脫出來了,使整個程式碼看起來很簡潔。

使用async/await與不使用async/await的差別:

login(userName) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('1001');
        }, 600);
    });
}

getData(userId) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (userId === '1001') {
                resolve('Success');
            } else {
                reject('Fail');
            }
        }, 600);
    });
}

// 不使用async/await ES7
doLogin(userName) {
    this.login(userName)
        .then(this.getData)
        .then(result => {
            console.log(result)
        })
}

// 使用async/await ES8
async doLogin2(userName) {
    const userId=await this.login(userName);
    const result=await this.getData(userId);
}

this.doLogin()// Success
this.doLogin2()// Success
複製程式碼

async/await的幾種應用場景

接下來我們來看一下async/await的幾種應用場景。

獲取非同步函式的返回值

非同步函式本身會返回一個Promise,所以我們可以通過then來獲取非同步函式的返回值。

async function charCountAdd(data1, data2) {
    const d1=await charCount(data1);
    const d2=await charCount(data2);
    return d1+d2;
}
charCountAdd('Hello','Hi').then(console.log);//通過then獲取非同步函式的返回值。
function charCount(data) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(data.length);
        }, 1000);
    });
}
複製程式碼

async/await在併發場景中的應用

對於上述的例子,我們呼叫await兩次,每次都是等待1秒一共是2秒,效率比較低,而且兩次await的呼叫並沒有依賴關係,那能不能讓其併發執行呢,答案是可以的,接下來我們通過Promise.all來實現await的併發呼叫。

async function charCountAdd(data1, data2) {
    const [d1,d2]=await Promise.all([charCount(data1),charCount(data2)]);
    return d1+d2;
}
charCountAdd('Hello','Hi').then(console.log);
function charCount(data) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(data.length);
        }, 1000);
    });
}
複製程式碼

通過上述程式碼我們實現了兩次charCount的併發呼叫,Promise.all接受的是一個陣列,它可以將陣列中的promise物件併發執行;

async/await的幾種錯誤處理方式

第一種:捕捉整個async/await函式的錯誤

async function charCountAdd(data1, data2) {
    const d1=await charCount(data1);
    const d2=await charCount(data2);
    return d1+d2;
}
charCountAdd('Hello','Hi')
    .then(console.log)
    .catch(console.log);//捕捉整個async/await函式的錯誤
...
複製程式碼

這種方式可以捕捉整個charCountAdd執行過程中出現的錯誤,錯誤可能是由charCountAdd本身產生的,也可能是由對data1的計算中或data2的計算中產生的。

第二種:捕捉單個的await表示式的錯誤

async function charCountAdd(data1, data2) {
    const d1=await charCount(data1)
        .catch(e=>console.log('d1 is null'));
    const d2=await charCount(data2)
        .catch(e=>console.log('d2 is null'));
    return d1+d2;
}
charCountAdd('Hello','Hi').then(console.log);
複製程式碼

通過這種方式可以捕捉每一個await表示式的錯誤,如果既要捕捉每一個await表示式的錯誤,又要捕捉整個charCountAdd函式的錯誤,可以在呼叫charCountAdd的時候加個catch

...
charCountAdd('Hello','Hi')
    .then(console.log)
    .catch(console.log);//捕捉整個async/await函式的錯誤
...
複製程式碼

第三種:同時捕捉多個的await表示式的錯誤

async function charCountAdd(data1, data2) {
    let d1,d2;
    try {
        d1=await charCount(data1);
        d2=await charCount(data2);
    }catch (e){
        console.log('d1 is null');
    }
    return d1+d2;
}
charCountAdd('Hello','Hi')
    .then(console.log);

function charCount(data) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(data.length);
        }, 1000);
    });
}
複製程式碼

2.Object.values()

Object.values()是一個與Object.keys()類似的新函式,但返回的是Object自身屬性的所有值,不包括繼承的值。

假設我們要遍歷如下物件obj的所有值:

const obj = {a: 1, b: 2, c: 3};
複製程式碼

不使用Object.values() :ES7

const vals=Object.keys(obj).map(key=>obj[key]);
console.log(vals);//[1, 2, 3]
複製程式碼

使用Object.values() :ES8

const values=Object.values(obj1);
console.log(values);//[1, 2, 3]
複製程式碼

從上述程式碼中可以看出Object.values()為我們省去了遍歷key,並根據這些key獲取value的步驟。

3.Object.entries

Object.entries()函式返回一個給定物件自身可列舉屬性的鍵值對的陣列。

接下來我們來遍歷上文中的obj物件的所有屬性的key和value:

不使用Object.entries() :ES7

Object.keys(obj).forEach(key=>{
	console.log('key:'+key+' value:'+obj[key]);
})
//key:a value:1
//key:b value:2
//key:c value:3
複製程式碼

使用Object.entries() :ES8

for(let [key,value] of Object.entries(obj1)){
	console.log(`key: ${key} value:${value}`)
}
//key:a value:1
//key:b value:2
//key:c value:3
複製程式碼

4.String padding

在ES8中String新增了兩個例項函式String.prototype.padStartString.prototype.padEnd,允許將空字串或其他字串新增到原始字串的開頭或結尾。

String.padStart(targetLength,[padString])

  • targetLength:當前字串需要填充到的目標長度。如果這個數值小於當前字串的長度,則返回當前字串本身。
  • padString:(可選)填充字串。如果字串太長,使填充後的字串長度超過了目標長度,則只保留最左側的部分,其他部分會被截斷,此引數的預設值為 " "。
console.log('0.0'.padStart(4,'10')) //10.0
console.log('0.0'.padStart(20))//                0.00    
複製程式碼

String.padEnd(targetLength,padString])

  • targetLength:當前字串需要填充到的目標長度。如果這個數值小於當前字串的長度,則返回當前字串本身。
  • padString:(可選) 填充字串。如果字串太長,使填充後的字串長度超過了目標長度,則只保留最左側的部分,其他部分會被截斷,此引數的預設值為 " ";
console.log('0.0'.padEnd(4,'0')) //0.00    
console.log('0.0'.padEnd(10,'0'))//0.00000000
複製程式碼

4.函式引數列表結尾允許逗號

這是一個不痛不癢的更新,主要作用是方便使用git進行多人協作開發時修改同一個函式減少不必要的行變更。

不使用ES8

//程式設計師A
var f = function(a,
  b
   ) { 
  ...
  }

//程式設計師B
var f = function(a,
  b,   //變更行
  c   //變更行
   ) { 
  ...
  }

//程式設計師C
var f = function(a,
  b,
  c,   //變更行
  d   //變更行
   ) { 
  ...
  }
複製程式碼

使用ES8

//程式設計師A
var f = function(a,
  b,
   ) { 
  ...
  }

//程式設計師B
var f = function(a,
  b,
  c,   //變更行
   ) { 
  ...
  }

//程式設計師C
var f = function(a,
  b,
  c,
  d,   //變更行
   ) { 
  ...
  }
複製程式碼

5.Object.getOwnPropertyDescriptors()

Object.getOwnPropertyDescriptors()函式用來獲取一個物件的所有自身屬性的描述符,如果沒有任何自身屬性,則返回空物件。

函式原型:

Object.getOwnPropertyDescriptors(obj)
複製程式碼

返回obj物件的所有自身屬性的描述符,如果沒有任何自身屬性,則返回空物件。

const obj2 = {
	name: 'Jine',
	get age() { return '18' }
};
Object.getOwnPropertyDescriptors(obj2)
// {
//   age: {
//     configurable: true,
//     enumerable: true,
//     get: function age(){}, //the getter function
//     set: undefined
//   },
//   name: {
//     configurable: true,
//     enumerable: true,
//		value:"Jine",
//		writable:true
//   }
// }
複製程式碼

總結

技術更替的車輪一直在前進中,JavaScript的規範和標準也在不斷的制定和完善。你會發現ECMAScript 新版的很多特性已經是Typescript,瀏覽器或其他polyfills的一部分,就拿ES8的async/await來說,它是2017年6月被納入ECMAScript的,但我在2016年的時候就已經開始使用這個特性了,這些特性通常由ECMAScript議員提交,然後會出現在在未來的某個ECMAScript版本中。

參考

相關文章