ES6、ES7、ES8學習指南

概述

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版本中。

參考

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