前言
ECMAScript 6.0(以下簡稱 ES6)是 JavaScript 語言的下一代標准,已經在 2015 年 6 月正式發布了。它的目標,是使得 JavaScript 語言可以用來編寫復雜的大型應用程序,成為企業級開發語言。
這句話基本涵蓋了為什么會產生ES6這次更新的原因——編寫復雜的大型應用程序。
回顧近兩年的前端開發,復雜度確實在快速增加,近期不論從系統復雜度還是到前端開發人員數量應該達到了一個飽和值,換個方式說,沒有ES6我們的前端代碼依舊可以寫很多復雜的應用,而ES6的提出更好的幫我們解決了很多歷史遺留問題,另一個角度ES6讓JS更適合開發大型應用,而不用引用太多的庫了。
本文,簡單介紹幾個ES6核心概念,個人感覺只要掌握以下新特性便能愉快的開始使用ES6做代碼了!
這里的文章,請配合着阮老師這里的教程,一些細節阮老師那邊講的好得多:http://es6.ruanyifeng.com/#docs/class-extends
除了阮老師的文章還參考:http://www.infoq.com/cn/articles/es6-in-depth-arrow-functions
PS:文中只是個人感悟,有誤請在評論提出
模塊Module的引入
都說了復雜的大型應用了,所以我們第一個要討論的重要特性就是模塊概念,我們做一個復雜的項目必定需要兩步走:
① 分得開,並且需要分開
② 合得起來
我們普遍認為沒有復雜的應用,只有分不開的應用,再復雜的應用,一旦可以使用組件化、模塊化的方式分成不同的小單元,那么其難度便會大大降低,模塊化是大型、復雜項目的主要攔路虎。為了解決這個問題,社區制定了一些模塊加載方案,對於瀏覽器開發來說,我們用的最多的是AMD規范,也就是大家熟知的requireJS,而ES6中在語音標准層面實現了模塊功能,用以取代服務端通信的CommonJS和AMD規范,成為了通用的規范,多說無益,我們這里上一段代碼說明:
1 /* 2 validate.js 多用於表單驗證 3 */ 4 export function isEmail (text) { 5 var reg = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 6 return reg.test(text); 7 } 8 9 export function isPassword (text) { 10 var reg = /^[a-zA-Z0-9]{6,20}$/; 11 return reg.test(text); 12 }
那么我們現在想在頁面里面使用這個工具類該怎么做呢:
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>Title</title> 6 </head> 7 <body> 8 <!-- 請注意這里type=module才能運行 --> 9 <script type="module"> 10 import {isEmail} from './validate.js'; 11 var e1 = 'dddd'; 12 var e2 = 'yexiaochai@qq.com' 13 console.log(isEmail(e1)) 14 console.log(isEmail(e2)) 15 </script> 16 </body> 17 </html>
ES6中的Module提出,在我這里看來是想在官方完成之前requireJS干的工作,這里也有一些本質上的不一樣:
① requireJS是使用加載script標簽的方式載入js,沒有什么限制
② import命令會被js引擎靜態分析,先於模塊其他語句執行
以上特性會直接給我們帶來一些困擾,比如原來我們項目控制器會有這么一段代碼:
1 var viewId = ''; //由瀏覽器獲取試圖id,url可能為?viewId=booking|list|... 2 //如果不存在則需要構建,記住構建時需要使用viewdata繼承源view 3 requirejs(viewId, function(View) { 4 //執行根據url參數動態加載view邏輯 5 })
前面說過了,import命令會被js引擎靜態分析,先於模塊其他語句執行,所以我們在根本不能將import執行滯后,或者動態化,做不到的,這種寫法也是報錯的:
if (viewId) { import view from './' + viewId; }
這種設計會有利於提高編譯器效率,但是之前的動態業務邏輯就不知道如何繼續了?而ES6如果提供import的方法,我們變可以執行邏輯:
1 import(viewId, function() {
2 //渲染頁面
3 })
事實上他也提供了:
現在看起來,JS中的模塊便十分完美了,至於其中一些細節,便可以用到的時候再說了
ES6中的類Class
我們對我們的定位一直是非常清晰的,我們就是要干大項目的,我們是要干復雜的項目,除了模塊概念,類的概念也非常重要,我們之前用的這種方式實現一個類,我們來溫故而知新。
當一個函數被創建時,Function構造函數產生的函數會隱式的被賦予一個prototype屬性,prototype包含一個constructor對象
而constructor便是該新函數對象(constructor意義不大,但是可以幫我們找到繼承關系)
每個函數都會有一個prototype屬性,該屬性指向另一對象,這個對象包含可以由特定類型的所有實例共享的屬性和方法
每次實例化后,實例內部都會包含一個[[prototype]](__proto__)的內部屬性,這個屬性指向prototype
① 我們通過isPrototypeOf來確定某個對象是不是我的原型 ② hasOwnPrototype 可以檢測一個屬性是存在實例中還是原型中,該屬性不是原型屬性才返回true
var Person = function (name, age) {
this.name = name; this.age = age; }; Person.prototype.getName = function () { return this.name; }; var y = new Person('葉小釵', 30);
為了方便,使用,我們做了更為復雜的封裝:

1 var arr = []; 2 var slice = arr.slice; 3 4 function create() { 5 if (arguments.length == 0 || arguments.length > 2) throw '參數錯誤'; 6 7 var parent = null; 8 //將參數轉換為數組 9 var properties = slice.call(arguments); 10 11 //如果第一個參數為類(function),那么就將之取出 12 if (typeof properties[0] === 'function') 13 parent = properties.shift(); 14 properties = properties[0]; 15 16 function klass() { 17 this.initialize.apply(this, arguments); 18 } 19 20 klass.superclass = parent; 21 klass.subclasses = []; 22 23 if (parent) { 24 var subclass = function () { }; 25 subclass.prototype = parent.prototype; 26 klass.prototype = new subclass; 27 parent.subclasses.push(klass); 28 } 29 30 var ancestor = klass.superclass && klass.superclass.prototype; 31 for (var k in properties) { 32 var value = properties[k]; 33 34 //滿足條件就重寫 35 if (ancestor && typeof value == 'function') { 36 var argslist = /^\s*function\s*\(([^\(\)]*?)\)\s*?\{/i.exec(value.toString())[1].replace(/\s/i, '').split(','); 37 //只有在第一個參數為$super情況下才需要處理(是否具有重復方法需要用戶自己決定) 38 if (argslist[0] === '$super' && ancestor[k]) { 39 value = (function (methodName, fn) { 40 return function () { 41 var scope = this; 42 var args = [function () { 43 return ancestor[methodName].apply(scope, arguments); 44 } ]; 45 return fn.apply(this, args.concat(slice.call(arguments))); 46 }; 47 })(k, value); 48 } 49 } 50 51 klass.prototype[k] = value; 52 } 53 54 if (!klass.prototype.initialize) 55 klass.prototype.initialize = function () { }; 56 57 klass.prototype.constructor = klass; 58 59 return klass; 60 }
這里寫一個demo:

1 var AbstractView = create({ 2 initialize: function (opts) { 3 opts = opts || {}; 4 this.wrapper = opts.wrapper || $('body'); 5 6 //事件集合 7 this.events = {}; 8 9 this.isCreate = false; 10 11 }, 12 on: function (type, fn) { 13 if (!this.events[type]) this.events[type] = []; 14 this.events[type].push(fn); 15 }, 16 trigger: function (type) { 17 if (!this.events[type]) return; 18 for (var i = 0, len = this.events[type].length; i < len; i++) { 19 this.events[type][i].call(this) 20 } 21 }, 22 createHtml: function () { 23 throw '必須重寫'; 24 }, 25 create: function () { 26 this.root = $(this.createHtml()); 27 this.wrapper.append(this.root); 28 this.trigger('onCreate'); 29 this.isCreate = true; 30 }, 31 show: function () { 32 if (!this.isCreate) this.create(); 33 this.root.show(); 34 this.trigger('onShow'); 35 }, 36 hide: function () { 37 this.root.hide(); 38 } 39 }); 40 41 var Alert = create(AbstractView, { 42 43 createHtml: function () { 44 return '<div class="alert">這里是alert框</div>'; 45 } 46 }); 47 48 var AlertTitle = create(Alert, { 49 initialize: function ($super) { 50 this.title = ''; 51 $super(); 52 53 }, 54 createHtml: function () { 55 return '<div class="alert"><h2>' + this.title + '</h2>這里是帶標題alert框</div>'; 56 }, 57 58 setTitle: function (title) { 59 this.title = title; 60 this.root.find('h2').html(title) 61 } 62 63 }); 64 65 var AlertTitleButton = create(AlertTitle, { 66 initialize: function ($super) { 67 this.title = ''; 68 $super(); 69 70 this.on('onShow', function () { 71 var bt = $('<input type="button" value="點擊我" />'); 72 bt.click($.proxy(function () { 73 alert(this.title); 74 }, this)); 75 this.root.append(bt) 76 }); 77 } 78 }); 79 80 var v1 = new Alert(); 81 v1.show(); 82 83 var v2 = new AlertTitle(); 84 v2.show(); 85 v2.setTitle('我是標題'); 86 87 var v3 = new AlertTitleButton(); 88 v3.show(); 89 v3.setTitle('我是標題和按鈕的alert');
ES6中直接從標准層面解決了我們的問題,他提出了Class關鍵詞讓我們可以更好的定義類,我們這里用我們ES6的模塊語法重新實現一次:
1 export class AbstractView { 2 constructor(opts) { 3 opts = opts || {}; 4 this.wrapper = opts.wrapper || $('body'); 5 //事件集合 6 this.events = {}; 7 this.isCreate = false; 8 } 9 on(type, fn) { 10 if (!this.events[type]) this.events[type] = []; 11 this.events[type].push(fn); 12 } 13 trigger(type) { 14 if (!this.events[type]) return; 15 for (var i = 0, len = this.events[type].length; i < len; i++) { 16 this.events[type][i].call(this) 17 } 18 } 19 createHtml() { 20 throw '必須重寫'; 21 } 22 create() { 23 this.root = $(this.createHtml()); 24 this.wrapper.append(this.root); 25 this.trigger('onCreate'); 26 this.isCreate = true; 27 } 28 show() { 29 if (!this.isCreate) this.create(); 30 this.root.show(); 31 this.trigger('onShow'); 32 } 33 hide() { 34 this.root.hide(); 35 } 36 } 37 export class Alert extends AbstractView { 38 createHtml() { 39 return '<div class="alert">這里是alert框</div>'; 40 } 41 } 42 export class AlertTitle extends Alert { 43 constructor(opts) { 44 super(opts); 45 this.title = ''; 46 } 47 createHtml() { 48 return '<div class="alert"><h2>' + this.title + '</h2>這里是帶標題alert框</div>'; 49 } 50 setTitle(title) { 51 this.title = title; 52 this.root.find('h2').html(title) 53 } 54 } 55 export class AlertTitleButton extends AlertTitle { 56 constructor(opts) { 57 super(opts); 58 this.on('onShow', function () { 59 var bt = $('<input type="button" value="點擊我" />'); 60 bt.click($.proxy(function () { 61 alert(this.title); 62 }, this)); 63 this.root.append(bt) 64 }); 65 } 66 }
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>Title</title> 6 </head> 7 <body> 8 <script type="text/javascript" src="zepto.js"></script> 9 10 <!-- 請注意這里type=module才能運行 --> 11 <script type="module"> 12 import {Alert, AlertTitle, AlertTitleButton} from './es6class.js'; 13 var v1 = new Alert(); 14 v1.show(); 15 var v2 = new AlertTitle(); 16 v2.show(); 17 v2.setTitle('我是標題'); 18 var v3 = new AlertTitleButton(); 19 v3.show(); 20 v3.setTitle('我是標題和按鈕的alert'); 21 </script> 22 </body> 23 </html>
這里的代碼完成了與上面一樣的功能,而代碼更加的清爽了。
ES6中的函數
我們這里學習ES6,由大到小,首先討論模塊,其次討論類,這個時候理所當然到了我們的函數了,ES6中函數也多了很多新特性或者說語法糖吧,首先我們來說一下這里的箭頭函數
箭頭函數
//ES5 $('#bt').click(function (e) { //doing something }) //ES6 $('#bt').click(e => { //doing something })
有點語法糖的感覺,有一個很大不同的是,箭頭函數不具有this屬性,箭頭函數直接使用的是外部的this的作用域,這個想不想用看個人習慣吧。
參數新特性
ES6可以為參數提供默認屬性
1 function log(x, y = 'World') { 2 console.log(x, y); 3 } 4 5 log('Hello') // Hello World 6 log('Hello', 'China') // Hello China 7 log('Hello', '') // Hello
至於不定參數撒的,我這里沒有多過多的使用,等項目遇到再說吧,如果研究的太細碎,反而不適合我們開展工作。
let、const和var
之前的js世界里,我們定義變量都是使用的var,別說還真挺好用的,雖有會有一些問題,但是對於熟悉js特性的小伙伴都能很好的解決,一般記住:變量提升會解決絕大多數問題。
就能解決很多問題,而且真實項目中,我們會會避免出現變量出現重名的情況所以有時候大家面試題中看到的場景在實際工作中很少發生,只要不刻意臆想、制造一些難以判斷的場景,其實並不會出現多少BUG,不能因為想考察人家對語言特性的了解,就做一些容易容易忘掉的陷阱題。
無論如何,var 聲明的變量受到了一定詬病,事實上在強類型語言看來也確實是設計BUG,但是完全廢棄var的使用顯然不是js該做的事情,這種情況下出現了let關鍵詞。
let與var一致用以聲明變量,並且一切用var的地方都可以使用let替換,新的標准也建議大家不要再使用var了,let具有更好的作用域規則,也許這個規則是邊界更加清晰了:
{ let a = 10; var b = 1; } a // ReferenceError: a is not defined. b // 1
這里是一個經典的閉包問題:
var a = []; for (var i = 0; i < 10; i++) { a[i] = function () { console.log(i); }; } a[6](); // 10
因為i在全局范圍有效,共享同一個作用域,所以i就只有10了,為了解決這個問題,我們之前會引入閉包,產生新的作用域空間(好像學名是變量對象,我給忘了),但是那里的i跟這里的i已經不是一個東西了,但如果將var改成let,上面的答案是符合預期的。可以簡單理解為每一次“{}”,let定義的變量都會產生新的作用域空間,這里產生了循環,所以每一次都不一樣,這里與閉包有點類似是開辟了不同的空間。
for (let i = 0; i < 3; i++) { let i = 'abc'; console.log(i); } // abc // abc // abc
這里因為內部重新聲明了i,事實上產生了3個作用域,這里一共有4個作用域指向,let最大的作用就是js中塊級作用域的存在,並且內部的變量不會被外部所訪問,所以之前為了防止變量侮辱的立即執行函數,似乎變得不是那么必要了。
之前我們定義一個常量會采用全部大寫的方式:
var NUM = 10;
為了解決這個問題,ES6引入了const命令,讓我們定義只讀常量,這里不對細節做過多研究,直接后續項目實踐吧,項目出真知。
生成器Generators
ES6中提出了生成器Generators的概念,這是一種異步編程的解決方案,可以將其理解為一種狀態機,封裝了多個內部狀態,這里來個demo:
function* helloWorldGenerator() { yield 'hello'; yield 'world'; return 'ending'; } var hw = helloWorldGenerator(); hw.next() // { value: 'hello', done: false } hw.next() // { value: 'world', done: false } hw.next() // { value: 'ending', done: true } hw.next() // { value: undefined, done: true }
這個yield(產出)類似於之前的return,直觀的理解就是一個函數可以返回多次了,或者說函數具有“順序狀態”,yield提供了暫停功能。這里我想寫個代碼來驗證下期中的作用域狀態:
function* test(){ let i = 0; setTimeout(function() { i++; }, 1000); yield i; yield i++; return i } let t = test(); console.log(t.next()); setTimeout(function() { console.log(t.next()); }, 2000); console.log(t.next()); //{value: 0, done: false} //{value: 0, done: false} //{value: 2, done: true}
之前我們寫一個城市級聯的代碼,可能會有些令人蛋疼:
1 $.get('getCity', {id: 0}, function(province) { 2 let pid = province[0]; 3 //根據省id獲取城市數據 4 $.get('getCity', {id: pid}, function(city) { 5 let cityId = city[0]; 6 //根據市級id獲取縣 7 $.get('getCity', {id: cityId}, function(city) { 8 //do smt. 9 }); 10 }); 11 });
這個代碼大家應當比較熟悉了,用promise能從語法層面解決一些問題,這里簡單介紹下promise。
Promise
Promise是一種異步解決方案,有些同事認為其出現就是為了我們代碼變得更好看,解決回調地獄的語法糖,ES6將其寫入了語音標准,提供了原生Promise對象。Promise為一容器,里面保存異步事件的結果,他是一個對象具有三個狀態:pending(進行中)、fulfilled(已成功)、rejected(已失敗),這里還是來個簡單代碼說明:
1 function timeout(ms) { 2 return new Promise((resolve, reject) => { 3 setTimeout(resolve, ms, 'done'); 4 }); 5 } 6 7 timeout(100).then((value) => { 8 console.log(value); 9 });
實例化Promise時,第一個回調必須提供,是進行轉為成功時候會執行,第二個也是一個函數失敗時候調用,非必須,這里來個demo:
1 let timeout = function (ms) { 2 return new Promise(function (resolve) { 3 setTimeout(resolve, ms); 4 }); 5 }; 6 7 timeout(1000).then(function () { 8 return timeout(1000).then(function () { 9 let s = '大家'; 10 console.log(s) 11 return s; 12 }) 13 14 }).then(function (data) { 15 return timeout(1000).then(function () { 16 let s = data + '好,'; 17 console.log(s) 18 return s; 19 }) 20 }).then(function(data) { 21 return timeout(1000).then(function () { 22 let s = data + '我是葉小釵'; 23 console.log(s) 24 return s; 25 }); 26 }).then(function(data) { 27 console.log(data) 28 });
如果我們請求有依賴的話,第一個請求依賴於第二個請求,代碼就可以這樣寫:
1 let getData = function(url, param) { 2 return new Promise(function (resolve) { 3 $.get(url, param, resolve ); 4 }); 5 } 6 getData('http://api.kuai.baidu.com/city/getstartcitys?callback=?').then(function (data) { 7 console.log('我獲取了省數據,我們馬上根據省數據申請市數據', data); 8 return getData('http://api.kuai.baidu.com/city/getstartcitys?callback=?').then(function (data1) { 9 console.log(data1); 10 return '我是市數據'; 11 }) 12 13 }).then(function(data) { 14 //前面的參數傳過來了 15 console.log(data); 16 console.log('我獲取了市數據,我們馬上根據市數據申請縣數據'); 17 getData('http://api.kuai.baidu.com/city/getstartcitys?callback=?').then(function (data1) { 18 console.log(data1); 19 }); 20 })
如此便可以避免多層嵌套了,關於Promise的知識點還很多,我們遇到復雜的工作場景再拿出來說吧,我對他的定位就是一個語法糖,將異步的方式變成同步的寫法,骨子里還是異步,上面我們用Promise解決回調地獄問題,但是回調地獄問題遇到的不多,卻發現Promise一堆then看見就有點煩,我們的Generator函數似乎可以讓這個情況得到緩解。
但是暫時在實際工作中我沒有找到更好的使用場景,這里暫時到這里,后面工作遇到再詳述,對這塊不是很熟悉也不妨礙我們使用ES6寫代碼。
代理
代理,其實就是你要做什么我幫你做了就行了,一般代理的原因都是,我需要做點手腳,或者多點操作,或者做點“賦能”,如我們常常包裝setTimeout一般:
1 let timeout = function (ms, callback) { 2 setTimeout(callback, ms); 3 }
我們包裝setTimeout往往是為了clearTimeout的時候能全部清理掉,其實就是攔截下,ES6提供了Proxy關鍵詞用於設置代理器:
1 var obj = new Proxy({}, { 2 get: function (target, key, receiver) { 3 console.log(`getting ${key}!`); 4 return Reflect.get(target, key, receiver); 5 }, 6 set: function (target, key, value, receiver) { 7 console.log(`setting ${key}!`); 8 return Reflect.set(target, key, value, receiver); 9 } 10 }); 11 obj.count = 1 12 // setting count! 13 ++obj.count 14 // getting count! 15 // setting count! 16 // 2
//target參數表示所要攔截的目標對象,handler參數也是一個對象,用來定制攔截行為 var proxy = new Proxy(target, handler);
我們這里繼續寫一個簡單的demo:
1 let person = { 2 constructor: function(name, age = 20) { 3 this.name = name; 4 this.age = age 5 }, 6 addAge: function() { 7 this.age++; 8 }, 9 getAge: function() { 10 return this.age; 11 } 12 } 13 14 var proxy = new Proxy(person, { 15 get: function(target, property) { 16 console.log(arguments); 17 return target[property]; 18 }, 19 set: function(target, property) { 20 console.log(arguments); 21 } 22 }); 23 24 person.constructor('葉小釵', 30); 25 console.log(person.age) 26 console.log(proxy.age)
但是暫時我沒有發現比較好的業務場景,比如說,我現在重寫了一個實例的get方法,便能在一個全局容器中記錄這個被執行了多少次,這里一個業務場景是:我一次個頁面連續發出了很多次請求,但是我單頁應用做頁面跳轉時候,我需要將所有的請求句柄移除,這個似乎也不是代理完成的工作,於是要使用ES6寫代碼,似乎可以暫時忽略代理。
結語
有了以上知識,基本從程序層面可以使用ES6寫代碼了,但是工程層面還需要引入webpack等工具,這些我們下次介紹吧。