Silent Reverie

Creating memories with the awesome stuff I've learnt.

浅尝ECMAScript 6

| Comments

现在ECMAScript 6草案制定在不断地推进,浏览器们也在跟进实现,了解ES6的提供的诸多特性是势在必行的事了。

有关浏览器们对ECMAScript 6草案的实现情况可参阅这里

有关ES6的语言特性可参阅这里

目前对ES6实现较为充分的环境有Google Traceur CompilerFirefox Nightly

有关traceur compiler的环境配置如下:

1
2
3
4
5
6
7
8
<script src="https://traceur-compiler.googlecode.com/git/bin/traceur.js"></script>
<script src="https://traceur-compiler.googlecode.com/git/src/bootstrap.js"></script>
<script>
traceur.options.experimental = true;
</script>
<script>
  // blablabla...
</script>

I、新的语法

1. 块作用域(Block Scope)

var关键字声明的变量具有函数作用域或全局作用域,而通过let关键字将变量声明为块作用域,对比如下:

1
2
3
4
5
6
// 在块(curly)作用域中声明并初始化变量
{ var foo = 'foo'; }
{ let bar = 'bar'; }

console.log(foo); // -> 'foo'
console.log(bar); // ReferenceError: bar is not defined

2. 常量(Constants)

许多语言都有常量的概念(值不能改变的变量)。ES6草案将常量引入到了JavaScript中。

使用const关键字将变量声明为常量,规范要求const声明的常量具块作用域,某些浏览器目前将其实现为函数作用域或全局作用域。声明变量为常量时如果不赋值,某些浏览器会将该变量赋undefined值,而某些浏览器则直接报错。为常量重新赋值,某些浏览器会忽略新赋值,某些浏览器会报错。将引用类型(如数组或对象)声明为常量,并不影响引用类型的扩充行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const noexist;             // Const must be initialized (IE)
const foo = 'foo';
foo = 'hoo';               // Assignment to const (IE)
console.log(foo);          // -> 'foo'

// 常量引用类型并不影响对其自身的扩充(`augument`)
const bar = [];
bar.push('spam', 'eggs');
console.log(bar.length);   // -> 2

// 同上
const baz = {};
baz.spam = 'eggs';
console.log(baz.spam);     // -> 'eggs'

3. 默认参数(Default Parameters)

函数声明时可以为参数赋予默认值,这样函数调用时如果参数缺省(undefined),则该参数会被赋予默认值,使用默认参数在一定程序上使函数声明更为直观,并且简化我们的代码。

1
2
3
4
5
6
7
8
9
10
11
12
// 传统写法
function fill(container, liquid) {
  if (typeof liquid === 'undefined') {
    liquid = 'coffee';
  }
  // blablabla...
}

// 新式写法
function fill(container, liquid = 'coffee') {
  // blablabla...
}

4. Rest和Spread操作符

很多时候我们处理函数的时候需要用到数组参数,ES6提供的RestSpread操作符可以让我们更方便地处理数组参数。

rest是将多值折叠为单值集合的过程,和rest相反,spread是将单值集合展开为多值的过程。很多语言都有对它的支持,语法略有差异,Python、Ruby和CoffeeScript将这种语法统称为splats,Python和Ruby用形如*var的语法来表达。CoffeeScript用形如var...的语法来表达,而ES6则用形如...var的语法来表达。

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// 1. rest
function unary(...first) {
  console.log(first);
}

function binary(first, ...rest) {
  console.log([first, rest]);
}

unary('foo');                 // -> ['foo']
unary('foo', 'bar');          // -> ['foo', 'bar']

binary('foo');                // -> ['foo', []]
binary('foo', 'bar', 'baz');  // -> ['foo', ['bar', 'baz']]

// rest实现
;(function (window, undefined) {
  var _slice = Array.prototype.slice;

  function variadic(fn) {
    var numParams    = fn.length, // 形参个数
        numNamedArgs = numParams - 1;


    return numParams < 1 ? fn : function () {
      var
        numArgs             = arguments.length, // 实参个数
        namedArgs           = _slice.call(arguments, 0, numNamedArgs),
        numMissingNamedArgs = Math.max(0, numNamedArgs - numArgs),
        argPadding          = Array(numMissingNamedArgs),
        variadicArgs        = _slice.call(arguments, numNamedArgs);

      return fn.apply(this, namedArgs
                              .concat(argPadding)
                              .concat([variadicArgs]));
    };
  }

  window.variadic = variadic;
}).call(this, this);

// -> ['why', 'hello', 'there']
variadic(unary)('why', 'hello', 'there');

// -> ['why', ['hello', 'there']]
variadic(binary)('why', 'hello', 'there');

// ----------------------------------------------------

// 2. spread
var numbers = [1, 2, 3, 4, 5];

// 传统写法
var max = Math.max.apply(Math, number);

var filled = numbers.slice(0);
[].push.apply(filled, [6, 7, 8, 9, 10]);

// 新式写法
var max = Math.max(...number);
var filled = [...numbers, 6, 7, 8, 9, 10]

5. 解构赋值(Destructuring Assignment)

如果你想从数组中拿取多个元素并赋予其他变量,或是从函数中返回多值,那么ES6提供的解构赋值(Destructuring Assignment)特性正是我们所需要的。

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 1. 数组解构赋值
let a = 1, b = 2;
[b, a] = [a, b];   // swap two variabls

console.log(a, b); // -> 2 1

function dest() {
  return ['why', 'hello', 'there'];
}

let [foo, bar, baz] = dest();

// -> 'why' 'hello' 'there'
console.log(foo, bar, baz);

// 忽略不需要的值
let [a, , c] = dest();

// -> 'why' 'there'
console.log(a, c);

// 2. 对象解构赋值
let janeDoe = {
  first: 'jane',
  last: 'doe',
  gender: 'female',
  hobbies: ['music', 'sports']
};

let { first: firstName, last: lastName } = janeDoe;

// -> 'jane' 'doe'
console.log(firstName, lastName);

let { first, last } = janeDoe;

// -> 'jane' 'doe'
console.log(first, last);

function whatever({ first: f, last: l }) {
  console.log(f, l);
}

// -> 'jane' 'doe'
whatever(janeDoe);

function another({ gender, hobbies: [hob1, hob2] }) {
  console.log(gender, hob1, hob2);
}

// -> 'female' 'music' 'sports'
another(janeDoe);

6. for…of 循环语句

ES6新引入了for…of循环语句,以往我们使用的for..in循环语句用于迭代对象的属性(properties),而新式的for…of循环语句则用于迭代对象的值(values)

1
2
3
4
5
6
7
8
9
10
11
var numbers = [1, 2, 3, 4, 5];

// 传统写法
numbers.forEach(function (number) {
  console.log(number);
});

// 新式写法
for (let number of numbers) {
  console.log(number);
}

7. 迭代器(Iterators)

for..of循环只对迭代对象有效,这意味着要使一个对象可迭代,对象自身需要迭代器。具体可移步参阅这里

8. 生成器(Generators)

生成器可以创建迭代器,并且可以用它来创建自定义迭代对象。生成器异常强大,网上你可以找到它联合Promises一起使用的情形,它为ajax异步调用开启一个新的世界,一个没有callbacks,没有then的世界,它的强大会超乎你的想象。

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// 基本用法
function* numberGen() {
  let limit = 3, i = 0;

  do {
    yield i++;
  } while (i < limit);
}

let iter = numberGen();

// -> { value: 0, done: false }
iter.next();

// -> { value: 1, done: false }
iter.next();

// -> { value: 2, done: false }
iter.next();

// -> { value: undefined, done: true }
iter.next();

// 过滤偶数
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

function* even(numbers) {
  for (let n of numbers) {
    if (n % 2 === 0) {
      yield n;
    }
  }
}

for (let n of even(numbers)) {
  console.log(n);
}

// 延伸用法1
let janeDoe = {
  first: 'jane',
  last: 'doe'
};

function* iterObj(obj) {
  for (let prop in obj) {
    if (obj.hasOwnProperty(prop)) {
      yield [prop, obj[prop]];
    }
  }
}

// -> "first: jane"
// -> "last: doe"
for (let [key, value] of iterObj(janeDoe)) {
  console.log([key, value].join(': '));
}

// 延伸用法2
function Person(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
}

Person.prototype.greeting = function (name) {
  return 'Hello, ' + name;
};

// Person.prototype[Symbol.iterator]
Person.prototype['@@iterator'] = function* () {
  for (let prop in this) {
    if (this.hasOwnProperty(prop)) {
      yield [prop, this[prop]];
    }
  }
};

let johndoe = new Person('john', 'doe');

// -> "first: john"
// -> "last: doe"
for (let [key, value] of johndoe) {
  console.log([key, value].join(': '));
}

9. Comprehensions

Python最早引入了List Comprehensions(列表推导)的概念,这一特性深受程序员的喜爱,这一特性继而引申出Dict ComprehensionsGenerator Comprehensions。CoffeeScript设计时也参考了Python,它也有Comprehensions的概念。而作为日渐流行的JavaScript,这么好的东西怎么能错过呢?自然而然地它也成为了JavaScript的标配。 我们在操作数组的时候经常会使用Array.prototype.mapArray.prototype.filter方法,ES6为我们提供的Comprehensions简直可以称为这两个方法提供语法糖(syntax sugar),其语法表达直白、简明、形象。

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
let fruits = ['apple', 'orange', 'banana'];

// 传统应用
let ripeFruits = fruits.map(function (item) {
  return 'ripe ' + item;
});

let fruitsWithN = fruits.filter(function (item) {
  return item.indexOf('n') > -1;
});

console.log(ripeFruits);
console.log(fruitsWithN);

// 新式玩法
// 1. 数组推导(Array Comprehension)
let ripeFruits2 = ['ripe ' + item for (item of fruits)];
let fruitsWithN2 = [item for (item of fruits) if (item.indexOf('n') > -1)];
let ripeFruitsWithN = ['ripe ' + item for (item of fruits) if (item.indexOf('n') > -1)];

console.log(ripeFruits2);
console.log(fruitsWithN2);
console.log(ripeFruitsWithN);

// 2. 生成器推导(Generator Comprehension)
let iter = ('ripe ' + item for (item of fruits) if (item.indexOf('n') > -1));
console.log(iter.next()); // -> "ripe orange"
console.log(iter.next()); // -> "ripe banana"

10. 箭头函数(Arrow Functions)

有关ES6箭头函数的介绍移步这里参阅。

II、新的代码组织方式

1. 类(Classes)

移步这里以及这里进行参阅。

2. 模块(Modules)

移步这里进行参阅。

III、新的标准类库

1. 集合(Set)

Python的作者是个数学家,所以这门语言早期就有了对Set的支持。现在,ES6也将Set纳为标配了。有了集合,数组去除已然不费吹灰之力了。 更详细的介绍请移步这里

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
// 创建集合实例
let items = new Set([1, 2, 2, 3, 4, 3, 5, '2']);

// 判断指定元素是否在集合中
console.log(items.has('2'));

// 遍历集合元素
for (let item of items) {
  console.log(item);
}

// 添加元素到集合中
items.add(6);

// 从集合中移除元素
items.delete(1);

// 获取集合里元素个数
console.log(items.size);

// 遍历集合元素
for (let item of items) {
  console.log(item);
}

// 移除集合里所有元素
items.clear();

2. 映射(Map)

以往我们都是使用字面对象构建键值对映射,ES6提供的Map结构提供了友好直白的API让我们可以更好地操纵键值对,就像localStorge一样,它使我们更好地在客户端保存数据。 更详细的介绍请移步这里

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 创建映射实例
let stuff = new Map();

// 设置键值对
stuff.set('foo', 'bar')
// 你没看错,对象也可以作为Map的key
stuff.set(document, document.createElement('div'));

// 判断是否设置了指定键
console.log(stuff.has('foo'));

// 获取键值对个数
console.log(stuff.size);

// 获取指定键的值
console.log(stuff.get('foo'));

// 删除指定的键
stuff.delete('foo');

// 清除所有键
stuff.clear();

// 遍历键值对
for (let item of stuff) {
  // key = item[0];
  // value = item[1];
  // do something
}

// 同上
for (let item of stuff.items()) {
  // do something
}

// 同上(解构赋值)
for (let [key, value] of stuff) {
  // do something
}

// 遍历所有键
for (let key of stuff.keys()) {
  // do something
}

// 遍历所有值
for (let value of stuff.values()) {
  // do something
}

2. 弱映射(WeakMap)

WeakMapMap类似,但WeakMap的键只能为对象,而不能为原始类型。 有关WeakMap的详细的介绍请移步这里以及这里

3. Promises

由于JavaScript的异步编程的普遍应用,Promises尤显得重要。众望所归,ES6也把Promises写入草案中了。目前Firefox 30+及Chrome33+实现了Promises,加了这个polyfill,我们可以在所有现代浏览器中使用它。

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
35
36
37
38
39
40
41
42
43
44
45
46
47
function get(url) {
  return new Promise(function (resolve, reject) {
    var xhr = new XMLHttpRequest();

    xhr.open('GET', url);

    xhr.onload = function () {
      var status = xhr.status;

      if ((status >= 200 && status < 300) || status === 304) {
        resolve(xhr.response);
      } else {
        reject(xhr.statusText);
      }
    };

    xhr.onerror = function () {
      reject('Network error');
    };

    xhr.send(null);
  });
}

function getJSON(url) {
  return get(url).then(JSON.parse);
}

// file1.txt: { "message": "This is the first file." }
// file2.txt: { "message": "This is the second file." }
// file3.txt: 404

let promise = getJSON('file1.txt');

promise
  .then(function (obj) {
    alert(obj.message);
    return getJSON('file2.txt');
  })
  .then(function () {
    alert(obj.message);
    return getJSON('file3.txt');
  })
  .then(function (obj) {
    alert(obj.message);
  })
  .catch(console.log);

3. 虚拟对象(Proxies)

Proxies实为虚拟对象,它为对象操纵添加了一道包装,使用它有点Ruby元编程的味道。更多关于Proxies的解释请参阅这里

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// 原型
let handler = {
  get: function (proxy, name) {
    console.log('Getter for ' + name);
  },

  set: function (proxy, name, value) {
    console.log('Setter for ' + name + ' and value of ' + value);
  },

  has: function (name) {
    console.log(name + ' is in the has trap.');
  }
};

var proxy = Proxy.create(handler);

// 实例
let createElement = (function () {
  let specialProps = ['id', 'className'];

  return function (tagName) {
    let element = document.createElement(tagName);

    let p = Proxy.create({
      get: function (proxy, name) {
        if (name === 'node') {
          return element;
        }

        if (name in element) {
          return element[name];
        }

        return element.getAttribute(name);
      },

      set: function (proxy, name, value) {
        if (name === 'node') {
          throw new Error('node cannot be set');
        }

        if (name in element) {
          if (specialProps.indexOf(name) === -1) {
            throw new Error(name + ' cannot be set');
          }

          element[name] = value;
        } else {
          element.setAttribute(name, value);
        }
      }
    });

    return p;
  };
})();

let el = createElement('div'); // proxy

el.id = 'proxyTest';
el.className = 'first-class';
el.classList.add('second-class');
el.foo = 'bar';
el['data-proxy-test'] = true;

console.log(el.node);
console.log(el.id);
console.log(el.className);
console.log(el.foo);
console.log(el['data-proxy-test']);

IV、尾声

更多关于ES6的信息可参阅如下链接:

  1. Addy Osmani ES6 Tools
  2. Tracking ECMAScript 6 Support
  3. ES6 Support in Mozilla
  4. Kangax’s ES6 Support Table
  5. ES6 Specification Wiki

Comments