前言
ES6,全稱ECMAScript 6,是ECMA委員會在 2015年6月正式發布的新ECMAScript標准。所以又稱ECMAScript 2015,也就是說,ES6就是ES2015。至今各大瀏覽器廠商所開發的 JavaScript 引擎都還沒有完成對 ES2015 中所有特性的完美支持,於是乎如 babel、Traceur 等編譯器便出現了。它們能將尚未得到支持的 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 的類機制依然很雞肋:
- 不支持私有屬性(
private
) - 不支持前置屬性定義,但可用
get
語句和set
語句實現 - 不支持多重繼承
- 沒有類似於協議(
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, 在很久以后 ,我們可以直接作用import和export在瀏覽器中導入和導出各個模塊了, 一個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
- Promise.resolve()
- Promise.reject()
- Promise.prototype.then()
- Promise.prototype.catch()
-
Promise.all() // 所有的完成
- 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
(未完待續……)