ES6、ES2015或者说Harmony给我们带来了很多有趣的特性,本文将对它所提供的这些新特性进行深入剖析。

我赞同这种说法,ES6从2015年6月就出来了,从那以后已经写了很多关于新特性的文章,你最好不要再写一篇文章了…但这并不能阻止我写这篇文章。为什么这么说呢?因为,一是尚未完成的本文已经在我的磁盘里躺了相当长一段时间了,二是虽然我看了一些关于这个主题的文章,但自己写一篇文章有助于给这些知识点留下深刻印象。所以,这篇文章不仅是要写给你,同时也是要写给我自己看的。听起来似乎很公平?好吧,那就开始吧。

默认为严格模式

  • ES5 - 使用 use strict来手动启用严格模式;
  • ES6 - 默认在模块中启用严格模式。

ES5在Javascript中引入了严格模式,让我们的Javascript代码在一个“严格”的操作环境下执行。当你试图写一些脏代码时,会抛出更多的异常。 举个例子,在严格模式下,你不能使用没有声明的变量。例如foo = "bar";,其中foo没有声明会运行失败等等。当然,我不会详细介绍strict模式,而是向你推荐这篇关于这个主题的精彩文章

ES6在它的模块中默认启用了strict模式,也就是说,你不再需要显式地写use strict来启用它了。

块作用域

  • ES5 - 使用var声明变量,并具有函数的作用域;
  • ES6 - 使用let/const声明块作用域变量。

在ES5中,我们只能通过var来声明变量,这些变量具有函数的作用域,而另一种使用方式则是不添加var,这将使它处于全局作用域。

现在我们又多了两种构造方式,即letconst,我们可以用它们来声明具有块级作用域的变量,即在大括号{}内。letconst的唯一区别是const的值一旦初始化就不能改变,而且必须在声明时初始化const变量。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 例子:
// `let`  `const` 声明的变量将不能在`if`块之外被访问
function scopeExample(){
    if(true){
        // `action`  `act` 只能在`if`块内被访问
        let action ="Some action";
        const act = "Some act";
        
        console.log("Doing: " + action);
        console.log("Now doing: " + act);
    }
    console.log(act); // 未捕获的引用错误:"act"未定义。
    console.log(action); // 未捕获的引用错误:"action"未定义
}
// 例子:
// `const`定义的变量一旦被赋值,就不能改变
function constExample(){
    const action = "something";
    action = "Updated action"; //未捕获的类型错误:给常量变量赋值
}

因此,我们不再需要使用IIFEs这种奇怪的语法了, 取而代之的是使用一个块{},使用letconst定义的变量。

模板字符串

以前,当你有一个变量,并想把它的值拼接到一个字符串中时,你必须使用连词操作符来实现,即:

1
2
var name = 'John';
console.log('Hello ' + name);

在ES6中,我们可以使用模板字符串,即可以这样做:

1
2
var name = 'John';
console.log(`Hello ${name}`);

需要注意的点是;对于模板字符串,你必须使用反引号``,而且模板字符串中使用的变量必须在使用模板字符串之前声明。

箭头函数

箭头函数是匿名函数的一种简略语法,其中没有function关键字,并有隐含的return语句。有点绕哈?请看例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// ES5 例子
var sayHello = function(name){
    return 'Hello ' + name + '!';
};

// ES6 例子
// 箭头函数(一行表示)
var sayHello = name => `Hello ${name}!`;
sayHello('World'); // Hello World

// 在涉及多条语句的情况下,使用块{}
var sayHello = name => {
    // 设置name的默认值
    name = name || 'World';
    return `Hello ${name}!`;
};

sayHello(); // Hello World!
sayHello('Jim'); // Hello Jim!

// 不含参数
var sayHello = () => {
    return 'Hello World!';
};
sayHello(); // Hello World!

// 含有多个参数
var greet = (greeting, name) => `${greeting} ${name}!`;
// 或者使用块{}
var greet = (greeting, name) => {
    return `${greeting} ${name}!`;
};

greet('Hello', 'World'); // Hello World

解构赋值

如果你写过PHP,你可能知道一种叫做list的数据结构,现在在Javascript中也可以实现类似的功能。请看例子:

应用于解构数组:

1
2
3
4
5
6
var parts = '2010-11-11'.split('-'),
    year = parts[0],
    month = parts[1],
    day = parts[2];

console.log()

在ES6中一行表示:

1
2
3
4
var [year, month, day] = '2020-10-11.split('-');
console.log(year); // 2010
console.log(month); // 10
console.log(day); // 11

也可以忽略不感兴趣的值:

1
2
3
4
5
6
7
8
9
// 只取month和day的值
var [, month, day] = '2010-10-11'.split('-');
console.log(month); // 10
console.log(day); // 11

// 只取year和day的值
var [year, , day] = '2010-10-11'.split('-');
console.log(year); // 2010
console.log(day); // 11

为了防止从数组中取出一个值为undefined的对象,可以在表达式左边的数组中为任意对象预设默认值。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 假设日期是可选的,如果没有找到,那么我们使用`1`代替
var [year, month, date=1] = '2010-10'.split('-');
console.log(year); // 2010
console.log(month); // 10
console.log(date); // 1

var [year, month, date=1] = '2010-10-11'.split('-');
console.log(year); // 2010
console.log(month); // 10
console.log(date); // 11

应用于解构对象:

1
2
3
4
5
var person = {name: 'John Doe', age: 23, gender: 'male'};
var {name, age, gender} = person;
console.log(name); // John Doe
console.log(age); // 23
console.log(gender); // male

从上面的例子中,你一定注意到了我在var {name, age, gender}中为属性使用了相同的变量名,即name, agegender。但是,如果我想把person.name存储在tag中,把person.gender存储在sex中呢?那么在这种情况下,自然而然会使用到别名:

1
2
3
4
5
var person = {name : 'John Doe', age: 23, gender: 'male'};
var {name:tag, age, gender:sex} = person;
console.log(tag); // John Doe
console.log(age); // 23
console.log(sex); // male

之前提到,对象也可以有默认值。例如,如果你想获得默认性别为male,即如果性别不存在,就使用male。可以通过以下方式实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var person = {name: 'John Doe', age: 23};
var {name, age, gender='male'} = person;
console.log(name); // John Doe
console.log(age); // 23
console.log(gender); // male

// 别名和默认值可以同时使用
var person = {name: 'John Doe', age: 23};
var {name, age, gender:sex = 'male'} = person;
console.log(name); // John Doe
console.log(age); // 23
console.log(sex); // male

这还有一些特殊的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 从函数调用中返回对象/多个值
function getLocation(){
    return {
    latitude: 23.4125,
    longitude: 45.128
    };
}

var {latitude:lat, longitude:long} = getLocation();
console.log(lat); // 23.4125
console.log(long); // 45.128

查看这个要点有很多例子

强大的for...of

以前迭代数组,可能会使用forforEachfor可以跳出循环,但不够简洁,而forEach不允许你跳出循环,但足够简洁。现在for...of既满足简洁又支持跳出循环。下面看看如何使用它来迭代数组:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
var numbers = [10, 20, 30, 40, 50, 60, 70, 80, 90];
for(let number of numbers) {
    console.log(number);
}

// 跳出循环
var numbers = [10, 20, 30, 40, 50, 60, 70, 80, 90];
for(let number of numbers){
    if (number === 50) {
        break;
    }
    console.log(number);
}

举一个使用解构赋值和for...of的例子

1
2
3
4
5
6
7
var guests = [{name: 'John Doe', title: 'Mr'}, {name: 'Jane                 Doe', title: 'Ms'}];
for(let {name} of guests) {
    console.log('Hello ' + name + '!');
}

// Hello John Doe!
// Hello Jane Doe!

默认参数值

在ES6中,我们可以设置默认的参数值。例如之前得这样子实现:

1
2
3
4
5
6
function greet(greet, name){
    greet = greet || 'Hi';
    name = name || 'John Doe';
    
    console.log(greet + ' ' + name); 
}

现在,我们有更简单的方式:

1
2
3
function greet(greet='Hi', name='John Doe'){
    console.log(greet + ' ' + name);
}

另外,与第一个例子不同的是,在第一个例子中,默认值会被赋值给任何为false的值,而后一个例子则会给某些未定义参数赋值。此外,默认值也适用于对象参数。

展开操作符...

以前当你需要一个未知数量的参数时,往往会使用arguments这样的特殊的变量。

1
2
3
4
5
6
function populateBucket(){
    var bucket = [];
    for(var itemCounter = 0; itemCounter < arguments.length; itemCounter++){
        bucket.push(arguments[itemCounter]);
    }
}

你可能已经注意到,这已经开始变得混乱了。在ES6中,您可以使用展开操作符。即:

1
2
3
4
5
6
function populateBucket(...items){
    var bucket = [];
    for(item of items){
        bucket.push(item);
    }
}

展开操作符的另一用法是合并数组:

1
2
3
4
5
6
7
8
9
var a = [1, 2, 3],
    b = [4, 5, 6],
    merged = [];
    
// 以前合并数组的方法
merged = a.concat(b);

// 现在更简洁:
merged = [...a, ...b];

还记得我们以前是如何使用构造函数来创建类的吗?ES6引入了一些语法糖来让它变得更加愉快。现在创建类已经非常方便了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Person{
    // 当初始化person实例的时候会被调用
    constructor(name){
        this.name = name;
        console.log('A person named "' + name + '" is born');
    }
    
    // 不再需要 function 关键字
    sayHello(){
        console.log(this.name + ' says hello'};
}

你也可以继承类而不用接触prototype了:

1
2
3
4
5
6
class Employee extends Person{
    constructor(name){
        super(name);
        console.log(this.name + ' has been employed');
    }
}

Maps

引入了一种新的数据结构,称为map,它使用键值对这样的方式存储数据。

1
2
3
4
5
6
var map = new Map();
map.set('spec', '2015');
map.set('year', '2015');

// 使用`.get()`来查询键值对
console.log(map.get('spec')); // 2015

当然,你也可以迭代maps:

1
2
3
for(var [key, value] of map) {
    console.log(key + ': ' + value);
}

此外,还有一些辅助函数来操作maps。

1
2
3
4
5
6
7
map.entries(); // 返回 map 中所有的键值对
map.keys(); // 返回 map 中所有的键
map.values(); // 获取存储在 map 中的值
map.has(keyName); // 检查 map 中是否有指定的键
map.delete(keyName); // 替换对应键的值
map.size; // 返回 map 的大小
map.clear(); // 清除所有的集合

WeakMaps

Map一样,WeakMap是一个键/值对的集合,其中键必须是对象,或者换句话说,它们必须是引用类型,而不是值类型,如numbers、symbols或strings等,值可以是任意值。如果WeakMap中没有存储键的其他引用,它们会触发垃圾回收。这意味着它们很适合在对象还在使用时为其保留元数据。让我们来看看我从mozilla JS引用中偷来的例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var wm1 = new WeakMap(),
    wm2 = new WeakMap(),
    wm3 = new WeakMap();
var o1 = {},
    o2 = function(){},
    o3 = window;

wm1.set(o1, 37);
wm1.set(o2, "azerty");
wm2.set(o1, o2); // 值可以是任何东西, 包括对象或者函数
wm2.set(o3, undefined);
wm2.set(wm1, wm2); // 键和值可以是任何对象,甚至是WeakMaps!

wm1.get(o2); // “azerty”
wm2.get(o2); // undefined, 因为wm2中的o2没有值
wm2.get(o3); // undefined, 因为这是设定的值

wm1.has(o2); // true
wm2.has(o2); // false
wm2.has(o3); // true (即使该值本身是 "undefined"。)

wm3.set(o1, 37);
wm3.get(o1); // 37

wm1.has(o1); // true
wm1.delete(o1);
wm1.has(o1); // false

Sets

Sets是存储唯一值的集合。任何重复的值都将被忽略。这些值也可以是原始数据或对象引用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let set = new Set();
set.add(1);
set.add('1');
set.add({key: 'value'});
console.log(set); // Set {1, '1', Object {key: 'value'}}

// 删除值
set.delete(2);

// 检查大小
console.log(set.size);

像map一样,set允许通过向其构造函数传递一个数组来创建集合:

1
2
let set = new Set([1, '1', {key: 'value' }]);
console.log(set); // Set {1, '1', Object {key: 'value'}}

为了迭代sets,我们有两个选项–内置的forEach函数或for..of结构。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// forEach
let set = new Set([1, '1', {key: 'value' }]);
set.forEach(function (value) {
    console.log(value);
    // 1
    // '1'
    // Object {key: 'value'}
});

// for...of
let set = new Set([1, '1', {key: 'value' }]);
for(let value of set){
    console.log(value);
    // 1
    // '1'
    // Object {key: 'value'}
};

Weaksets

Set可以是任何东西的集合不同,WeakSet只是对象的集合,而不是任何类型的任意值的集合。WeakSet是弱引用的:对集合中的对象的引用是弱引用的。如果WeakSet中存储的对象没有其他引用,它们就触发垃圾回收。另外,WeakSets不是可枚举的,所以不能使用枚举方法,比如.forEach.clear等。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var ws = new weakSet();
var obj = {};
var foo = {};

ws.add(window);
ws.add(obj);

ws.has(window); // true
ws.has(foo); // false, set中没有foo变量

ws.delete(window); // 从set中删除window
ws.has(window); // false, window 已经被删除了

新的字符串函数

引入了一些新的字符串方法,让我们的生活变得更轻松。还记得我们以前是如何使用indexOf来检查一个字符串是否存在于另一个字符串中,或者检查一个字符串是否以一个特定的字符开始/结束的吗?

1
2
3
4
5
var message = 'Hello World';
message.startsWith('H'); // 而不是 message.indexOf('H') === 0
message.endsWith('d'); // 而不是其他什么 ¯\_(ツ)_/¯

message.includes('or'); //而不是 message.indexOf('or') >= 0

此外,还有一个用于重复字符n次的辅助函数。

1
2
3
4
5
// 之前得要这样做
new Array(3+1) * '*'

// 现在很简单就能实现
'*'.repeat(3); // 将会生成 ***

新的数组函数

引入了一些新的数组函数。Array.from可以让你从类似数组的对象中创建数组。比如说:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 类似数组的对象(参数)转换成数组
function fooBar(){
    return Array.from(arguments);
}

fooBar(1, 2, 3, 4, 5); // [1, 2, 3, 4, 5];

// 任何可迭代的对象
// Set
var s = new Set(['foo', window]);
Array.from(s);
// ['foo', window]

// Map
var m = new Map([[1, 2], [2, 4], [4, 8]]);
Array.from(m);
// [[1, 2], [2, 4], [4, 8]]

// 使用一个箭头函数作为map函数来操作元素
Array.from([1, 2, 3], x => x + x);
// [2, 4, 6]

Array.fill允许你用传递的元素替换一个数组中的所有元素。

1
2
var randomNumbers = [1, 5, 7, 77, 12, 3];
randomNumbers.fill('*'); // ['*', '*', '*','*', '*', '*']

Array.find接收一个回调函数,并返回满足回调函数中条件的第一个元素。

1
2
var users = [{name: 'John Doe', age: 23}, {name: 'Jane Doe', age: 23}, {name: 'Kane Doe', age: 30}];
users.find(user => user.age > 25); // {name: 'Kane Doe', age:30}

还有一个类似的函数是Array.findIndex,它的工作原理是一样的,但返回匹配元素的索引。

模块

ES缺少原生模块支持已经有很长一段时间了。人们想出了AMD、CommonJS或模块化模式等类似的办法来解决。而ES6现在终于有了原生模块的支持。让我们先简单了解一下。

默认情况下,在文件中声明的任何东西都不会在文件外部可见,除非你使用export

以下面的User模块为例

1
2
3
4
5
6
7
// user.js

var localVariable = 123; // 在文件外部不可见

export default function User(age){
    this.age = age;
}; // 可以被其他文件作为模块导入

现在我们希望使用User模块:

1
2
3
4
// user-details.js
import User from 'user';

var user = new User(24);

还有很多的内容。更多详情请看这篇文章

说在最后

虽然ES6已经发布了,但在浏览器中对它的支持仍然悬而未决。但是,现在有大量的工具可以帮助我们使用ES6。最流行的是BabelJS,它既可以作为一个独立的工具运行,也可以和你的构建系统一起使用。他们有针对Grunt、Gulp等的插件

好了,伙计们,差不多就到此为止了。还有一些值得注意的、让人心动的特性,这里没有提到 Generators、Promises、Proxy 和 Symbols 等等。但是,本文所涉及的东西已经足够让你入门了。不过,如果可以的话,我会强烈推荐这一系列的文章这本书。而规范也许是为超前者准备的。

欢迎大家提出建议。

本文翻译自 ES6 Succinctly