ES6簡述(持續更新…)


前言

ES6,全稱ECMAScript 6,是ECMA委員會在 2015年6月正式發布的新ECMAScript標准。所以又稱ECMAScript 2015,也就是說,ES6就是ES2015。至今各大瀏覽器廠商所開發的 JavaScript 引擎都還沒有完成對 ES2015 中所有特性的完美支持,於是乎如 babelTraceur 等編譯器便出現了。它們能將尚未得到支持的 ES2015 特性轉換為 ES5 標准的代碼,使其得到瀏覽器的支持。其中,babel 因其模塊化轉換器(Transformer)的設計特點贏得了絕大部份 JavaScript 開發者的青睞。

一、變化

一言以蔽之:ES2015 標准提供了許多新的語法和編程特性以提高 JavaScript 的開發效率和體驗。

二、新的語法

1、let、const

他們是繼 var 之后,新的變量定義方法。

const 更容易被理解:const 也就是 constant 的縮寫,跟 C/C++ 等經典語言一樣,用於定義常量,即不可變量。

ES5只有全局作用域和函數作用域,沒有塊級作用域,這帶來很多不合理的場景。第一種場景就是你現在看到的內層變量覆蓋外層變量。而let則實際上為JavaScript新增了塊級作用域。用它所聲明的變量,只在let命令所在的代碼塊內有效。

2、箭頭函數 (arrow function)

這個恐怕是ES6最最常用的一個新特性了,用它來寫function比原來的寫法要簡潔清晰很多:

function(i){ return i + 1; } //ES5
(i) => i + 1 //ES6

如果方程比較復雜,則需要用{}把代碼包起來:

function(x, y) { 
    x++;
    y--;
    return x + y;
}
(x, y) => {x++; y--; return x+y}

除了看上去更簡潔以外,arrow function還有一項超級無敵的功能!
長期以來,JavaScript語言的this對象一直是一個令人頭痛的問題,在對象方法中使用this,必須非常小心。例如:

class Animal {
    constructor(){
        this.type = 'animal'
    }
    says(say){
        setTimeout(function(){
            console.log(this.type + ' says ' + say)
        }, 1000)
    }
}

 var animal = new Animal()
 animal.says('hi')  //undefined says hi

運行上面的代碼會報錯,這是因為setTimeout中的this指向的是全局對象。所以為了讓它能夠正確的運行,傳統的解決方法有兩種:

(1)第一種是將this傳給self,再用self來指代this

says(say){
   var self = this;
   setTimeout(function(){
     console.log(self.type + ' says ' + say)
}, 1000);

(2)第二種方法是用bind(this),即:

says(say){
   setTimeout(function(){
     console.log(self.type + ' says ' + say)
}.bind(this), 1000);

但現在我們有了箭頭函數,就不需要這么麻煩了:

class Animal {
    constructor(){
        this.type = 'animal'
    }
    says(say){
        setTimeout( () => {
            console.log(this.type + ' says ' + say)
        }, 1000)
    }
}
 var animal = new Animal()
 animal.says('hi')  //animal says hi

當我們使用箭頭函數時,函數體內的this對象,就是定義時所在的對象,而不是使用時所在的對象。
並不是因為箭頭函數內部有綁定this的機制,實際原因是箭頭函數根本沒有自己的this,它的this是繼承外面的,因此內部的this就是外層代碼塊的this。

3、模板字符串(template string)

這個東西也是非常有用,當我們要插入大段的html內容到文檔中時,傳統的寫法非常麻煩,所以之前我們通常會引用一些模板工具庫。

可以先看下面一段代碼:

$("#result").append(
  "There are <b>" + basket.count + "</b> " +
  "items in your basket, " +
  "<em>" + basket.onSale +
  "</em> are on sale!"
);

我們要用一堆的'+'號來連接文本與變量,而使用ES6的新特性模板字符串``后,我們可以直接這么來寫:

$("#result").append(`
  There are <b>${basket.count}</b> items
   in your basket, <em>${basket.onSale}</em>
  are on sale!
`);

用反引號(`)來標識起始,用${}來引用變量,而且所有的空格和縮進都會被保留在輸出之中。

4、對象字面量擴展語法

4.1 方法屬性省略 function

// es5
function bar() {
  return 'bar'
},

// es6
bar() {
  return 'bar'
}

4.2 支持 __proto__ 注入

在 ES2015 中,我們可以給一個對象硬生生的賦予其 __proto__,這樣它就可以成為這個值所屬類的一個實例了。

class Foo {
  constructor() {
    this.pingMsg = 'pong'
  }

  ping() {
    console.log(this.pingMsg)
  }
}

let o = {
  __proto__: new Foo()
}

o.ping() //=> pong

有什么用呢?當我想擴展或者覆蓋一個類的方法,並生成一個實例,但覺得另外定義一個類就感覺浪費了。那我可以這樣做:

let o = {
  __proto__: new Foo(),

  constructor() {
    this.pingMsg = 'alive'
  },

  msg: 'bang',
  yell() {
    console.log(this.msg)
  }
}

o.yell() //=> bang
o.ping() //=> alive

4.3 同名方法屬性省略語法

也是看上去有點雞肋的新特性,不過在做 JavaScript 模塊化工程的時候則有了用武之地。

// module.js
export default {
  someMethod
}

function someMethod() {
  // ...
}

// app.js
import Module from './module'

Module.someMethod()

4.4 可以動態計算的屬性名稱(這個我覺得還是非常有用的)

let arr = [1, 2, 3]
let outArr = arr.map(n => {
  return {
    [ n ]: n,
    [ `${n}^2` ]: Math.pow(n, 2)
  }
})
console.dir(outArr) //=>
  [
    { '1': 1, '1^2': 1 },
    { '2': 2, '2^2': 4 },
    { '3': 3, '3^2': 9 }
  ]

5、表達式解構(destructuring)

這是es6相當有用的一個特性。

ES6允許按照一定模式,從數組和對象中提取值,對變量進行賦值,這被稱為解構(Destructuring)。

例:

let cat = 'ken'
let dog = 'lili'
let zoo = {cat: cat, dog: dog}
console.log(zoo)  //Object {cat: "ken", dog: "lili"}

用ES6完全可以像下面這么寫:

let cat = 'ken'
let dog = 'lili'
let zoo = {cat, dog}
console.log(zoo)  //Object {cat: "ken", dog: "lili"}

反過來可以這么寫:

let dog = {type: 'animal', many: 2}
let { type, many} = dog
console.log(type, many)   //animal 2

 6、默認參數(default)和后續參數(rest)

6.1 默認參數

調用animal()方法時忘了傳參數,傳統的做法就是加上這一句type = type || 'cat'來指定默認值。

function animal(type){
    type = type || 'cat'  
    console.log(type)
}
animal()

如果用ES6我們可以直接這么寫:

function animal(type = 'cat'){
    console.log(type)
}
animal()

6.2 后續參數

我們知道,函數的 call 和 apply 在使用上的最大差異便是一個在首參數后傳入各個參數,一個是在首參數后傳入一個包含所有參數的數組。

如果我們在實現某些函數或方法時,也希望實現像 call 一樣的使用方法,在ES5中我們得使用arguments

function fetchSomethings() {
  var args = [].slice.apply(arguments)

  // ...
}
function doSomeOthers(name) {
  var args = [].slice.apply(arguments, 1)

  // ...
}

而在 ES6 中,我們可以很簡單的使用 ... 語法糖來實現:

function fetchSomethings(...args) {
  // ...
}
function doSomeOthers(name, ...args) {
  // ...
}

要注意的是,...args 后不可再添加。

雖然從語言角度看,arguments 和 ...args 是可以同時使用 ,但有一個特殊情況則不可:arguments 在箭頭函數中,會跟隨上下文綁定到上層,所以在不確定上下文綁定結果的情況下,盡可能不要再箭頭函數中再使用 arguments,而使用 ...args

注意事項

默認參數值后續參數需要遵循順序原則,否則會出錯。

function(...args, last = 1) {
  // This will go wrong
}

三、新的數據類型

在 ES5 中,JavaScript 中基本的數據類型:

  • String 字符串
  • Number 數字(包含整型和浮點型)
  • Boolean 布爾值
  • Object 對象
  • Array 數組

其中又分為值類型引用類型,Array 其實是 Object 的一種子類。

1、Set(集) 和 WeakSet(弱集)

高中數學中,集不能包含相同的元素。

let s = new Set()
s.add('hello').add('world').add('hello')
console.log(s.size) //=> 2
console.log(s.has('hello')) //=> true

在實際開發中,我們有很多需要用到集的場景,如搜索、索引建立等。

WeakSet 在 JavaScript 底層作出調整(在非降級兼容的情況下),檢查元素的變量引用情況。如果元素的引用已被全部解除,則該元素就會被刪除,以節省內存空間。這意味著無法直接加入數字或者字符串。另外 WeakSet 對元素有嚴格要求,必須是 Object,當然了,你也可以用 new String('...') 等形式處理元素。

let weaks = new WeakSet()
weaks.add("hello") //=> Error
weaks.add(3.1415) //=> Error

let foo = new String("bar")
let pi = new Number(3.1415)
weaks.add(foo)
weaks.add(pi)
weaks.has(foo) //=> true
foo = null
weaks.has(foo) //=> false

2、Map 和 WeakMap

從數據結構的角度來說,映射(Map)跟原本的 Object 非常相似,都是 Key/Value 的鍵值對結構。但是 Object 有一個讓人非常不爽的限制:key 必須是字符串或數字。在一般情況下,我們並不會遇上這一限制,但若我們需要建立一個對象映射表時,這一限制顯得尤為棘手。

而 Map 則解決了這一問題,可以使用任何對象作為其 key,這可以實現從前不能實現或難以實現的功能,如在項目邏輯層實現數據索引等。

let map = new Map()
let object = { id: 1 }

map.set(object, 'hello')
map.set('hello', 'world')
map.has(object) //=> true
map.get(object) //=> hello

而 WeakMap 和 WeakSet 很類似,只不過 WeakMap 的鍵和值都會檢查變量引用,只要其一的引用全被解除,該鍵值對就會被刪除。

let weakm = new WeakMap()
let keyObject = { id: 1 }
let valObject = { score: 100 }

weakm.set(keyObject, valObject)
weakm.get(keyObject) //=> { score: 100 }
keyObject = null
weakm.has(keyObject) //=> false

四、類(Class)

回想一下在 ES5 中,我們是怎么在 JavaScript 中實現類的?

function Foo() {}
var foo = new Foo()

ES6 中的只是一種語法糖,用於定義原型(Prototype)的。

1、語法

1.1 定義

class Person {
  constructor(name, gender, age) {
    this.name = name
    this.gender = gender
    this.age = age
  }

  isAdult() {
    return this.age >= 18
  }
}

let me = new Person('Me', 'man', 19)
console.log(me.isAdult()) //=> true

1.2 繼承

class Animal {
  say() {
    console.log('say')
  }
}

class Person extends Animal {
  constructor(name, gender, age) {
    super() // must call `super` before using `this` if this class has a superclass

    this.name = name
    this.gender = gender
    this.age = age
  }

  isAdult() {
    return this.age >= 18
  }
}

class Man extends Person {
  constructor(name, age) {
    super(name, 'man', age)
  }
}

let me = new Man('Me', 19)
console.log(me.isAdult()) //=> true
me.say() //=> say

ES6 中若要是一個類繼承於另外一個類而作為其子類,只需要在子類的名字后面加上 extends {SuperClass} 即可。

1.3 靜態方法

ES6 中的類機制支持 static 類型的方法定義,比如說 Man 是一個類,而我希望為其定義一個 Man.isMan() 方法以用於類型檢查,我們可以這樣做:

class Man {
  // ...

  static isMan(obj) {
    return obj instanceof Man
  }
}

let me = new Man()
console.log(Man.isMan(me)) //=> true

遺憾的是,ES2015 的類並不能直接地定義靜態成員變量,但若必須實現此類需求,可以用static 加上 get 語句和 set 語句實現。

class SyncObject {
  // ...

  static get baseUrl() {
    return 'http://example.com/api/sync'
  }
}

遺憾與期望

就目前來說,ES6 的類機制依然很雞肋:

  1. 不支持私有屬性(private
  2. 不支持前置屬性定義,但可用 get 語句和 set 語句實現
  3. 不支持多重繼承
  4. 沒有類似於協議(Protocl)或接口(Interface)等的概念

五、生成器(Generator)

Generator 的設計初衷是為了提供一種能夠簡便地生成一系列對象的方法,如計算斐波那契數列(Fibonacci Sequence)(俗稱兔子數列):

function* fibo() {
  let a = 1
  let b = 1

  yield a
  yield b

  while (true) {
    let next = a + b
    a = b
    b = next
    yield next
  }
}

let generator = fibo()

for (var i = 0; i < 10; i++)
  console.log(generator.next().value) //=> 1 1 2 3 5 8 13 21 34 55

如果你沒有接觸過 Generator,你一定會對這段代碼感到很奇怪:為什么 function 后會有一個 *?為什么函數里使用了 while (true)卻沒有進入死循環而導致死機?yield 又是什么鬼?

1、基本概念

1.1 Generator Function

生成器函數用於生成生成器(Generator),它與普通函數的定義方式的區別就在於它需要在 function 后加一個 * 。

function* FunctionName() {
  // ...Generator Body
}

生成器函數的聲明形式不是必須的,同樣可以使用匿名函數的形式:

let FunctionName = function*() { /* ... */ }

生成器函數的函數內容將會是對應生成器的運行內容,其中支持一種新的語法 yield。它的作用與 return 有點相似,但並非退出函數,而是切出生成器運行時

你可以把整個生成器運行時看成一條長長的面條(while (true) 則就是無限長的),JavaScript 引擎在每一次遇到 yield 就要切一刀,而切面所成的“紋路”則是 yield 出來的值。

1.2 Generator

生成器在某種意義上可以看做為與 JavaScript 主線程分離的運行時,它可以隨時被 yield 切回主線程(生成器不影響主線程)。

每一次生成器運行時被 yield 都可以帶出一個值,使其回到主線程中;此后,也可以從主線程返回一個值回到生成器運行時中:

let inputValue = yield outputValue

生成器切出主線程並帶出 outputValue,主函數經過處理后(可以是異步的),把 inputValue 帶回生成器中;主線程可以通過 .next(inputValue) 方法返回值到生成器運行時中。

2、基本使用方法

2.1 構建生成器函數

使用 Generator 的第一步自然是要構建生成器函數,理清構建思路。拿斐波那契數列作為例子:

斐波那契數列的定義:第 n (n ≥ 3) 項是第 n - 1 項和第 n - 2 之和,而第 1 項和第 2 項都是 1。

function* fibo() {
  let [a, b] = [1, 1]

  yield a
  yield b

  while (true) {
    [a, b] = [b, a + b]
    yield b
  }
}

這樣設計生成器函數,就可以先把預先設定好的首兩項輸出,然后通過無限循環不斷把后一項輸出。

2.2  啟動生成器

生成器函數不能直接用來作為生成器使用,需要先使用這個函數得到一個生成器,用於運行生成器內容和接收返回值。

let gen = fibo()

2.3 運行生成器內容

得到生成器以后,我們就可以通過它進行數列項生成了。此處演示獲得前 10 項。

let arr = []
for (let i = 0; i < 10; i++)
  arr.push(gen.next().value)

console.log(arr) //=> [ 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 ]

 

六、原生的模塊化

在ES6之前, 前端就使用RequireJS或者seaJS實現模塊化, requireJS是基於AMD規范的模塊化庫,  而像seaJS是基於CMD規范的模塊化庫,  兩者都是為了為了推廣前端模塊化的工具。

現在ES6自帶了模塊化, 也是JS第一次支持module, 在很久以后 ,我們可以直接作用importexport在瀏覽器中導入和導出各個模塊了, 一個js文件代表一個js模塊;

現代瀏覽器對模塊(module)支持程度不同, 目前都是使用babelJS, 或者Traceur把ES6代碼轉化為兼容ES5版本的js代碼。

1、ES6的模塊化的基本規則或特點:

(1):每一個模塊只加載一次, 每一個JS只執行一次, 如果下次再去加載同目錄下同文件,直接從內存中讀取。 一個模塊就是一個單例,或者說就是一個對象;

(2):每一個模塊內聲明的變量都是局部變量, 不會污染全局作用域;

(3):模塊內部的變量或者函數可以通過export導出;

(4):一個模塊可以導入別的模塊。

//lib.js
//導出常量
export const sqrt = Math.sqrt;
//導出函數
export function square(x) {
    return x * x;
}
//導出函數
export function diag(x, y) {
    return sqrt(square(x) + square(y));
}

//main.js
import { square, diag } from './lib';
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5

2、幾種導出方式:

2.1 內聯導出

export class Employee{  
  constructor(id, name, dob){  
    this.id = id;  
    this.name=name;  
    this.dob= dob;  
  }  
  getAge(){  
    return (new Date()).getYear() - this.dob.getYear();  
  }  
}  
export function getEmployee(id, name, dob){  
  return new Employee(id, name, dob);  
}  
var emp = new Employee(1, "Rina", new Date(1987, 1, 22));  

案例中的模塊導出了兩個對象: Employee類,getEmployee函數。因對象emp未被導出,所以其仍為模塊私有。

2.2 導出一組對象

在模塊的末尾單獨進行導出聲明,以導出該模塊中的需要導出的對象。

class Employee{  
  constructor(id, name, dob){  
    this.id = id;  
    this.name=name;  
    this.dob= dob;  
  }  
  getAge(){  
    return (new Date()).getYear() - this.dob.getYear();  
  }  
}  
function getEmployee(id, name, dob){  
  return new Employee(id, name, dob);  
}  
var x = new Employee(1, "Rina", new Date(1987, 1, 22));  
export {Employee, getEmployee};  

在導出時,重命名對象也是可以的。如下例所示,Employee在導出時名字改為了Associate,函數GetEmployee改名為getAssociate。

export {  
    Associate as Employee,  
    getAssociate as getEmployee  
};  

2.3 Default導出

使用關鍵字default,可將對象標注為default對象導出。default關鍵字在每一個模塊中只能使用一次。它既可以用於內聯導出,也可以用於一組對象導出聲明中。

這種導出的方式不需要知道變量的名字, 相當於是匿名的, 直接把開發的接口給export;

如果一個js模塊文件就只有一個功能, 那么就可以使用default導出:

function foo(..) {  
// ..  
}  
export default foo;  
// or:
export{ foo as default }; 

3 、導入

3.1 無對象導入

import './module1.js'; 

3.2 導入默認對象

import foo from "foo";  
// or:  
import { default as foo } from "foo";  

3.3 導入命名的對象

import { foo } from "foo";  

當然也可在同一個聲明中導入默認對象和命名對象。這種情況下,默認對象必須定義一個別名:

import {default as d, foo} from './module1.js';  

3.4 導入所有對象

import * as allFromModule1 from './module1.js';  

3.5 可編程式的按需導入

如果想基於某些條件或等某個事件發生后再加載需要的模塊,可通過使用加載模塊的可編程API(programmatic API)來實現。使用System.import方法,可按程序設定加載模塊。這是一個異步的方法,並返回Promise。

System.import('./module1.js')  
    .then(function(module1){  
        //use module1  
    }, function(e){  
        //handle error  
    });  

如果模塊加載成功且將導出的模塊成功傳遞給回調函數,Promise將會通過。如果模塊名稱有誤或由於網絡延遲等原因導致模塊加載失敗,Promise將會失敗。

等會,什么是Promise?

七、Promise

Promise 是一種用於解決回調函數無限嵌套的工具(當然,這只是其中一種),其字面意義為“保證”。它的作用便是“免去”異步操作的回調函數,保證能通過后續監聽而得到返回值,或對錯誤處理。它能使異步操作變得井然有序,也更好控制。

1、基本用法

要為一個函數賦予 Promise 的能力,先要創建一個 Promise 對象,並將其作為函數值返回。Promise 構造函數要求傳入一個函數,並帶有 resolve 和 reject 參數。這是兩個用於結束 Promise 等待的函數,對應的成功失敗。而我們的邏輯代碼就在這個函數中進行。

function fetchData() {
    return new Promise((resolve, reject) => {
        if (/* 異步操作成功 */){
          resolve(value);
        } else {
          reject(error);
        }
    });
}

Promise 構造函數接受一個函數作為參數,該函數的兩個參數分別是 resolve 方法和 reject 方法。

如果異步操作成功,則用 resolve 方法將 Promise 對象的狀態,從「未完成」變為「成功」(即從 pending 變為 resolved);

如果異步操作失敗,則用 reject 方法將 Promise 對象的狀態,從「未完成」變為「失敗」(即從 pending 變為 rejected)。

基本的 api

  1. Promise.resolve()
  2. Promise.reject()
  3. Promise.prototype.then()
  4. Promise.prototype.catch()
  5. Promise.all() // 所有的完成

  6. Promise.race() // 競速,完成一個即可

2、進階

promises 的奇妙在於給予我們以前的 return 與 throw,每個 Promise 都會提供一個 then() 函數,和一個 catch(),實際上是 then(null, ...) 函數:

somePromise().then(functoin(){
  // do something
});

我們可以做三件事:

1. return 另一個 promise
2. return 一個同步的值 (或者 undefined)
3. throw 一個同步異常 ` throw new Eror('');`

 2.1 封裝同步與異步代碼

new Promise(function (resolve, reject) {
  resolve(someValue); });
// 寫成 Promise.resolve(someValue);

2.2  捕獲同步異常

new Promise(function (resolve, reject) {
  throw new Error('悲劇了,又出 bug 了'); }).catch(function(err){   console.log(err); });

如果是同步代碼,可以寫成:

Promise.reject(new Error("什么鬼"));

2.3 多個異常捕獲,更加精准的捕獲

somePromise.then(function() {
  return a.b.c.d();
}).catch(TypeError, function(e) {
//If a is defined, will end up here because
//it is a type error to reference property of undefined
}).catch(ReferenceError, function(e) {
//Will end up here if a wasn't defined at all
}).catch(function(e) {
//Generic catch-the rest, error wasn't TypeError nor
//ReferenceError
});

2.4 獲取兩個 Promise 的返回值

2.4.1 .then 方式順序調用

2.4.2 設定更高層的作用域

2.4.3 spread

(未完待續……)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM