本章節說明 ES6 的核心功能。這些功能很容易採用;其餘的功能主要對函式庫作者有興趣。我透過對應的 ES5 程式碼來解釋每個功能。
var
到 const
/let
for
到 forEach()
到 for-of
arguments
到 rest 參數
apply()
到展開運算子(...
)
Math.max()
Array.prototype.push()
concat()
到展開運算子(...
)
Error
的子類別
Array.prototype.indexOf
到 Array.prototype.findIndex
Array.prototype.slice()
到 Array.from()
或展開運算子apply()
到 Array.prototype.fill()
var
到 const
/let
在 ES5 中,您透過 var
來宣告變數。此類變數是函式作用域,其作用域是內層包覆的函式。var
的行為偶爾會令人困惑。以下是一個範例
var
x
=
3
;
function
func
(
randomize
)
{
if
(
randomize
)
{
var
x
=
Math
.
random
();
// (A) scope: whole function
return
x
;
}
return
x
;
// accesses the x from line A
}
func
(
false
);
// undefined
func()
會傳回 undefined
,這可能會令人驚訝。如果您重新撰寫程式碼,使其更接近實際發生的情況,您就能看出原因
var
x
=
3
;
function
func
(
randomize
)
{
var
x
;
if
(
randomize
)
{
x
=
Math
.
random
();
return
x
;
}
return
x
;
}
func
(
false
);
// undefined
在 ES6 中,您還可以透過 let
和 const
來宣告變數。此類變數是區塊作用域,其作用域是內層包覆的區塊。let
大致上是 var
的區塊作用域版本。const
的作用類似於 let
,但建立的變數值無法變更。
let
和 const
的行為更嚴謹,而且會擲出更多例外狀況(例如,在宣告之前於其作用域內存取其變數時)。區塊作用域有助於讓程式碼片段的效果更為區域化(請參閱下一節的示範)。而且,它比函式作用域更主流,這讓在 JavaScript 和其他程式語言之間的轉換更為容易。
如果您在初始版本中將 var
替換為 let
,您會得到不同的行為
let
x
=
3
;
function
func
(
randomize
)
{
if
(
randomize
)
{
let
x
=
Math
.
random
();
return
x
;
}
return
x
;
}
func
(
false
);
// 3
這表示您無法在現有程式碼中盲目地將 var
替換為 let
或 const
;您必須在重構時小心謹慎。
我的建議是
const
。您可以將它用於所有值永不變更的變數。let
,用於值會變更的變數。var
。更多資訊:章節「變數和作用域」。
在 ES5 中,如果您想要將變數 tmp
的作用域限制在區塊中,您必須使用稱為 IIFE(立即呼叫函式運算式)的模式
(
function
()
{
// open IIFE
var
tmp
=
···
;
···
}());
// close IIFE
console
.
log
(
tmp
);
// ReferenceError
在 ECMAScript 6 中,你可以簡單地使用一個區塊和一個 let
宣告(或是一個 const
宣告)
{
// open block
let
tmp
=
···
;
···
}
// close block
console
.
log
(
tmp
);
// ReferenceError
更多資訊:章節「避免在 ES6 中使用 IIFE」。
有了 ES6,JavaScript 終於有了字串內插和多行字串的文字。
在 ES5 中,你可以透過串接這些值和字串片段來將值放入字串中
function
printCoord
(
x
,
y
)
{
console
.
log
(
'('
+
x
+
', '
+
y
+
')'
);
}
在 ES6 中,你可以透過範本字串使用字串內插
function
printCoord
(
x
,
y
)
{
console
.
log
(
`(
${
x
}
,
${
y
}
)`
);
}
範本字串也有助於表示多行字串。
例如,這是你在 ES5 中必須執行的動作來表示一個
var
HTML5_SKELETON
=
'<!doctype html>\n'
+
'<html>\n'
+
'<head>\n'
+
' <meta charset="UTF-8">\n'
+
' <title></title>\n'
+
'</head>\n'
+
'<body>\n'
+
'</body>\n'
+
'</html>\n'
;
如果你透過反斜線跳脫換行符號,事情看起來會好一點(但你仍然必須明確加入換行符號)
var
HTML5_SKELETON
=
'\
<!doctype html>\n\
<html>\n\
<head>\n\
<meta charset="UTF-8">\n\
<title></title>\n\
</head>\n\
<body>\n\
</body>\n\
</html>'
;
ES6 範本字串可以跨越多行
const
HTML5_SKELETON
=
`
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
</body>
</html>`
;
(範例在包含多少空白方面有所不同,但在這種情況下這並不重要。)
更多資訊:章節「範本字串和標記範本」。
在目前的 ES5 程式碼中,每當你使用函式運算式時,都必須小心處理 this
。在以下範例中,我在 A 行建立了輔助變數 _this
,以便可以在 B 行存取 UiComponent
的 this
。
function
UiComponent
()
{
var
_this
=
this
;
// (A)
var
button
=
document
.
getElementById
(
'myButton'
);
button
.
addEventListener
(
'click'
,
function
()
{
console
.
log
(
'CLICK'
);
_this
.
handleClick
();
// (B)
});
}
UiComponent
.
prototype
.
handleClick
=
function
()
{
···
};
在 ES6 中,你可以使用箭頭函式,它不會遮蔽 this
(A 行)
function
UiComponent
()
{
var
button
=
document
.
getElementById
(
'myButton'
);
button
.
addEventListener
(
'click'
,
()
=>
{
console
.
log
(
'CLICK'
);
this
.
handleClick
();
// (A)
});
}
(在 ES6 中,你也可以選擇使用類別,而不是建構函式。這會在稍後探討。)
箭頭函式特別適用於僅傳回運算式結果的簡短回呼。
在 ES5 中,此類回呼相對冗長
var
arr
=
[
1
,
2
,
3
];
var
squares
=
arr
.
map
(
function
(
x
)
{
return
x
*
x
});
在 ES6 中,箭頭函式簡潔多了
const
arr
=
[
1
,
2
,
3
];
const
squares
=
arr
.
map
(
x
=>
x
*
x
);
在定義參數時,如果參數只是一個識別碼,你甚至可以省略括號。因此:(x) => x * x
和 x => x * x
都是允許的。
更多資訊:章節「箭頭函式」。
有些函式或方法會透過陣列或物件回傳多重值。在 ES5 中,如果你想要存取這些值,你總是需要建立中間變數。在 ES6 中,你可以透過解構來避免使用中間變數。
exec()
會透過類陣列物件回傳擷取到的群組。在 ES5 中,你需要一個中間變數(在以下範例中為 matchObj
),即使你只對群組有興趣。
var
matchObj
=
/^(\d\d\d\d)-(\d\d)-(\d\d)$/
.
exec
(
'2999-12-31'
);
var
year
=
matchObj
[
1
];
var
month
=
matchObj
[
2
];
var
day
=
matchObj
[
3
];
在 ES6 中,解構讓這段程式碼更為簡潔
const
[,
year
,
month
,
day
]
=
/^(\d\d\d\d)-(\d\d)-(\d\d)$/
.
exec
(
'2999-12-31'
);
陣列樣式的開頭處的空槽會跳過索引為零的陣列元素。
方法 Object.getOwnPropertyDescriptor()
會回傳一個屬性描述符,一個在屬性中儲存多重值的物件。
在 ES5 中,即使你只對物件的屬性感興趣,你仍然需要一個中間變數(在以下範例中為 propDesc
)
var
obj
=
{
foo
:
123
};
var
propDesc
=
Object
.
getOwnPropertyDescriptor
(
obj
,
'foo'
);
var
writable
=
propDesc
.
writable
;
var
configurable
=
propDesc
.
configurable
;
console
.
log
(
writable
,
configurable
);
// true true
在 ES6 中,你可以使用解構
const
obj
=
{
foo
:
123
};
const
{
writable
,
configurable
}
=
Object
.
getOwnPropertyDescriptor
(
obj
,
'foo'
);
console
.
log
(
writable
,
configurable
);
// true true
{writable, configurable}
是
{
writable
:
writable
,
configurable
:
configurable
}
更多資訊:章節「解構」。
for
到 forEach()
到 for-of
在 ES5 之前,你會以如下方式迭代陣列
var
arr
=
[
'a'
,
'b'
,
'c'
];
for
(
var
i
=
0
;
i
<
arr
.
length
;
i
++
)
{
var
elem
=
arr
[
i
];
console
.
log
(
elem
);
}
在 ES5 中,你可以選擇使用陣列方法 forEach()
arr
.
forEach
(
function
(
elem
)
{
console
.
log
(
elem
);
});
for
迴圈的優點是你可以中斷它,forEach()
的優點是簡潔。
在 ES6 中,for-of
迴圈結合了兩者的優點
const
arr
=
[
'a'
,
'b'
,
'c'
];
for
(
const
elem
of
arr
)
{
console
.
log
(
elem
);
}
如果你想要每個陣列元素的索引和值,for-of
也能滿足你的需求,透過新的陣列方法 entries()
和解構
for
(
const
[
index
,
elem
]
of
arr
.
entries
())
{
console
.
log
(
index
+
'. '
+
elem
);
}
更多資訊:章節「for-of
迴圈」。
在 ES5 中,您可以這樣為參數指定預設值
function
foo
(
x
,
y
)
{
x
=
x
||
0
;
y
=
y
||
0
;
···
}
ES6 有更棒的語法
function
foo
(
x
=
0
,
y
=
0
)
{
···
}
ES6 的額外好處是,參數預設值僅會由 undefined
觸發,而它會由前一個 ES5 程式碼中的任何假值觸發。
更多資訊:區段「參數預設值」。
在 JavaScript 中,命名參數的常見方式是透過物件文字(所謂的選項物件模式)
selectEntries
({
start
:
0
,
end
:
-
1
});
這種方法的兩個優點是:程式碼變得更能自我描述,而且更容易省略任意參數。
在 ES5 中,您可以如下實作 selectEntries()
function
selectEntries
(
options
)
{
var
start
=
options
.
start
||
0
;
var
end
=
options
.
end
||
-
1
;
var
step
=
options
.
step
||
1
;
···
}
在 ES6 中,您可以在參數定義中使用解構,而且程式碼變得更簡單
function
selectEntries
({
start
=
0
,
end
=-
1
,
step
=
1
})
{
···
}
若要在 ES5 中讓參數 options
變成選配,您會將 A 行新增至程式碼
function
selectEntries
(
options
)
{
options
=
options
||
{};
// (A)
var
start
=
options
.
start
||
0
;
var
end
=
options
.
end
||
-
1
;
var
step
=
options
.
step
||
1
;
···
}
在 ES6 中,您可以指定 {}
作為參數預設值
function
selectEntries
({
start
=
0
,
end
=-
1
,
step
=
1
}
=
{})
{
···
}
更多資訊:區段「模擬命名參數」。
arguments
到 rest 參數 在 ES5 中,如果您希望函式(或方法)接受任意數量的引數,您必須使用特殊變數 arguments
function
logAllArguments
()
{
for
(
var
i
=
0
;
i
<
arguments
.
length
;
i
++
)
{
console
.
log
(
arguments
[
i
]);
}
}
在 ES6 中,您可以透過 ...
營運子宣告 rest 參數(在下面的範例中為 args
)
function
logAllArguments
(...
args
)
{
for
(
const
arg
of
args
)
{
console
.
log
(
arg
);
}
}
如果您只對尾隨參數有興趣,rest 參數會更好用
function
format
(
pattern
,
...
args
)
{
···
}
在 ES5 中處理這個案例很笨拙
function
format
(
pattern
)
{
var
args
=
[].
slice
.
call
(
arguments
,
1
);
···
}
Rest 參數讓程式碼更易於閱讀:您只要看其參數定義,就能知道函式有變數個參數。
更多資訊:區段「Rest 參數」。
apply()
到散佈運算子 (...
) 在 ES5 中,您透過 apply()
將陣列轉換成參數。ES6 有散佈運算子可供此目的使用。
Math.max()
Math.max()
傳回其引數中數值最大的值。它適用於任意數量的引數,但不適用於陣列。
ES5 – apply()
> Math.max.apply(Math, [-1, 5, 11, 3])
11
ES6 – 展開運算子
> Math.max(...[-1, 5, 11, 3])
11
Array.prototype.push()
Array.prototype.push()
會將其所有參數作為元素附加到其接收者。沒有方法可以破壞性地將一個陣列附加到另一個陣列。
ES5 – apply()
var
arr1
=
[
'a'
,
'b'
];
var
arr2
=
[
'c'
,
'd'
];
arr1
.
push
.
apply
(
arr1
,
arr2
);
// arr1 is now ['a', 'b', 'c', 'd']
ES6 – 展開運算子
const
arr1
=
[
'a'
,
'b'
];
const
arr2
=
[
'c'
,
'd'
];
arr1
.
push
(...
arr2
);
// arr1 is now ['a', 'b', 'c', 'd']
更多資訊:章節「展開運算子 (...
)」。
concat()
到展開運算子 (...
) 展開運算子也可以(非破壞性地)將其運算元的內容轉換為陣列元素。這表示它成為陣列方法 concat()
的替代方案。
ES5 – concat()
var
arr1
=
[
'a'
,
'b'
];
var
arr2
=
[
'c'
];
var
arr3
=
[
'd'
,
'e'
];
console
.
log
(
arr1
.
concat
(
arr2
,
arr3
));
// [ 'a', 'b', 'c', 'd', 'e' ]
ES6 – 展開運算子
const
arr1
=
[
'a'
,
'b'
];
const
arr2
=
[
'c'
];
const
arr3
=
[
'd'
,
'e'
];
console
.
log
([...
arr1
,
...
arr2
,
...
arr3
]);
// [ 'a', 'b', 'c', 'd', 'e' ]
更多資訊:章節「展開運算子 (...
)」。
在 JavaScript 中,方法是其值為函式的屬性。
在 ES5 物件文字中,方法的建立方式與其他屬性相同。屬性值透過函式表達式提供。
var
obj
=
{
foo
:
function
()
{
···
},
bar
:
function
()
{
this
.
foo
();
},
// trailing comma is legal in ES5
}
ES6 有方法定義,這是建立方法的特殊語法
const
obj
=
{
foo
()
{
···
},
bar
()
{
this
.
foo
();
},
}
更多資訊:章節「方法定義」。
ES6 類別大多只是建構函式的更方便語法。
在 ES5 中,您直接實作建構函式
function
Person
(
name
)
{
this
.
name
=
name
;
}
Person
.
prototype
.
describe
=
function
()
{
return
'Person called '
+
this
.
name
;
};
在 ES6 中,類別提供建構函式稍更方便的語法
class
Person
{
constructor
(
name
)
{
this
.
name
=
name
;
}
describe
()
{
return
'Person called '
+
this
.
name
;
}
}
請注意方法定義的簡潔語法 – 不需要關鍵字 function
。另請注意,類別各部分之間沒有逗號。
在 ES5 中,子類別化很複雜,特別是參考超建構函式和超屬性。這是建立 Person
的子建構函式 Employee
的標準方式
function
Employee
(
name
,
title
)
{
Person
.
call
(
this
,
name
);
// super(name)
this
.
title
=
title
;
}
Employee
.
prototype
=
Object
.
create
(
Person
.
prototype
);
Employee
.
prototype
.
constructor
=
Employee
;
Employee
.
prototype
.
describe
=
function
()
{
return
Person
.
prototype
.
describe
.
call
(
this
)
// super.describe()
+
' ('
+
this
.
title
+
')'
;
};
ES6 透過 extends
子句內建對子類別化的支援
class
Employee
extends
Person
{
constructor
(
name
,
title
)
{
super
(
name
);
this
.
title
=
title
;
}
describe
()
{
return
super
.
describe
()
+
' ('
+
this
.
title
+
')'
;
}
}
更多資訊:章節「類別」。
Error
的子類別 在 ES5 中,無法對內建的例外建構函式 Error
進行子類別化。以下程式碼顯示了一個解決方法,它為建構函式 MyError
提供了堆疊追蹤等重要功能
function
MyError
()
{
// Use Error as a function
var
superInstance
=
Error
.
apply
(
null
,
arguments
);
copyOwnPropertiesFrom
(
this
,
superInstance
);
}
MyError
.
prototype
=
Object
.
create
(
Error
.
prototype
);
MyError
.
prototype
.
constructor
=
MyError
;
function
copyOwnPropertiesFrom
(
target
,
source
)
{
Object
.
getOwnPropertyNames
(
source
)
.
forEach
(
function
(
propKey
)
{
var
desc
=
Object
.
getOwnPropertyDescriptor
(
source
,
propKey
);
Object
.
defineProperty
(
target
,
propKey
,
desc
);
});
return
target
;
};
在 ES6 中,所有內建建構函式都可以進行子類別化,這就是以下程式碼可以達成 ES5 程式碼只能模擬的效果的原因
class
MyError
extends
Error
{
}
更多資訊:章節「內建建構函式的子類別化」。
在 JavaScript 中,一直以來使用語言建構 物件作為從字串到任意值的對應(資料結構)都是一種權宜之計。最安全的方法是建立原型為 null
的物件。然後你仍然必須確保沒有任何鍵是字串 '__proto__'
,因為此屬性鍵會在許多 JavaScript 引擎中觸發特殊功能。
以下 ES5 程式碼包含使用物件 dict
作為對應的函式 countWords
var
dict
=
Object
.
create
(
null
);
function
countWords
(
word
)
{
var
escapedWord
=
escapeKey
(
word
);
if
(
escapedWord
in
dict
)
{
dict
[
escapedWord
]
++
;
}
else
{
dict
[
escapedWord
]
=
1
;
}
}
function
escapeKey
(
key
)
{
if
(
key
.
indexOf
(
'__proto__'
)
===
0
)
{
return
key
+
'%'
;
}
else
{
return
key
;
}
}
在 ES6 中,你可以使用內建資料結構 Map
,而且不必跳脫鍵。缺點是,在 Map 中遞增值較不方便。
const
map
=
new
Map
();
function
countWords
(
word
)
{
const
count
=
map
.
get
(
word
)
||
0
;
map
.
set
(
word
,
count
+
1
);
}
Map 的另一個好處是你可以使用任意值作為鍵,而不仅仅是字串。
更多資訊
ECMAScript 6 標準程式庫提供多種新的字串方法。
從 indexOf
到 startsWith
if
(
str
.
indexOf
(
'x'
)
===
0
)
{}
// ES5
if
(
str
.
startsWith
(
'x'
))
{}
// ES6
從 indexOf
到 endsWith
function
endsWith
(
str
,
suffix
)
{
// ES5
var
index
=
str
.
indexOf
(
suffix
);
return
index
>=
0
&&
index
===
str
.
length
-
suffix
.
length
;
}
str
.
endsWith
(
suffix
);
// ES6
從 indexOf
到 includes
if
(
str
.
indexOf
(
'x'
)
>=
0
)
{}
// ES5
if
(
str
.
includes
(
'x'
))
{}
// ES6
從 join
到 repeat
(ES5 重複字串的方式比較像是權宜之計)
new
Array
(
3
+
1
).
join
(
'#'
)
// ES5
'#'
.
repeat
(
3
)
// ES6
更多資訊:章節「新的字串功能」
ES6 中也有多種新的陣列方法。
Array.prototype.indexOf
到 Array.prototype.findIndex
後者可用於尋找 NaN
,而前者無法偵測
const
arr
=
[
'a'
,
NaN
];
arr
.
indexOf
(
NaN
);
// -1
arr
.
findIndex
(
x
=>
Number
.
isNaN
(
x
));
// 1
順帶一提,新的 Number.isNaN()
提供一種安全的偵測 NaN
的方法(因為它不會將非數字強制轉換為數字)
> isNaN('abc')
true
> Number.isNaN('abc')
false
Array.prototype.slice()
到 Array.from()
或展開運算子 在 ES5 中,Array.prototype.slice()
用於將類陣列物件轉換為陣列。在 ES6 中,您有 Array.from()
var
arr1
=
Array
.
prototype
.
slice
.
call
(
arguments
);
// ES5
const
arr2
=
Array
.
from
(
arguments
);
// ES6
如果一個值是可迭代的(現在所有類陣列 DOM 資料結構都是),您也可以使用展開運算子(...
)將其轉換為陣列
const
arr1
=
[...
'abc'
];
// ['a', 'b', 'c']
const
arr2
=
[...
new
Set
().
add
(
'a'
).
add
(
'b'
)];
// ['a', 'b']
apply()
到 Array.prototype.fill()
在 ES5 中,您可以使用 apply()
作為一種技巧,建立一個任意長度的陣列,並用 undefined
填滿
// Same as Array(undefined, undefined)
var
arr1
=
Array
.
apply
(
null
,
new
Array
(
2
));
// [undefined, undefined]
在 ES6 中,fill()
是更簡單的替代方案
const
arr2
=
new
Array
(
2
).
fill
(
undefined
);
// [undefined, undefined]
如果您想建立一個填滿任意值的陣列,fill()
甚至更方便
// ES5
var
arr3
=
Array
.
apply
(
null
,
new
Array
(
2
))
.
map
(
function
(
x
)
{
return
'x'
});
// ['x', 'x']
// ES6
const
arr4
=
new
Array
(
2
).
fill
(
'x'
);
// ['x', 'x']
fill()
會用給定的值取代所有陣列元素。孔洞會被視為元素。
更多資訊:章節「建立填滿值的陣列」
即使在 ES5 中,基於 AMD 語法或 CommonJS 語法的模組系統大多已取代手寫的解決方案,例如 揭露模組模式。
ES6 已內建支援模組。但遺憾的是,目前還沒有 JavaScript 引擎原生支援它們。但諸如 browserify、webpack 或 jspm 等工具讓您可以使用 ES6 語法建立模組,讓您編寫的程式碼能因應未來。
在 CommonJS 中,您可以如下匯出多個實體
//------ lib.js ------
var
sqrt
=
Math
.
sqrt
;
function
square
(
x
)
{
return
x
*
x
;
}
function
diag
(
x
,
y
)
{
return
sqrt
(
square
(
x
)
+
square
(
y
));
}
module
.
exports
=
{
sqrt
:
sqrt
,
square
:
square
,
diag
:
diag
,
};
//------ main1.js ------
var
square
=
require
(
'lib'
).
square
;
var
diag
=
require
(
'lib'
).
diag
;
console
.
log
(
square
(
11
));
// 121
console
.
log
(
diag
(
4
,
3
));
// 5
或者,您可以將整個模組匯入為一個物件,並透過它存取 square
和 diag
//------ main2.js ------
var
lib
=
require
(
'lib'
);
console
.
log
(
lib
.
square
(
11
));
// 121
console
.
log
(
lib
.
diag
(
4
,
3
));
// 5
在 ES6 中,多重匯出稱為命名匯出,並像這樣處理
//------ 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
));
}
//------ main1.js ------
import
{
square
,
diag
}
from
'lib'
;
console
.
log
(
square
(
11
));
// 121
console
.
log
(
diag
(
4
,
3
));
// 5
將模組匯入為物件的語法如下所示(第 A 行)
//------ main2.js ------
import
*
as
lib
from
'lib'
;
// (A)
console
.
log
(
lib
.
square
(
11
));
// 121
console
.
log
(
lib
.
diag
(
4
,
3
));
// 5
Node.js 延伸 CommonJS,並讓您可以透過 module.exports
從模組中匯出單一值
//------ myFunc.js ------
module
.
exports
=
function
()
{
···
};
//------ main1.js ------
var
myFunc
=
require
(
'myFunc'
);
myFunc
();
在 ES6 中,相同的事情是透過所謂的預設匯出(透過 export default
宣告)來完成
//------ myFunc.js ------
export
default
function
()
{
···
}
// no semicolon!
//------ main1.js ------
import
myFunc
from
'myFunc'
;
myFunc
();
更多資訊:章節「模組」。
現在您已經初步體驗 ES6,您可以瀏覽各章節繼續探索:每個章節涵蓋一個功能或一組相關功能,並從概觀開始。 最後一章在單一位置收集所有這些概觀區段。