ECMAScript6,也稱爲ECMAScript2015,是JavaScript語言的下一代標準。雖然目前並不是所有瀏覽器都能兼容ES6全部特性,但越來越多的程序員在實際項目當中已經開始
使用ES6了。我們可以點此訪問traceur-compiler 在線版本實時編輯ES6代碼並查看轉換後的結果,代碼運行結果會在console顯示。
最常用的ES6特性
let, const, for of,class, extends, super, arrow
functions,增強的對象字面量, template string, destructuring,默認參數值,不定參數,拓展參
數,import,export,Set,Map,WeakSet,weakMap,proxies,symbol,promise等等
這些是ES6最常用的幾個語法,基本上學會它們,我們就可以走遍天下都不怕啦!我會用最通俗易懂的語言和例子來講解它們,保證一看就懂,一學就會。
let const
這兩個的用途與var
類似,都是用來聲明變量的,但在實際運用中他倆都有各自的特殊用途。
首先來看下面這個例子:
var fullName='hmz';
while(true){
var fullName='yingxuan';
console.log(fullName);//yingxuan
break;
}
console.log(fullName);//yingxuan
使用var
兩次輸出都是obama,這是因爲ES5只有全局作用域和函數作用域,沒有塊級作用域,這帶來很多不合理的場景。第一種場景就是你現在看到的內層變量覆蓋外層
變量。而let
則實際上爲JavaScript新增了塊級作用域。用它所聲明的變量,只在let命令所在的代碼塊內有效。
let fullName='john';
while(true){
let fullName="joe";
console.log(fullName);//joe
break;
}
console.log(fullName);//john
另外一個var
帶來的不合理場景就是用來計數的循環變量泄露爲全局變量,看下面的例子:
var a=[];
for(var i=0;i<10;i++){
a[i]=function(){
console.log(i);
};
}
a[6]();//10
上面代碼中,變量i是var聲明的,在全局範圍內都有效。所以每一次循環,新的i值都會覆蓋舊值,導致最後輸出的是最後一輪的i的值。而使用let則不會出現這個問題。
var a=[];
for(let i=0;i<10;i++){
a[i]=function(){
console.log(i);
}
}
a[6]();//6
再來看一個更常見的例子,瞭解下如果不用ES6,而用閉包如何解決這個問題。
var clickBox=document.querySelectorAll(".clickBox");
for(var i=0;i<clickBox.length;i++){
clickBox[i].onclick=function(){
console.log(i);
}
}
我們本來希望的是點擊不同的clickBox,顯示不同的i,但事實是無論我們點擊哪個clickBox,輸出的都是同一個數。下面我們來看下,如何用閉包搞定它。function iteratorFactory(i){
var onclick=function(e){
console.log(i);
}
return onclick;
}
var clickBox=document.querySelectorAll(".clickBox");
for(var i=0;i<clickBox.length;i++){
clickBox[i].onclick=iteratorFactory(i);
}
const
也用來聲明變量,但是聲明的是常量。一旦聲明,常量的值就不能改變。const PI = Math.PI
PI = 23 //Module build failed: SyntaxError: /es6/app.js: "PI" is read-only
當我們嘗試去改變用const聲明的常量時,瀏覽器就會報錯。const有一個很好的應用場景,就是當我們引用第三方庫的時聲明的變量,用const來聲明可以避免未來不小心重命
名而導致出現bug:
const moment=require('moment');
for of值遍歷
var someArray=["a","b","c"];
for(v of someArray){
console.log(v);//打印a b c
}
class extends super
這三個特性涉及了ES5中最令人頭疼的的幾個部分:原型、構造函數,繼承...你還在爲它們複雜難懂的語法而煩惱嗎?你還在爲指針到底指向哪裏而糾結萬分嗎?
有了ES6我們不再煩惱!
ES6提供了更接近傳統語言的寫法,引入了Class(類)這個概念。新的class寫法讓對象原型的寫法更加清晰、更像面向對象編程的語法,也更加通俗易懂。
class Animal{
constructor(name){
this.name=name;
}
sayName(){
console.log("My name is "+this.name)
}
}
class Programmer extends Animal{
constructor(name){
super(name);
}
program(){
console.log("I'm coding...")
}
}
var animal=new Animal("dummy");
var wayou=new Programmer("wayou");
animal.sayName();//My name is dummy
wayou.sayName();//My name is wayou
wayou.program();//I'm coding...
上面代碼首先用class定義了一個“類”,可以看到裏面有一個constructor方法,這就是構造方法,而this關鍵字則代表實例對象。簡單地說,constructor內定義的方法和屬性是實例對象自己的,而constructor外定義的方法和屬性則是所有實例對象可以共享的。
Class之間可以通過extends關鍵字實現繼承,這比ES5的通過修改原型鏈實現繼承,要清晰和方便很多。上面定義了一個Programmer類,該類通過extends關鍵字,繼承了Animal類的所有屬性和方法。
super關鍵字,它指代父類的實例(即父類的this對象)。子類必須在constructor方法中調用super方法,否則新建實例時會報錯。這是因爲子類沒有自己的this對象,而是繼承父類的this對象,然後對其進行加工。如果不調用super方法,子類就得不到this對象。
ES6的繼承機制,實質是先創造父類的實例對象this(所以必須先調用super方法),然後再用子類的構造函數修改this。
arrow function(箭頭操作符)
function(i){ return i + 1; } //ES5
(i) => i + 1 //ES6
var array=[1,2,3];
//傳統寫法
array.forEach(function(v,i,a){
console.log(v);//1 2 3
});
//ES6
array.forEach(v=>console.log(v));//1 2 3
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指向的是全局對象。所以爲了讓它能夠正確的運行,傳統的解決方法有兩種:- 第一種是將this傳給self,再用self來指代this
says(say){
var self=this;
setTimeout(function(){
console.log(self.type+" says "+say);
},1000);
}
- .第二種方法是用
bind(this)
,即
says(say){
setTimeout(function(){
console.log(this.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。增強的對象字面量
對象字面量被增強了,寫法更加簡潔與靈活,同時在定義對象的時候能夠做的事情更多了。具體表現在:
- 可以在對象字面量裏面定義原型
- 定義方法可以不用function關鍵字
- 直接調用父類方法
這樣一來,對象字面量與前面提到的類概念更加吻合,在編寫面向對象的JavaScript時更加輕鬆方便了。
//通過對象字面量創建對象
var human = {
breathe() {
console.log('breathing...');
}
};
var worker = {
__proto__: human, //設置此對象的原型爲human,相當於繼承human
company: 'freelancer',
work() {
console.log('working...');
}
};
human.breathe();//輸出 ‘breathing...’
//調用繼承來的breathe方法
worker.breathe();//輸出 ‘breathing...’
template string(字符串模板)
$("#result").append(
"There are <b>" + basket.count + "</b> " +
"items in your basket, " +
"<em>" + basket.onSale +
"</em> are on sale!"
);
我們要用一堆的'+'號來連接文本與變量,而ES6中允許使用反引號 ` 來創建字符串,此種方法創建的字符串裏面可以包含由美元符號加花括號包裹的變量${vraible}。如果你使用過像C#等後端強類型語言的話,對此功能應該不會陌生。$("#result").append(`
There are <b>${basket.count}</b> items
in your basket, <em>${basket.onSale}</em>
are on sale!
`);
用反引號(`
)來標識起始,用
${}`來引用變量,而且所有的空格和縮進都會被保留在輸出之中,是不是非常爽?!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
默認參數值 不定參數 拓展參數
1.默認參數值很簡單,意思就是在定義函數的時候指定參數的默認值,而不用像以前那樣通過邏輯或操作符來達到目的了。大家可以看下面的例子,調用animal()
方法時忘了傳參數,傳統的做法就是加上這一句type
= type || 'cat' 來指定默認值。
function animal(type){
type=type || 'cat';
console.log(type);
}
animal();
function animal(type='cat'){
console.log(type);
}
animal();
function sayHello(name){
//傳統的指定默認參數的方式
var name=name||'dude';
console.log('Hello '+name);
}
//運用ES6的默認參數
function sayHello2(name='dude'){
console.log(`Hello ${name}`);
}
sayHello();//輸出:Hello dude
sayHello('Wayou');//輸出:Hello Wayou
sayHello2();//輸出:Hello dude
sayHello2('Wayou');//輸出:Hello Wayou
function animals(...types){
console.log(types);
}
animals('cat','dog','fish');//["cat","dog","fish"]
再來一個例子://將所有參數相加的函數
function add(...x){
return x.reduce((m,n)=>m+n);
}
//傳遞任意個數的參數
console.log(add(1,2,3));//6
console.log(add(1,2,3,4,5));//15
3.拓展參數則是另一種形式的語法糖,它允許傳遞數組或者類數組直接做爲函數的參數而不用通過apply。var people=["wayou","john","sherlock"];
//sayHello函數本來接收三個單獨的參數人一,人二和人三
function sayHello(people1,people2,people3){
console.log(`Hello ${people1},${people2},${people3}`);
}
//但是我們將一個數組以拓展參數的形式傳遞,它能很好地映射到每個單獨的參數
sayHello(...people);//Hello wayou,jobh,sherlock
//而在以前,如果需要傳遞數組當參數,我們需要使用函數的apply方法
sayHello.apply(null,people);//Hello wayou,jobh,sherlock
import export
這兩個傢伙對應的就是es6自己的module功能,在ES6標準中,JavaScript原生支持module了。將不同功能的代碼分別寫在不同文件中,各模塊只需導出公共接口部分,然後通過模塊的導入的方式可以在其他地方使用。
我們之前寫的Javascript一直都沒有模塊化的體系,無法將一個龐大的js工程拆分成一個個功能相對獨立但相互依賴的小工程,再用一種簡單的方法把這些小工程連接在一起。
這有可能導致兩個問題:
-
一方面js代碼變得很臃腫,難以維護
-
另一方面我們常常得很注意每個script標籤在html中的位置,因爲它們通常有依賴關係,順序錯了可能就會出bug
而現在我們有了es6的module功能,它實現非常簡單,可以成爲服務器和瀏覽器通用的模塊解決方案。
ES6模塊的設計思想,是儘量的靜態化,使得編譯時就能確定模塊的依賴關係,以及輸入和輸出的變量。CommonJS和AMD模塊,都只能在運行時確定這些東西。
傳統的寫法
首先我們回顧下require.js的寫法。假設我們有兩個js文件: index.js和content.js,現在我們想要在index.js中使用content.js返回的結果,我們要怎麼做呢?
首先定義:
//content.js
define('content.js', function(){
return 'A cat';
})
然後require:
//index.js
require(['./content.js'], function(animal){
console.log(animal); //A cat
})
那CommonJS是怎麼寫的呢?
//index.js
var animal = require('./content.js')
//content.js
module.exports = 'A cat'
ES6的寫法
//index.js
import animal from './content'
//content.js
export default 'A cat'
ES6 module的其他高級用法
//content.js
export default 'A cat'
export function say(){
return 'Hello!'
}
export const type = 'dog'
上面可以看出,export命令除了輸出變量,還可以輸出函數,甚至是類(react的模塊基本都是輸出類)
//index.js
import { say, type } from './content'
let says = say()
console.log(`The ${type} says ${says}`) //The dog says Hello
這裏輸入的時候要注意:大括號裏面的變量名,必須與被導入模塊(content.js)對外接口的名稱相同。
如果還希望輸入content.js中輸出的默認值(default), 可以寫在大括號外面。
//index.js
import animal, { say, type } from './content'
let says = say()
console.log(`The ${type} says ${says} to ${animal}`)
//The dog says Hello to A cat
修改變量名
此時我們不喜歡type這個變量名,因爲它有可能重名,所以我們需要修改一下它的變量名。在es6中可以用as實現一鍵換名。
//index.js
import animal, { say, type as animalType } from './content'
let says = say()
console.log(`The ${animalType} says ${says} to ${animal}`)
//The dog says Hello to A cat
模塊的整體加載
除了指定加載某個輸出值,還可以使用整體加載,即用星號(*)指定一個對象,所有輸出值都加載在這個對象上面。
//index.js
import animal, * as content from './content'
let says = content.say()
console.log(`The ${content.type} says ${says} to ${animal}`)
//The dog says Hello to A cat
通常星號*結合as一起使用比較合適。
終極祕籍
考慮下面的場景:上面的content.js一共輸出了三個變量(default, say, type),假如我們的實際項目當中只需要用到type這一個變量,其餘兩個我們暫時不需要。我們可以只輸入一個變量:
import { type } from './content'
由於其他兩個變量沒有被使用,我們希望代碼打包的時候也忽略它們,拋棄它們,這樣在大項目中可以顯著減少文件的體積。
ES6幫我們實現了!
不過,目前無論是webpack還是browserify都還不支持這一功能...
如果你現在就想實現這一功能的話,可以嘗試使用rollup.js
他們把這個功能叫做Tree-shaking,哈哈哈,意思就是打包前讓整個文檔樹抖一抖,把那些並未被依賴或使用的東西統統抖落下去。。。
看看他們官方的解釋吧:
Normally if you require a module, you import the whole thing. ES2015 lets you just import the bits you need, without mucking around with custom builds. It's a revolution in how we use libraries in JavaScript, and it's happening right now.
Map Set和WeakMap WeakSet
這些是新加的集合類型,提供了更加方便的獲取屬性值的方法,不用像以前一樣用hasOwnProperty來檢查某個屬性是屬於原型鏈上的呢還是當前對象的。同時,在進行屬性值添加與獲取時有專門的get,set方法。//Sets
var s=new Set();
s.add("hello").add("goodbye").add("hello");
console.log(s);//Set{"hello","goodbye"}
console.log(s.size);//2
console.log(s.has("hello"));//true
//Maps
var m=new Map();
m.set("hello",42);
m.set(s,34);
console.log(m);//Map{"hello"=>42,Set{...}=>34}
console.log(m.get(s));//34
有時候我們會把對象作爲一個對象的鍵用來存放屬性值,普通集合類型比如簡單對象會阻止垃圾回收器對這些作爲屬性鍵存在的對象的回收,有造成內存泄漏的危險。//Weak Maps
var wm=new WeakMap();
wm.set(s,{extra:42});
console.log(wm.size); //報錯,s is undefined
//Weak Sets
var ws=new WeakSet();
ws.add({data:42});//因爲添加到ws的這個臨時對象沒有其他變量引用它,所以ws不會保存它的值,也就是說這次添加其實沒有意思
console.log(ws.size);//undefined
Proxies
Proxy可以監聽對象身上發生了什麼事情,並在這些事情發生後執行一些相應的操作。一下子讓我們對一個對象有了很強的追蹤能力,同時在數據綁定方面也很有用處。//定義被偵聽的目標對象
var engineer={name:"Joe Sixpack",salary:50};
//定義處理程序
var interceptor={
set:function(receiver,property,value){
console.log(property," is changed to ",value);
receiver[property]=value;
}
};
//創建代理以進行偵聽
engineer=Proxy(engineer,interceptor);
//做一些改動來觸發代理
engineer.salary=60;//salary is changed to 60
上面代碼我已加了註釋,這裏進一步解釋。對於處理程序,是在被偵聽的對象身上發生了相應事件之後,處理程序裏面的方法就會被調用,上面例子中我們設置了set的處理函數,表明,如果我們偵聽的對象的屬性被更改,也就是被set了,那這個處理程序就會被調用,同時通過參數能夠得知是哪個屬性被更改,更改爲了什麼值。Symbols
(function () {
//創建symbol
var key=Symbol("key");
function MyClass(privateData){
this[key]=privateData;
}
MyClass.prototype={
doStuff:function(){
...this[key]...
}
};
})();
var c=new MyClass("hello");
console.log(c["key"]);//無法訪問該屬性,因爲是私有的
Math Number String Object的新API
Number.EPSILON
Number.isInteger(Infinity) // false
Number.isNaN("NaN") // false
Math.acosh(3) // 1.762747174039086
Math.hypot(3, 4) // 5
Math.imul(Math.pow(2, 32) - 1, Math.pow(2, 32) - 2) // 2
"abcde".contains("cd") // true
"abc".repeat(3) // "abcabcabc"
Array.from(document.querySelectorAll('*')) // Returns a real Array
Array.of(1, 2, 3) // Similar to new Array(...), but without special one-arg behavior
[0, 0, 0].fill(7, 1) // [0,7,7]
[1,2,3].findIndex(x => x == 2) // 1
["a", "b", "c"].entries() // iterator [0, "a"], [1,"b"], [2,"c"]
["a", "b", "c"].keys() // iterator 0, 1, 2
["a", "b", "c"].values() // iterator "a", "b", "c"
Object.assign(Point, { origin: new Point(0,0) })
Promises
Promises是處理異步操作的一種模式,之前在很多三方庫中有實現,比如jQuery的deferred 對象。當你發起一個異步請求,並綁定了.when(),.done()等事件處理程序時,其實就是在應用promise模式。//創建promise
var promise = new Promise(function(resolve, reject) {
// 進行一些異步或耗時操作
if ( /*如果成功 */ ) {
resolve("Stuff worked!");
} else {
reject(Error("It broke"));
}
});
//綁定處理程序
promise.then(function(result) {
//promise成功的話會執行這裏
console.log(result); // "Stuff worked!"
}, function(err) {
//promise失敗會執行這裏
console.log(err); // Error: "It broke"
});