-
ECMAScript 6
鎖定
ECMAScript 6(簡稱ES6)是於2015年6月正式發佈的JavaScript語言的標準,正式名為ECMAScript 2015(ES2015)。它的目標是使得JavaScript語言可以用來編寫複雜的大型應用程序,成為企業級開發語言
[1]
。
另外,一些情況下ES6也泛指ES2015及之後的新增特性,雖然之後的版本應當稱為ES7、ES8等。
- 外文名
- ECMAScript 6
- 簡 稱
- ES6
- 類 型
- 前端語言
- 通過日期
- 2015年6月
- 正式名字
- ECMAScript 2015(ES2015)
- 性 質
- JavaScript語言的標準
ECMAScript 6發展歷史
2000年,ECMAScript 4.0開始醖釀。這個版本最後沒有通過,但是它的大部分內容被ECMAScript6繼承了。因此,ECMAScript6制定的起點其實是2000年。
2007年10月,ECMAScript 4.0草案發布,本來預計2008年8月發佈正式版本。但是,各方對於是否通過這個標準,發生了嚴重分歧。以Yahoo、Microsoft、Google為首的大公司,反對JavaScript的大幅升級,主張小幅改動;以JavaScript創造者Brendan Eich為首的Mozilla公司,則堅持當前的草案。
2008年7月,由於對於下一個版本應該包括哪些功能,各方分歧太大,爭論過於激烈,ECMA開會決定,中止ECMAScript 4.0的開發,將其中涉及現有功能改善的一小部分,發佈為ECMAScript 3.1,而將其他激進的設想擴大範圍,放入以後的版本,由於會議的氣氛,該版本的項目代號起名為Harmony(和諧)。會後不久,ECMAScript 3.1就改名為ECMAScript 5。
2009年12月,ECMAScript 5.0正式發佈。Harmony項目則一分為二,一些較為可行的設想定名為 JavaScript.next繼續開發,後來演變成ECMAScript 6;一些不是很成熟的設想,則被視為JavaScript.next.next,在更遠的將來再考慮推出。
2011年,ECMAScript 5.1發佈後開始6.0版的制定。
2013年3月,ECMAScript 6草案凍結,不再添加新功能。新的功能設想將被放到ECMAScript 7。
2013年12月,ECMAScript 6草案發布。然後是12個月的討論期,聽取各方反饋。由於這個版本引入的語法功能太多,而且制定過程當中,還有很多組織和個人不斷提交新功能。標準委員會最終決定,標準在每年的6月份正式發佈一次,作為當年的正式版本。接下來的時間,就在這個版本的基礎上做改動,直到下一年的6月份,草案就自然變成了新一年的版本。
2015年6月,ECMAScript 6(ES6)正式通過,成為國際標準,正式名稱是“ECMAScript 2015”(簡稱ES2015)。
2016年6月,小幅修訂的“ECMAScript 2016”(簡稱ES2016或ES7)標準發佈,相當於ES6.1版,因為兩者的差異非常小(只新增了數組實例的includes方法和指數運算符),基本上是同一個標準
[1]
。
ECMAScript 6新增功能
ECMAScript 6聲明命令
1. let命令
ES6新增了let命令,用來聲明變量。它的用法類似於var,但是所聲明的變量,只在let命令所在的代碼塊內有效。下面代碼在代碼塊之中,分別用let和var聲明瞭兩個變量。然後在代碼塊之外調用這兩個變量,結果let聲明的變量報錯,var聲明的變量返回了正確的值。這表明,let聲明的變量只在它所在的代碼塊有效。
{ let a = 10; var b = 1; } console.log(b); // 1 console.log(a); // 報錯a沒有定義
for循環的計數器,就很合適使用let命令,計數器i只在for循環體內有效,在循環體外引用就會報錯。
下面的代碼如果使用var,最後輸出的是10。因為變量i是var命令聲明的,在全局範圍內都有效,每一次循環,變量i的值都會發生改變,而循環內被賦給數組a的函數內部的console.log(i),裏面的i指向的就是全局的i。也就是説,所有數組a的成員裏面的i,指向的都是同一個i,導致運行時輸出的是最後一輪的i的值,也就是10。
如果使用let,聲明的變量僅在塊級作用域內有效,最後輸出的是6。因為變量i是let聲明的,當前的i只在本輪循環有效,所以每一次循環的i其實都是一個新的變量,所以最後輸出的是6。由於JavaScript引擎內部會記住上一輪循環的值,初始化本輪的變量i時,就在上一輪循環的基礎上進行計算:
var a = []; for (var i = 0; i < 10; i++) { a[i] = function () { console.log(i); }; } a[6](); // 10 var a = []; for (let i = 0; i < 10; i++) { a[i] = function () { console.log(i); }; } a[6](); // 6
let不允許在相同作用域內,重複聲明同一個變量,不能在函數內部相同模塊範圍重新聲明參數。
let a = 10;// 即使聲明是var a = 10;後面一樣報錯 let a = 1;// 報錯 function func(arg) { let arg; // 調用時因同範圍重名報錯 } function func(arg) { { let arg; // 不報錯,因為對上一個arg來看在子模塊中 } }
另外,for循環還有一個特別之處,就是設置循環變量的那部分是一個單獨的父作用域,而循環體內部是子作用域:
let i = 123; console.log(i); for (let i = 0; i < 2; i++,console.log(i)) { let i = 'abc'; console.log(i); } // 123 // abc // 1 // abc // 2
var命令會發生“變量提升”現象,即變量可以在聲明之前使用值為了糾正這種現象,let命令改變了語法行為,它所聲明的變量一定要在聲明後使用,否則報錯。這在語法上,稱為“暫時性死區”(temporal dead zone,簡稱TDZ)。例如:在let x前增加一句typeof x就會報錯,因為同一塊作用域在let x之前,x無法進行任何操作。
let實際上為JavaScript新增了塊級作用域,在{}被包圍的範圍外,不受內層的let變量影響(但會受var的“變量提升”影響):
function text1(){ let n = 5; //或var n = 5; if (true) { let n = 10; } console.log(n); // 5 } function text2(){ var n = 5; if (true) { var n = 10; } console.log(n); // 10 } function text3(){ let n = 5; if (true) { var n = 10; //報錯,已經聲明瞭n } }
2. const命令
const聲明一個只讀的常量。一旦聲明,常量的值就不能改變,且聲明時必須立即初始化,不能留到以後賦值。const的作用域與let命令相同:只在聲明所在的塊級作用域內有效。
const實際上保證的,並不是變量的值不得改動,而是變量指向的那個內存地址不得改動。對於簡單類型的數據(數值、字符串、布爾值),值就保存在變量指向的那個內存地址,因此等同於常量。但對於複合類型的數據(主要是對象和數組),變量指向的內存地址,保存的只是一個指針,const只能保證這個指針是固定的,至於它指向的數據結構是不是可變的,就完全不能控制了。因此,將一個對象聲明為常量必須非常小心。
const foo = {}; // const foo = []同理,可以正常使用push等功能 foo.prop = 123; // 為foo添加一個屬性,可以成功 console.log(foo.prop); //123 foo = {}; // 將foo指向另一個對象,就會報錯
3.Class命令
ES6 提供了更接近傳統語言的寫法,引入了Class(類)這個概念(類的數據類型就是函數,類本身就指向構造函數),作為對象的模板。通過class關鍵字,可以定義類。class可以看作只是一個語法糖,它的絕大部分功能,ES5都可以做到,新的class寫法只是讓對象原型的寫法更加清晰、更像面向對象編程的語法而已:
function Point(x, y) { this.x = x; this.y = y; } Point.prototype.toString = function () { return '(' + this.x + ', ' + this.y + ')'; }; // 上面為原先寫法,下面為ES6的Class寫法 class Point { constructor(x, y) { // 構造方法,this關鍵字代表實例對象 this.x = x; this.y = y; } toString() { // 自定義方法,方法之間不需要逗號分隔,加了會報錯 return '(' + this.x + ', ' + this.y + ')'; } }
構造函數的prototype屬性,在ES6的類上面繼續存在。事實上,類的所有方法都定義在類的prototype屬性上面。但類的內部所有定義的方法,都是不可枚舉的(non-enumerable):
class Point { constructor() { } toString() { } toValue() { } } console.log(Object.keys(Point.prototype)); // [] 不可枚舉 console.log(Object.getOwnPropertyNames(Point.prototype)); // ["constructor", "toString", "toValue"] // 相當於 function Point() { } Point.prototype = { constructor() {}, toString() {}, toValue() {}, }; console.log(Object.keys(Point.prototype)); // ["constructor", "toString", "toValue"]
類的屬性名,可以採用表達式:
let methodName = 'getArea'; class Square { [methodName]() { } }
與函數一樣,類也可以使用表達式的形式定義。下面代碼使用表達式定義了一個類。需要注意的是,這個類的名字是MyClass而不是Me,Me只在 Class 的內部代碼可用,指代當前類:
const MyClass = class Me { // 如果類的內部沒用到的話,可以省略Me getClassName() { return Me.name; } }; let inst = new MyClass(); console.log(inst.getClassName()) // Me Me.name // 報錯,Me沒有定義
類相當於實例的原型,所有在類中定義的方法,都會被實例繼承。如果在一個方法前,加上static關鍵字,就表示該方法不會被實例繼承,而是直接通過類來調用,這就稱為“靜態方法”。
class Foo { static classMethod() { return 'hello'; } } Foo.classMethod() // 'hello' var foo = new Foo(); foo.classMethod() // 報錯foo.classMethod不是一個函數(不存在該方法)
如果靜態方法包含this關鍵字,這個this指的是類,而不是實例。靜態方法可以與非靜態方法重名,父類的靜態方法,可以被子類繼承:
class Foo { static bar () { this.baz(); //等同於調用Foo.baz } static baz () { console.log('hello'); } baz () { console.log('world'); } } Foo.bar() // hello class Bar extends Foo { } Bar.bar() // hello
4.import命令
import雖然屬於聲明命令,但它是和export命令配合使用的。export命令用於規定模塊的對外接口,import命令用於輸入其他模塊提供的功能。
一個模塊就是一個獨立的文件。該文件內部的所有變量,外部無法獲取。如果外部能夠讀取模塊內部的某個變量、函數或類,就必須使用export關鍵字輸出。export輸出的變量就是本來的名字,但是可以使用as關鍵字重命名:
// profile.js export var firstName = 'Michael'; export function f() {}; export var year = 1958; //寫法2,與上等同 var firstName = 'Michael'; function f() {}; var y = 1958; export {firstName, f, y as year};
export語句輸出的接口,與其對應的值是動態綁定關係,即通過該接口,可以取到模塊內部實時的值。這一點與CommonJS規範完全不同,CommonJS模塊輸出的是值的緩存。export命令可以出現在模塊的任何位置,只要處於模塊頂層就可以。如果處於塊級作用域內,就會報錯:
export var foo = 'bar'; setTimeout(() => foo = 'baz', 500); // 輸出變量foo,值為bar,500毫秒之後變成baz function foo() { export default 'bar' // 語法錯誤 }
使用export命令定義了模塊的對外接口以後,其他JS文件就可以通過import命令加載這個模塊,變量名必需與被導入模塊(profile.js)對外接口的名稱相同。import命令可以使用as關鍵字,將輸入的變量重命名。除了指定加載某個輸出值,還可以使用整體加載,即用*指定一個對象,所有輸出值都加載在這個對象上面。
import {firstName as name, f, year} from './profile.js'; import * as p from './profile.js'; function setName(element) { element.textContent = name + ' ' + year; // 值等同於p.firstName + ' ' + p.year; }
import命令輸入的變量都是隻讀的,因為它的本質是輸入接口。也就是説,不允許在加載模塊的腳本里面,改寫接口。但是,如果是一個對象,改寫對象的屬性是允許的。並且由於import是靜態執行,所以不能使用表達式和變量,這些只有在運行時才能得到結果的語法結構。
import {a} from './xxx.js'; // 也可以是絕對路徑,.js後綴可以省略 a.foo = 'hello'; // 合法操作 a = {}; // 報錯:a是隻讀的 import { 'f' + 'oo' } from '/my_module.js';// 報錯,語法錯誤(不能用運算符) if (x === 1) { import { foo } from 'module1'; // 報錯,語法錯誤(import不能在{}內) } else { import { foo } from 'module2'; }
注意,import命令具有提升效果,會提升到整個模塊的頭部,首先執行。import可以不導入模塊中的任何內容,只運行模塊中的全局代碼。如果多次執行同一模塊的import語句,那麼只會執行一次其全局代碼,但變量均會正常引入(相當於合併處理)。
foo(); import { foo } from '/my_module.js'; // 不會報錯,因為import的執行早於foo的調用 import '/modules/my-module.js'; // 不引入變量,但執行其中全局代碼 import { a } from '/modules/my-module.js'; // 重複引入不執行全局代碼,但引入變量a
除了用大括號引入變量,import還可以直接自定義引入默認變量:
// export-default.js export default function () { console.log('foo'); } // import-default.js import customName from './export-default.js'; //customName可以是任意名字 customName(); // 'foo'
ECMAScript 6解構賦值
ES6允許按照一定模式,從數組和對象中提取值,對變量進行賦值,這被稱為解構(Destructuring)。本質上,這種寫法屬於“模式匹配”,只要等號兩邊的模式相同,左邊的變量就會被賦予對應的值。
let a = 1; let b = 2; let c = 3; // 等價於 let [a, b, c] = [1, 2, 3]; let [ , third] = ["foo", "bar", "baz"]; third // "bar" let [head, ...tail] = [1, 2, 3, 4]; head // 1 tail // [2, 3, 4] let [x, y, ...z] = ['a']; x // "a" y // 變量解構不成功,賦值為undefined z // 數組解構不成功,賦值為[]
解構賦值允許指定默認值,只有當一個數組成員嚴格等於,默認值才會生效:
let [foo = true] = []; // foo = true let [x, y = 'b'] = ['a']; // x='a', y='b' let [q = 1, w = 'b'] = ['a', undefined]; // q='a', w='b' let [e = 1] = [null]; // e = null
解構不僅可以用於數組,還可以用於對象。對象的解構與數組有一個重要的不同。數組的元素是按次序排列的,變量的取值由它的位置決定;而對象的屬性沒有次序,變量必須與屬性同名,才能取到正確的值。
let { bar, foo } = { foo: "aaa", bar: "bbb" }; foo // "aaa" bar // "bbb" let { abc } = { foo: "aaa", bar: "bbb" }; abc // undefined let { foo: baz } = { foo: 'aaa', bar: 'bbb' }; baz // "aaa" const node = { loc: { start: { line: 1, column: 5 } } }; let { loc, loc: { start }, loc: { start: { line }} } = node; line // 1 loc // Object {start: Object} start // Object {line: 1, column: 5}
解構賦值用途示例:
//交換變量的值 let x = 1; let y = 2; [x, y] = [y, x]; //提取 JSON 數據 let jsonData = { id: 42, status: "OK", data: [867, 5309] }; let { id, status, data: number } = jsonData; console.log(id, status, number); // 42, "OK", [867, 5309] //遍歷 Map 結構 const map = new Map(); map.set('first', 'hello'); map.set('second', 'world'); for (let [key, value] of map) { console.log(key + " is " + value); } // first is hello // second is world
ECMAScript 6字符處理
ES6 又提供了三種新方法用來確定一個字符串是否包含在另一個字符串中,而且這三個方法都支持第二個參數,表示開始搜索的位置:
- includes():返回布爾值,表示是否找到了參數字符串。
- startsWith():返回布爾值,表示參數字符串是否在原字符串的頭部。
- endsWith():返回布爾值,表示參數字符串是否在原字符串的尾部。
repeat方法返回一個新字符串,表示將原字符串重複n次。參數如果是小數,會被取整。參數是負數或者Infinity,會報錯。但是,如果參數是 0 到-1 之間的小數,則等同於 0,這是因為會先進行取整運算。0 到-1 之間的小數,取整以後等於-0,repeat視同為0。
傳統的 JavaScript 語言,輸出模板通常是字符串與變量相互交錯累加而成,寫法相當繁瑣不方便,ES6 引入了模板字符串解決這個問題。模板字符串(template string)是增強版的字符串,用反引號(`)標識。它可以當作普通字符串使用,也可以用來定義多行字符串,或者在字符串中嵌入變量。
//傳統寫法 alert( '<div>There are <b>' + basket.count + '</b> ' + 'items in your basket, ' + '<em>' + basket.onSale + '</em> are on sale!</div>' ); //模板字符串,換行、空格、縮進均會保留 alert(` <div>There are <b>${basket.count}</b> items in your basket, <em>${basket.onSale}</em> are on sale!</div> `);
大括號內部可以放入任意的JavaScript表達式,可以進行運算,還能調用函數,甚至還能嵌套。
const tmpl = addrs => ` <table> ${addrs.map(addr => ` <tr><td>${addr.first}</td></tr> <tr><td>${addr.last}</td></tr> `).join('')} </table> `; const data = [ { first: '<Jane>', last: 'Bond' }, { first: 'Lars', last: '<Croft>' }, ]; console.log(tmpl(data)); // <table> // // <tr><td><Jane></td></tr> // <tr><td>Bond</td></tr> // // <tr><td>Lars</td></tr> // <tr><td><Croft></td></tr> // // </table>
將模板字符串放於函數名後面,該函數將被調用來處理這個模板字符串,被稱為標籤模板功能(tagged template)。標籤模板其實不是模板,而是函數調用的一種特殊形式。它的解析方式非常特別,當字符串中含有變量時,解析後的第一個參數將會是數組,而數組內各項值,由模板中最前面的字符串+每個變量之後的字符串組成。
let a = 5; let b = 10; console.log`Hello ${ a + b } world ${ a * b }`; // 等同於 console.log(['Hello ', ' world ', ''], 15, 50);
ECMAScript 6性能擴展
ECMAScript 6字符串
之前的JavaScript只能識別“\u0000”~“\uFFFF”之間的Unicode碼字符。超出這個範圍的字符,必須用兩個雙字節的形式表示,所以會把“\u20BB7”理解成\u20BB+7,只會顯示一個空格(\u20BB是一個不可打印字符),後面跟着一個7。ES6對這一點做出了改進,只要將碼點放入大括號,就能正確解讀該字符。“\u{20BB7}”解讀為“𠮷”,相當於“\uD842\uDFB7”
[6]
。
ECMAScript 6遍歷器
ES6 為字符串添加了遍歷器接口,使得字符串可以被for...of循環遍歷。除了遍歷字符串,這個遍歷器最大的優點是可以識別大於0xFFFF的碼點,傳統的for循環無法識別這樣的碼點。
for (let codePoint of 'foo') { console.log(codePoint) } // "f" // "o" // "o" let text = String.fromCodePoint(0x20BB7); // 相當於let text = "𠮷"; for (let i = 0; i < text.length; i++) { console.log(text[i]); } // " " // " " 字符串text只有一個字符,但是正常的for循環會認為它包含兩個字符(都不可打印) for (let i of text) { console.log(i); } // "𠮷"
ECMAScript 6正則表達式
在 ES5 中,RegExp構造函數的參數有兩種情況。第一種情況是,參數是字符串,這時第二個參數表示正則表達式的修飾符(flag)。第二種情況是,參數是一個正則表示式,這時會返回一個原有正則表達式的拷貝。但是,ES5不允許此時使用第二個參數添加修飾符,否則會報錯。ES6改變了這種行為。如果RegExp構造函數第一個參數是一個正則對象,那麼可以使用第二個參數指定修飾符。而且,返回的正則表達式會忽略原有的正則表達式的修飾符,只使用新指定的修飾符。
ES6對正則表達式添加了u修飾符,含義為“Unicode 模式”,用來正確處理大於\uFFFF的Unicode字符。也就是説,會正確處理四個字節的UTF-16編碼。利用這一點,可以寫出一個正確返回字符串長度的函數:
function codePointLength(text) { var result = text.match(/[\s\S]/gu); return result ? result.length : 0; } var s = '𠮷𠮷'; s.length // 4 codePointLength(s) // 2
ECMAScript 6兼容問題
不同瀏覽器的不同版本對ES6的支持度不同,而Babel是一個廣泛使用的ES6轉碼器,可以將ES6代碼轉為ES5代碼,從而在現有環境執行。
另外,使用Google公司的Traceur轉碼器,也可以將ES6代碼轉為ES5代碼,需要提前引入其js庫,包含traceur.js、BrowserSystem.js、bootstrap.js。第一個是加載Traceur的庫文件,第二個和第三個是將這個庫文件用於瀏覽器環境。然後就可以加載ES6的用户腳本了,如:
//引用外部ES6的js <script type="module"> import './Greeter.js'; </script> //直接寫ES6的JS <script type="module"> class Calc { constructor() { console.log('Calc constructor'); } add(a, b) { return a + b; } } var c = new Calc(); console.log(c.add(4,5)); //正常情況下,會在控制枱打印出9。 </script>
自定義的script標籤的type屬性的值是module,而不是text/javascript。這是Traceur編譯器識別ES6代碼的標誌,編譯器會自動將所有type=module的代碼編譯為ES5,然後再交給瀏覽器執行。
如果想對Traceur的行為有精確控制,可以採用下面參數配置的寫法:
<script> // 創建系統對象 window.System = new traceur.runtime.BrowserTraceurLoader(); // 設置參數 var metadata = { traceurOptions: { experimental: true, properTailCalls: true, symbols: true, arrayComprehension: true, asyncFunctions: true, asyncGenerators: exponentiation, forOn: true, generatorComprehension: true } }; // 加載模塊 System.import('./myModule.js', {metadata: metadata}).catch(function(ex) { console.error('Import failed', ex.stack || ex); }); </script>
上面代碼中,首先生成Traceur的全局對象window.System,然後System.import方法可以用來加載ES6。加載的時候,需要傳入一個配置對象metadata,該對象的traceurOptions屬性可以配置支持ES6功能。如果設為experimental:true,就表示除了ES6以外,還支持一些實驗性的新功能
[1]
。
- 參考資料
-
- 1. ECMAScript 6 簡介 .ruanyifeng[引用日期2018-05-24]
- 2. let 和 const 命令 .ruanyifeng[引用日期2018-05-28]
- 3. class的基本語法 .ruanyifeng[引用日期2018-05-29]
- 4. Module 的語法 .ruanyifeng[引用日期2018-06-04]
- 5. 變量的解構賦值 .ruanyifeng[引用日期2018-06-05]
- 6. 字符串的擴展 .ruanyifeng[引用日期2018-08-03]
- 7. 正則的擴展 .ruanyifeng[引用日期2018-08-07]