1.簡介
ECMAScript 6.0(以下簡稱ES6)是JavaScript語言的下一代標准,已經在2015年6月正式發布了。它的目標,是使得JavaScript語言可以用來編寫復雜的大型應用程序,成為企業級開發語言。
JavaScript的創造者Netscape公司,之後將JavaScript提交給國際標准化組織ECMA,希望這種語言能夠成為國際標准,ECMAScript和JavaScript的關系是,前者是後者的規格,後者是前者的一種實現,
之所以不叫JavaScript,有兩個原因。
一是商標,Java是Sun公司的商標,根據授權協議,只有Netscape公司可以合法地使用JavaScript這個名字,且JavaScript本身也已經被Netscape公司注冊為商標。
二是想體現這門語言的制定者是ECMA,不是Netscape,這樣有利於保證這門語言的開放性和中立性。
在線將ES6代碼轉為ES5代碼(https://babeljs.io/repl/)
Babel是一個廣泛使用的ES6轉碼器,可以將ES6代碼轉為ES5代碼,從而在現有環境執行。這意味著,你可以用ES6的方式編寫程序,又不用擔心現有環境是否支持。
2.變量聲明
ES5只有2種聲明變量的方法:var、function。
ES6共有6種聲明變量的方法:var、function、let、const、import(require)、class。
2.1 var命令
var a = 10;
var b = 20;
var c = 30;
var a = 10,b = 20,c = 30;
var arr = [1,2,3,4,5];
var a = arr[0];
var b = arr[1];
var c = arr[3];
var obj = {
name: 'gary',
age: 20
}
var a = obj.name;
var b = obj.age;
沒有用var關鍵字,使用直接賦值方式聲明的是全局變量,例如:
a = 10;
全局對象是最頂層的對象,在浏覽器環境指的是window對象,在Node.js指的是global對象。ES5之中,全局對象的屬性與全局變量是等價的。
window.a = 1;
a // 1
a = 2;
window.a // 2
2.2 function命令
var 方式定義的函數,不能先調用函數,後聲明,只能先聲明函數,然後調用。
function方式定義函數可以先調用,後聲明。
aaa();//這樣調用就會出錯
var aaa = function(){
alert("aaa");
}
aaa();//這樣就不會出錯
//先調用後聲明
bbb();
function bbb(){
alert("bbb");
}
2.3 let命令
塊級有效
ES5只有全局作用域和函數作用域,沒有塊級作用域,在ES6中,let實際上為JavaScript新增了塊級作用域。
用來聲明變量,用法類似於var,但是所聲明的變量,只在let命令所在的代碼塊內有效。
function f1() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}
for循環的計數器,就很合適使用let命令。例如:
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[5](); //10
a[6](); // 10
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[5](); //5
a[6](); // 6
變量提升
let不像var那樣會發生“變量提升”現象??????
console.log(foo); // 輸出undefined
console.log(bar); // 報錯ReferenceError
var foo = 2;
let bar = 2;
實測結果兩個都是undefined,應該是網上資料錯誤,可以通過Babel來了解底層原理
暫時性死區
只要塊級作用域內存在let命令,它所聲明的變量就“綁定”(binding)這個區域,不再受外部的影響。
var tmp='dev';
if (true) {
console.log(tmp);
let tmp;
console.log(tmp);
tmp = 123;
console.log(tmp);
}
在let命令聲明變量tmp之前,都屬於變量tmp的“死區”。
不允許重復聲明
let不允許在相同作用域內,重復聲明同一個變量。
// 報錯
function test() {
let a = 10;
var a = 1;
}
// 報錯
function test() {
let a = 10;
let a = 1;
}
因此,不能在函數內部重新聲明參數。
function func(arg) {
let arg; // 報錯
}
function func(arg) {
{
let arg; // 不報錯
}
}
2.4 const命令
const聲明一個只讀的常量。一旦聲明,就必須立即初始化,不能留到以後賦值。也不能改變。
const PI = 3.1415;
console.log(PI); // 3.1415
PI = 3;// TypeError: Assignment to constant variable.
const的作用域與let命令相同:只在聲明所在的塊級作用域內有效,聲明的常量,也與let一樣不可重復聲明。
const命令只是保證變量名指向的地址不變,並不保證該地址的數據不變,所以將一個對象聲明為常量必須非常小心。
const foo = {};
foo.prop = 123;
console.log(foo.prop);// 123
foo = {}; // TypeError: "foo" is read-only
var命令和function命令聲明的全局變量,依舊是全局對象的屬性;
let命令、const命令、class命令聲明的全局變量,不屬於全局對象的屬性。也就是說,從ES6開始,全局變量將逐步與全局對象的屬性脫鉤。
var a = 1;
// 如果在Node的REPL環境,可以寫成global.a
// 或者采用通用方法,寫成this.a
window.a // 1
let b = 1;
window.b // undefined
2.5 import命令
模塊的功能主要由 export 和 import 組成.每一個模塊都有自己單獨的作用域,模塊之間的相互調用關系是通過 export 來規定模塊對外暴露的接口,通過import來引用其它模塊提供的接口。同時還為模塊創造了命名空間,防止函數的命名沖突。
ES6將一個文件視為一個模塊,通過export 向外輸出了一個變量。一個模塊也可以同時往外面輸出多個變量。
//test.js
var name = 'Rainbow';
var age = '24';
export {name, age};
定義好模塊的輸出以後就可以在另外一個模塊通過import引用。
import {name, age} from './test.js'
整體輸入,module指令
//test.js
export function getName() {
return name;
}
export function getAge(){
return age;
}
通過 import * as 就完成了模塊整體的導入。
import * as test form './test.js';
通過指令 module 也可以達到整體的輸入。
module test from 'test.js';
test.getName();
export default
不用關系模塊輸出了什麼,通過 export default 指令就能加載到默認模塊,不需要通過 花括號來指定輸出的模塊,一個模塊只能使用 export default 一次
// default 導出
export default function getAge() {}
// 或者寫成
function getAge() {}
export default getAge;
// 導入的時候不需要花括號
import test from './test.js';
一條import 語句可以同時導入默認方法和其它變量.
import defaultMethod, { otherMethod } from 'xxx.js';
2.6 class命令
如果你用過純面向對象語言,那麼你會對class的語法非常熟悉。
class People {
constructor(name) { //構造函數
this.name = name;
}
sayName() {
console.log(this.name);
}
}
var p = new People("Tom");
p.sayName();
上面定義了一個People類,他有一個屬性 name 和一個方法 sayName(),還有一個構造函數;
就像函數有函數聲明和函數表達式兩種定義方式,類也可以通過類表達式來定義:
let People = class {
constructor(name) { //構造函數
this.name = name;
}
sayName() {
console.log(this.name);
}
}
你可能以為類聲明和類表達式的區別在於變量提升的不同。但是事實是無論是類聲明還是類表達式的方式來定義,都不會有變量提升。
通過關鍵字 extends 來繼承一個類,並且,可以通過 super 關鍵字來引用父類。
class Student extends People {
constructor(name, grade) { //構造函數
super(name); //通過 super 調用父類的構造函數的。
this.grade = grade;
}
sayGrade() {
console.log(this.grade);
}
}
上面的例子中我們定義了一個 Student ,他是 People 的子類。
下面我們給 name 屬性定義 getter 和 setter
class People {
constructor(name) { //構造函數
this.name = name;
}
get name() {
return this._name.toUpperCase();
}
set name(name) {
this._name = name;
}
sayName() {
console.log(this.name);
}
}
var p = new People("tom");
console.log(p.name); //TOM
console.log(p._name); //tom
p.sayName(); //TOM
仔細看上面的例子,搞清楚最後三行分別會輸出什麼,就明白getter 和 setter該怎麼用了。
主要是要區分 this._name 和 this.name 的區別。因為我們定義了 name 的讀寫器,而沒有定義 _name 的讀寫器,所以訪問這兩個屬性的結果是不同的。
但是要注意一點,不要這樣寫:
set name(name) {
this.name = name;
}
因為給 this.name 賦值的時候會調用 set name ,這樣會導致無限遞歸直到棧溢出。
通過 static 關鍵字定義靜態方法:
class People {
constructor(name) { //構造函數
this.name = name;
}
sayName() {
console.log(this.name);
}
static formatName(name) {
return name[0].toUpperCase() + name.sustr(1).toLowerCase();
}
}
console.log(People.formatName("tom"));
3.解構賦值
ES6允許按照一定模式,從數組和對象中提取值,對變量進行賦值,這被稱為解構(Destructuring)。
“模式匹配”,只要等號兩邊的模式相同,左邊的變量就會被賦予對應的值。下面是一些使用嵌套數組進行解構的例子。
如果解構不成功,變量的值就等於undefined。
let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3
let [x, , y] = [1, 2, 3];//不完全解構
x // 1
y // 3
let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]
let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []
解構賦值允許指定默認值。
var [foo = true] = [];
foo // true
[x, y = 'b'] = ['a']; // x='a', y='b'
[x, y = 'b'] = ['a', undefined]; // x='a', y='b'
注意,ES6內部使用嚴格相等運算符(===),判斷一個位置是否有值。所以,如果一個數組成員不嚴格等於undefined,默認值是不會生效的。
默認值可以引用解構賦值的其他變量,但該變量必須已經聲明。
let [x = 1, y = x] = []; // x=1; y=1
let [x = 1, y = x] = [2]; // x=2; y=2
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = []; // ReferenceError
解構賦值不僅適用於var命令,也適用於let和const命令。
var [v1, v2, ..., vN ] = array;
let [v1, v2, ..., vN ] = array;
const [v1, v2, ..., vN ] = array;
對於Set結構,也可以使用數組的解構賦值。
let [x, y, z] = new Set(["a", "b", "c"]);
x // "a"
對象的解構賦值
解構不僅可以用於數組,還可以用於對象。
var { foo, bar } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"
對象的解構與數組有一個重要的不同。數組的元素是按次序排列的,變量的取值由它的位置決定;而對象的屬性沒有次序,變量必須與屬性同名,才能取到正確的值。
var { bar, foo } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"
var { baz } = { foo: "aaa", bar: "bbb" };
baz // undefined
如果變量名與屬性名不一致,必須寫成下面這樣。
var { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world'
這實際上說明,對象的解構賦值是下面形式的簡寫。
var { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };
也就是說,對象的解構賦值的內部機制,是先找到同名屬性,然後再賦給對應的變量。真正被賦值的是後者,而不是前者。
var { foo: baz } = { foo: "aaa", bar: "bbb" };
baz // "aaa"
foo // error: foo is not defined
變量的聲明和賦值是一體的。對於let和const來說,變量不能重新聲明,所以一旦賦值的變量以前聲明過,就會報錯。
let foo;
let {foo} = {foo: 1}; // SyntaxError: Duplicate declaration "foo"
let baz;
let {bar: baz} = {bar: 1}; // SyntaxError: Duplicate declaration "baz"
上面代碼中,解構賦值的變量都會重新聲明,所以報錯了。不過,因為var命令允許重新聲明,所以這個錯誤只會在使用let和const命令時出現。如果沒有第二個let命令,上面的代碼就不會報錯。
let foo;
({foo} = {foo: 1}); // 成功
let baz;
({bar: baz} = {bar: 1}); // 成功
上面代碼中,let命令下面一行的圓括號是必須的,否則會報錯。因為解析器會將起首的大括號,理解成一個代碼塊,而不是賦值語句。
和數組一樣,解構也可以用於嵌套結構的對象。
var obj = {
p: [
'Hello',
{ y: 'World' }
]
};
var { p: [x, { y }] } = obj;
x // "Hello"
y // "World"
注意,這時p是模式,不是變量,因此不會被賦值。
var node = {
loc: {
start: {
line: 1,
column: 5
}
}
};
var { loc: { start: { line }} } = node;
line // 1
loc // error: loc is undefined
start // error: start is undefined
默認值生效的條件是,對象的屬性值嚴格等於undefined。
如果要將一個已經聲明的變量用於解構賦值,必須非常小心。
// 錯誤的寫法
var x;
{x} = {x: 1};
// SyntaxError: syntax error
上面代碼的寫法會報錯,因為JavaScript引擎會將{x}理解成一個代碼塊,從而發生語法錯誤。只有不將大括號寫在行首,避免JavaScript將其解釋為代碼塊,才能解決這個問題。
// 正確的寫法
({x} = {x: 1});
由於數組本質是特殊的對象,因此可以對數組進行對象屬性的解構。
var arr = [1, 2, 3];
var {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 3
字符串的解構賦值
字符串也可以解構賦值。這是因為此時,字符串被轉換成了一個類似數組的對象。
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
類似數組的對象都有一個length屬性,因此還可以對這個屬性解構賦值。
let {length : len} = 'hello';
len // 5
數值和布爾值的解構賦值
解構賦值時,如果等號右邊是數值和布爾值,則會先轉為對象。
let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true
上面代碼中,數值和布爾值的包裝對象都有toString屬性,因此變量s都能取到值。
解構賦值的規則是,只要等號右邊的值不是對象,就先將其轉為對象。由於undefined和null無法轉為對象,所以對它們進行解構賦值,都會報錯。
let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError
函數參數的解構也可以使用默認值。
function move({x = 0, y = 0} = {}) {
return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]
函數move的參數是一個對象,通過對這個對象進行解構,得到變量x和y的值。如果解構失敗,x和y等於默認值。
function move({x, y} = { x: 0, y: 0 }) {
return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, undefined]
move({}); // [undefined, undefined]
move(); // [0, 0]
函數move的參數指定默認值,而不是為變量x和y指定默認值,所以會得到與前一種寫法不同的結果。
事實上,只要某種數據結構具有Iterator接口,都可以采用數組形式的解構賦值。
function* fibs() {
var a = 0;
var b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
var [first, second, third, fourth, fifth, sixth] = fibs();
sixth // 5
解構賦值用途:
(1)交換變量的值
[x, y] = [y, x];
上面代碼交換變量x和y的值,這樣的寫法不僅簡潔,而且易讀,語義非常清晰。
(2)從函數返回多個值
函數只能返回一個值,如果要返回多個值,只能將它們放在數組或對象裡返回。有了解構賦值,取出這些值就非常方便。
// 返回一個數組
function example() {
return [1, 2, 3];
}
var [a, b, c] = example();
// 返回一個對象
function example() {
return {
foo: 1,
bar: 2
};
}
var { foo, bar } = example();
(3)函數參數的定義
解構賦值可以方便地將一組參數與變量名對應起來。
// 參數是一組有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);
// 參數是一組無次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});
(4)提取JSON數據
解構賦值對提取JSON對象中的數據,尤其有用。
var jsonData = {
id: 42,
status: "OK",
data: [867, 5309]
};
let { id, status, data: number } = jsonData;
console.log(id, status, number);
// 42, "OK", [867, 5309]
上面代碼可以快速提取JSON數據的值。
(5)函數參數的默認值
jQuery.ajax = function (url, {
async = true,
beforeSend = function () {},
cache = true,
complete = function () {},
crossDomain = false,
global = true,
// ... more config
}) {
// ... do stuff
};
指定參數的默認值,就避免了在函數體內部再寫var foo = config.foo || 'default foo';這樣的語句。
(6)遍歷Map結構
任何部署了Iterator接口的對象,都可以用for...of循環遍歷。Map結構原生支持Iterator接口,配合變量的解構賦值,獲取鍵名和鍵值就非常方便。
var 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
如果只想獲取鍵名,或者只想獲取鍵值,可以寫成下面這樣。
// 獲取鍵名
for (let [key] of map) {
// ...
}
// 獲取鍵值
for (let [,value] of map) {
// ...
}
(7)輸入模塊的指定方法
加載模塊時,往往需要指定輸入那些方法。解構賦值使得輸入語句非常清晰。
const { SourceMapConsumer, SourceNode } = require("source-map");
4.編程風格
4.1 采用嚴格模式:'use strict';
主要有以下限制:
變量必須聲明後再使用
函數的參數不能有同名屬性,否則報錯
不能使用with語句
不能對只讀屬性賦值,否則報錯
不能使用前綴0表示八進制數,否則報錯
不能刪除不可刪除的屬性,否則報錯
不能刪除變量delete prop,會報錯,只能刪除屬性delete global[prop]
eval不會在它的外層作用域引入變量
eval和arguments不能被重新賦值
arguments不會自動反映函數參數的變化
不能使用arguments.callee
不能使用arguments.caller
禁止this指向全局對象
不能使用fn.caller和fn.arguments獲取函數調用的堆棧
增加了保留字(比如protected、static和interface)
4.2 let取代var
在塊級作用域下,let完全可以取代var,因為兩者語義相同,而且let沒有副作用。
4.3 全局常量和線程安全
在let和const之間,建議優先使用const,尤其是在全局環境,不應該設置變量,只應設置常量。所有的函數都應該設置為常量。這符合函數式編程思想,有利於將來的分布式運算。
const聲明常量還有兩個好處,一是閱讀代碼的人立刻會意識到不應該修改這個值,二是防止了無意間修改變量值所導致的錯誤。
長遠來看,JavaScript可能會有多線程的實現(比如Intel的River Trail那一類的項目),這時let表示的變量,只應出現在單線程運行的代碼中,不能是多線程共享的,這樣有利於保證線程安全。
4.4 字符串
靜態字符串一律使用單引號或反引號,不使用雙引號。動態字符串使用反引號。
4.5 解構賦值
使用數組成員對變量賦值時,優先使用解構賦值。
函數的參數如果是對象的成員,優先使用解構賦值。
如果函數返回多個值,優先使用對象的解構賦值,而不是數組的解構賦值。這樣便於以後添加返回值,以及更改返回值的順序。
4.6 對象
單行定義的對象,最後一個成員不以逗號結尾。多行定義的對象,最後一個成員以逗號結尾。
對象盡量靜態化,一旦定義,就不得隨意添加新的屬性。如果添加屬性不可避免,要使用Object.assign方法。
如果對象的屬性名是動態的,可以在創造對象的時候,使用屬性表達式定義。
另外,對象的屬性和方法,盡量采用簡潔表達法,這樣易於描述和書寫。
4.7 數組
使用擴展運算符(...)拷貝數組。
使用Array.from方法,將類似數組的對象轉為數組。
4.8 函數
立即執行函數可以寫成箭頭函數的形式。
那些需要使用函數表達式的場合,盡量用箭頭函數代替。因為這樣更簡潔,而且綁定了this。
簡單的、單行的、不會復用的函數,建議采用箭頭函數。如果函數體較為復雜,行數較多,還是應該采用傳統的函數寫法。
所有配置項都應該集中在一個對象,放在最後一個參數,布爾值不可以直接作為參數。
不要在函數體內使用arguments變量,使用rest運算符(...)代替。因為rest運算符顯式表明你想要獲取參數,而且arguments是一個類似數組的對象,而rest運算符可以提供一個真正的數組。
4.9 Map結構
注意區分Object和Map,只有模擬現實世界的實體對象時,才使用Object。如果只是需要key: value的數據結構,使用Map結構。因為Map有內建的遍歷機制。
4.10 Class
總是用Class,取代需要prototype的操作。因為Class的寫法更簡潔,更易於理解。
使用extends實現繼承,因為這樣更簡單,不會有破壞instanceof運算的危險。
4.11 模塊
首先,Module語法是JavaScript模塊的標准寫法,堅持使用這種寫法。使用import取代require。
如果模塊只有一個輸出值,就使用export default,如果模塊有多個輸出值,就不使用export default,不要export default與普通的export同時使用。
不要在模塊輸入中使用通配符。因為這樣可以確保你的模塊之中,有一個默認輸出(export default)。
如果模塊默認輸出一個對象,對象名的首字母應該大寫。