本章介绍了核心 ES6 特性。这些特性易于采用;其余特性主要对库作者有用。我将通过相应的 ES5 代码解释每个特性。
var
到 const
/let
for
到 forEach()
到 for-of
arguments
到剩余参数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
。在以下示例中,我创建了辅助变量 _this
(A 行),以便可以在 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
到剩余参数 在 ES5 中,如果您希望函数(或方法)接受任意数量的参数,则必须使用特殊变量 arguments
function
logAllArguments
()
{
for
(
var
i
=
0
;
i
<
arguments
.
length
;
i
++
)
{
console
.
log
(
arguments
[
i
]);
}
}
在 ES6 中,您可以使用 ...
运算符声明剩余参数(以下示例中的 args
)
function
logAllArguments
(...
args
)
{
for
(
const
arg
of
args
)
{
console
.
log
(
arg
);
}
}
如果您只对尾随参数感兴趣,则剩余参数会更好
function
format
(
pattern
,
...
args
)
{
···
}
在 ES5 中处理这种情况很笨拙
function
format
(
pattern
)
{
var
args
=
[].
slice
.
call
(
arguments
,
1
);
···
}
剩余参数使代码更易于阅读:您只需查看函数的参数定义即可判断该函数是否具有可变数量的参数。
更多信息:“剩余参数”一节。
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 代码包含函数 countWords
,该函数使用对象 dict
作为映射。
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
,并且不必转义键。缺点是,在映射中递增值不太方便。
const
map
=
new
Map
();
function
countWords
(
word
)
{
const
count
=
map
.
get
(
word
)
||
0
;
map
.
set
(
word
,
count
+
1
);
}
映射的另一个好处是您可以使用任意值作为键,而不仅仅是字符串。
更多信息
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,您可以继续阅读各章来继续探索:每一章都涵盖了一个或一组相关的功能,并从概述开始。最后一章在一个位置收集了所有这些概述部分。