十分钟领略ECMAScript 2015新特性

开篇

大家好,我不是谷阿莫,我不能用十分钟就能让大家精通ECMAScript 2015,但我希望大家读完这篇文章后有能够再花多点点时间去探索ECMAScript 2015。那么问题来了,ECMAScript 2015会是个什么样的东东?如果说现在使用的Javascript是Pyhton2的话,那么ECMAScript 2015就是全新的Python3了。我们需要也应该花一定的时间和精力来学习ECMAScript 2015并考虑将来如何将它的新特性应用到我们的产品中。言归正传,我们一起来看看ECMAScript 2015有哪些特性。

在上一个版本发布了四年之后,最新版本ECMAScript终于在2015年6月17日由ECMA国际会议上批准。在这个会议中,最新版本的ECMAScript并没有被命名为大家之前所认为的ECMAScript 6th Edition,而使用了ECMAScript 2015代替,而ECMAScript 2016也在制定中(看上去是一年出一个标准的节奏呀)。为了兼顾之前的叫法,接下来我还是用ES6来称呼它

ES6和之前ES5相比,对Javascript语法进行了大量创新和优化。ES6提供了关于classesmodulespromisesgeneratorsmapssetstuplespattern matching,以及traits的支持。虽然ES6是向下兼容的,但如果现在就直接使用这些新特性,是无法在绝大多数浏览器以及服务器端环境下运行的。幸运的是,我们可以通过一些工具将基于ES6转换成现在主流浏览器和服务器端环境能够运行的代码,现在我会具体介绍。

运行环境

由于ES6标准才制定出来,主流浏览器包括Chrome,Firefox等都没有做到完全的支持。我们可以通过ES6适配表来查看最完整的包括浏览器,服务器端,移动端等各个端对ES6的支持率。

我们可以通过以下几种方式来运行ES6代码:

通过浏览器访问Babel

Babel是十分著名的ES6ES5产品,我们可以通过在线方式将代码转换成现在普通浏览器能够执行的ES5代码。

它的访问地址是:http://babeljs.io/repl/,可能需要翻墙。

通过Node.js来运行ES6代码

node.js 0.12之后的版本能够通过 --harmony 命令行来运行ES6代码。

joshua:markdown dongua$ more es6.js 
"use strict";
/*********let作用域*********/
(function () {
    {
        let netease = "163.com";
        //163.com
        console.log(netease);
    }
})();

joshua:markdown dongua$ node --harmony es6.js 
163.com

我们也可以通过npm下载babel-node模块来运行ES6代码。io.js最新版本不需要加--harmony参数就能够运行ES6代码,如果有io.js环境的同学可以试试。

介绍完运行ES6的环境,现在我们就来简单的了解一下ES6的新特性,大家也可以通过以上介绍的合适的运行环境来运行一下下面的代码片段,或者自己可以写一些玩一下。

新语法特性

下面介绍的语法新特性是ES6特有的,ES6的语法新特性比之前任何一版的语法新特性都要多得多,可以这么说,学习ES6可以说是重新学习了一门语言。如果我们要掌握ES6,我们一定需要对ES6新增语法有更深的认识。

我在此介绍一些基础的新特性,还有很多高级特性以后再和大家一起分享。

let和const定义变量

ES6新增了两种定义变量的方式,分别是letconst

let

ES6建议使用let取代var。它的用法类似于var,但是所声明的变量,只在let所在的代码块内有效。我们来看一段示例代码:

{
    let me = "163.com";
    //163.com
    console.log(netease);
}
//ReferenceError: netease is not defined
console.log(netease);

我们可以看到,let在{}代码块中定义,因此代码块外调用netease变量就会报引用错误。另外需要注意let不允许在相同作用域内,重复声明同一个变量。

let实际上为JavaScript新增了块级作用域。块级作用域的出现,使得获得广泛应用的立即执行匿名函数(IIFE)不再必要了。

const

let类似,const作用域也在代码块中。但它不是代表定义常量,而是定义常量索引。常量索引顾名思义就是指向这个值的索引一旦定义就不能够改变。换句话说,索引在内存中的指针不能够改变,但是如果指向的值是引用类型,那么依然可以对它进行操作。我们来看一段代码:

const domains = [];
domains.push("163.com");
domains.push("126.com");
domains.push("188.com");
//3
console.log(domains.length);

如果我们在定义了domains后再重新给它赋值,那么会报解析错误:

const domains = [];
//SyntaxError: Assignment to constant variable.
domains = [];

如果你定义一个指向stringnumberbooleanundefined或者nullconst,变量一旦定义后就无法再做任何更改,从另外一个角度上来说,它也成为了常量

变量的解构赋值

ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。解构分配允许我们有像变量声明表达式描述一样在左侧定义变量,在右侧分配值。听起来有点混乱?让我们看看具体的例子。

数组的解构赋值

传统方式定义:

var domains = ["163.com", "126.com", "188.com"];
var domain163,domain126,domain188;
domain163 = domains[0];
domain126 = domains[1];
domain188 = domains[2];

解构方式定义:

let domains = ["163.com", "126.com", "188.com"];
let [domain163,domain126,domain188] = domains;

右侧赋值可以不是一个变量,因此可以简化为:

let [domain163,domain126,domain188]  = ["163.com", "126.com", "188.com"];

这样定义是否是十分简单?

数组的解构赋值还支持嵌套,例如:

let [one, [[two], three]] = [1, [[2], 3]];
//1, 2, 3
console.log(one, two, three);

我们还可以利用这个功能实现之前一句话无法实现的代码:

let main = "163.com", backup = "126.com";
//一句话交换变量值
[main, backup] = [backup, main];

对象的解构赋值

解构不仅可以用于数组,还可以用于对象。我们看如何实现:

let {domain163,domain126,domain188} = {"domain163":"163.com","domain126":"126.com","domain188":"188.com"}
//163.com, 126.com, 188.com
console.log(domain163, domain126, domain188);

如果左侧的变量名和右侧对象属性名不能对应,可以这样使用:

let {"163": main} = {"163": "163.com"};
//163.com
console.log(main);

和数组一样,解构也可以用于嵌套结构的对象。

let info = {
    url : [
        "163.com",
        {product: "mail"}
    ]
};
let {url : [domain, { product }]} = info;
//163.com, mail
console.log(domain, product);

对象的解构也可以指定默认值。

let {domain = "163.com"} = {};
//163.com
console.log(163.com);

解构的用处

  • 交换变量的值
  • 一次性重函数返回多个值
  • 定义函数参数的默认值
  • 其他用途待在开发中发觉

对象标记

方法缩写

在处理对象中,我们可以这样定义方法:

let info = {
//直接定义方法,而无需使用 domain : function(){}方式
  domain() {
    console.log("163.com");
  }
};
//163.com
info.domain();

Getter和Setter

let info = {
    _domain : "163.com",
    get domain() {
        return this._domain;
    },
    set domain(domain){
      this._domain = domain;
    }
};
//163.com
console.log(info.domain);
info.domain = "126.com";
//126.com
console.log(info.domain);

我们可以通过Getter和Setter来处理设置对象属性和获得对象属性时的逻辑。

计算属性名

我们可以把变量作为属性参数传递到一个定义中:

const sub = 'mail';
const config = {
    [`${sub}domain`]: "mail.163.com"
};
//mail.163.com
console.log(config.maildomain);

下面是另外一个例子:

var index = 0;
var obj = {
    [`key${index}`]: index++,
    [`key${index}`]: index++,
    [`key${index}`]: index++,
};
//0,1,2
console.log(obj.key0, obj.key1, obj.key2);

注意,这里属性名使用的是 反引号` ,而不是使用的引号'"

简化方法定义

let info = {
    //get:get
    get
};
function get(){
    return "163.com";
}
//163.com
console.log(info.get());

可以看到,在上下文中有定义了get这个函数,因此在info这个对象定义中,我们就无需再重复写get:get,而直接使用get定义就好了。

循环

for...of

总所周知,for...in是循环获得键名,而for...of用来获取循环的键值。

let domains = ["163.com", "!26.com", "188.com"]
//分别输出163.com,126.com,188.com
for(let domain of domains){
    console.log(domain);
}

不是所有对象都支持for...of,对象需要在支持了Iterator之后才能够使用for...of。

Iterator

遍历器(Iterator)是一种接口规格,任何对象只要部署这个接口,就可以完成遍历操作。它的作用有两个,一是为各种数据结构,提供一个统一的、简便的接口,二是使得对象的属性能够按某种次序排列。在ES6中,遍历操作特指for...of循环,即Iterator接口主要供for...of循环使用。 遍历器提供了一个指针,指向当前对象的某个属性,使用next方法,就可以将指针移动到下一个属性。next方法返回一个包含value和done两个属性的对象。其中,value属性是当前遍历位置的值,done属性是一个布尔值,表示遍历是否结束。

Iterator接口返回的遍历器,原生具备next方法,不用自己部署。所以,真正需要部署的是Iterator接口,让其返回一个遍历器。在ES6中,有三类数据结构原生具备Iterator接口:数组、类似数组的对象、Set和Map结构。除此之外,其他数据结构(主要是对象)的Iterator接口都需要自己部署。

下面就是如何部署Iterator接口。一个对象如果要有Iterator接口,必须部署一个@@iterator方法(原型链上的对象具有该方法也可),该方法部署在一个键名为Symbol.iterator的属性上,对应的键值是一个函数,该函数返回一个遍历器对象。

class MySpecialTree {
  // ...
  [Symbol.iterator]() { 
    // ...
    return theIterator;
  }
}

上面代码是一个类部署Iterator接口的写法。Symbol.iterator是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为Symbol的特殊值,所以要放在方括号内。这里要注意,@@iterator的键名是Symbol.iterator,键值是一个方法(函数),该方法执行后,返回一个当前对象的遍历器。

下面是为对象添加Iterator接口的例子:

let obj = {
  data: [ 'hello', 'world' ],
  [Symbol.iterator]() {
    const self = this;
    let index = 0;
    return {
      next() {
        if (index < self.data.length) {
          return {
            value: self.data[index++],
            done: false
          };
        } else {
          return { value: undefined, done: true };
        }
      }
    };
  }
};

Set和Map数据结构

Set

ES6提供了新的数据结构Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。Set本身是一个构造函数,用来生成Set数据结构。

let s = new Set();
[1,1,2,3,5,4,5,2,2].map(x => s.add(x))
//1 2 3 4 5
for (let i of s) {
    console.log(i)
}

Set可以接受一个数组作为参数,用来初始化:

var items = new Set([1,2,3,4,5,5,5,5]);
//5
console.log(items.size);

向Set加入值的时候,不会发生类型转换,5和“5”是两个不同的值。Set内部判断两个值是否精确相等,使用的是精确相等运算符(===)。这意味着,两个对象总是不相等的。

let set = new Set();

set.add({})
set.size // 1

set.add({})
set.size // 2

上面代码表示,由于两个空对象不是精确相等,所以它们被视为两个值。

属性和方法

属性

  • Set.prototype.constructor:构造函数,默认就是Set函数。
  • Set.prototype.size:返回Set的成员总数。
    方法
  • add(value):添加某个值,返回Set结构本身。如果值不唯一,不会重复添加。
  • delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • has(value):返回一个布尔值,表示该值是否为Set的成员。
  • clear():清除所有成员,没有返回值。

遍历

Set提供三个遍历方法:

  • values():返回一个遍历器。Set结构的默认遍历器就是它的values方法,因此在使用for...of的时候可以省略values方法调用。
  • keys():返回一个遍历器。值和values一样。
  • entries():返回一个遍历器,key和value对应。

Set结构的默认遍历器就是它的values方法。这意味着,可以省略values方法,在for...of循环中直接使用Set。为了与Map结构保持一致,Set结构也有keys和entries方法,这时每个值的键名就是键值。

Map

JavaScript的对象,本质上是键值对的集合,但是只能用字符串当作键。这给它的使用带来了很大的限制。为了解决这个问题,ES6提供了map数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object结构提供了“字符串—值”的对应,Map结构提供了“值—值”的对应。

let map = new Map();
let netease = {"domain":"163.com"};
map.set(netease,"NTES");
//NTES
console.log(map.get(netease));

属性和方法

Map数据结构有以下属性和方法:

  • size:返回成员总数。
  • set(key, value):设置key所对应的键值,然后返回整个Map结构。如果key已经有值,则键值会被更新,否则就新生成该键。
  • get(key):读取key对应的键值,如果找不到key,返回undefined。
  • has(key):返回一个布尔值,表示某个键是否在Map数据结构中。
  • delete(key):删除某个键,返回true。如果删除失败,返回false。
  • clear():清除所有成员,没有返回值。

遍历

Map原生提供三个遍历器:

  • keys():返回键名的遍历器。
  • values():返回键值的遍历器。
  • entries():返回所有成员的遍历器。

Map结构的默认遍历器接口(Symbol.iterator属性),就是entries方法。

Class和Module

Class

ES6引入了Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。一个简单的类定义是这样的:

//定义类
class Point {

  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '('+this.x+', '+this.y+')';
  }

}

var point = new Point(2,3);
point.toString() // (2, 3)

上面代码定义了一个“类”,可以看到里面有一个constructor函数,这就是构造函数,而this关键字则代表实例对象。这个类除了构造方法,还定义了一个toString方法。注意,定义方法的时候,前面不需要加上function这个保留字,直接把函数定义放进去了就可以了。

Class之间可以通过extends关键字,实现继承。

class ColorPoint extends Point {

  constructor(x, y, color) {
    super(x, y); // 等同于super.constructor(x, y)
    this.color = color;
  }

  toString() {
    return this.color+' '+super();
  }

}

Module

ES6的模块标准由两部分组成:

声明式语法(导入和导出用)。 编程式API加载器:设置模块如何加载以及如何有条件地加载模块。 ES6模块不再需要开发人员去将整个JavaScript文件的尴尬地包装成一个对象或函数闭报,这和以前大多数异步模块装载器在浏览器中的做法一样。相反,可以在最顶层进行定义,而只有函数和显式定义的导出变量将可以暴露给模块的消费者:

let privateVar = "this is a variable private to the module";
export let publicVar = "and this one is public";

export function returnPrivateVar() {
   return privateVar;
};

假设将上面的代码保存在mymodule.js中,我们现在可以用两种方法导入,或者是通过导入指定函数和变量,或者通过导入模块作为模块的对象:

import { returnPrivateVar, publicVar } from 'mymodule';
console.log(returnPrivateVar());

或者是:

import 'mymodule' as mm;
console.log(mm.returnPrivateVar());

新的模块标准也支持对模块的内嵌定义和动态模块加载。

小结一下

我们可以看到ES6在设计上的诸多优点,例如模块化设计,定义类更加规范,在处理变量以及对象上更加灵活和严谨,这会对前端开发,甚至基于ES6的后端前台开发带来革命性的改革。现在各个环境对ES6的支持不是很完美,我们也只有在生产环境中逐步升级到ES6,可以考虑后端前台先尝试通过Node.js,io.js来率先使用,等积累了一定的类库以及用户浏览器普及后,逐步在前端使用。现在业界流行的主流框架,包括AngularJSMeteorTypeScript以及Ember等都在尽可能多的支持ES6的新特性,如果有产品在使用这些框架,也可以关注一下官方的更新日志以及API。

本文还有很多ES6的特性没有覆盖到(如果覆盖到了也就不一篇十分钟就能够读完的文章了)。本文也是抛砖引玉,能够让大家快速熟悉ES6常用语法新特性,并能够很快速的对它其他性能进行研究。

也希望之后能够有更多介绍ES6新特性的优秀文章面世。

**下面是一些属性方法扩展,觉得累了的同学可以现在休息休息,之后在用到的时候参考手册。**

已有类扩展

String的扩展

类方法扩展

  • fromCodePoint():可以识别0xFFFF的字符,弥补了String.fromCharCode方法的不足。在作用上,正好与codePointAt方法相反。

    对象方法扩展

  • codePointAt():能够正确处理4个字节储存的字符,返回一个字符的码点。
  • at() 可以识别Unicode编号大于0xFFFF的字符,返回正确的字符。
  • normalize():将字符的不同表示方法统一为同样的形式,这称为Unicode正规化。
  • contains(s):返回布尔值,表示是否找到了参数字符串。
  • startsWith(s):返回布尔值,表示参数字符串是否在源字符串的头部。
  • endsWith(s):返回布尔值,表示参数字符串是否在源字符串的尾部。
  • repeat(n):返回一个新字符串,表示将原字符串重复n次。

Number扩展

类方法的扩展

  • isFinite(n):检查Infinite。
  • isNaN(n):检查NaN。
  • parseInt(n):全局的同名方法移到Number上。
  • parseFloat(n):全局的同名方法一到Number上。
  • isInteger(n):判断一个值是否为整数。

Math对象的扩展

类方法的扩展

  • trunc():去除一个数的小数部分,返回整数部分。
  • Math.acosh(x):返回x的反双曲余弦(inverse hyperbolic cosine)。
  • Math.asinh(x):返回x的反双曲正弦(inverse hyperbolic sine)。
  • Math.atanh(x):返回x的反双曲正切(inverse hyperbolic tangent)。
  • Math.cbrt(x):返回x的立方根。
  • Math.clz32(x):返回x的32位二进制整数表示形式的前导0的个数。
  • Math.cosh(x):返回x的双曲余弦(hyperbolic cosine)。
  • Math.expm1(x):返回eˆx - 1。
  • Math.fround(x):返回x的单精度浮点数形式。
  • Math.hypot(...values):返回所有参数的平方和的平方根。
  • Math.imul(x, y):返回两个参数以32位整数形式相乘的结果。
  • Math.log1p(x):返回1 + x的自然对数。
  • Math.log10(x):返回以10为底的x的对数。
  • Math.log2(x):返回以2为底的x的对数。
  • Math.sign(x):如果x为负返回-1,x为0返回0,x为正返回1。
  • Math.tanh(x):返回x的双曲正切(hyperbolic tangent)。

Array的扩展

类方法的扩展

  • from(obj):两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象,其中包括ES6新增的Set和Map结构。
  • of(x,y,z):将一组值,转换为数组。
  • observe(array, callback):监听数组变化。
  • unobserve(array, callback):取消数组变化的监听。

对象方法的扩展

  • find(function(value, index, arr){}):用于找出第一个符合条件的数组元素。
  • findIndex(function(value, index, arr)):返回第一个符合条件的数组元素的位置,如果所有元素都不符合条件,则返回-1。
  • fill(n):使用给定值,填充一个数组。
  • entries():包含键名和键值的遍历器。
  • keys():包含键名的遍历器。
  • values():包含键值的遍历器。

对象的扩展

类方法的扩展

  • is():用来比较两个值是否严格相等。它与严格比较运算符(===)的行为基本一致,不同之处只有两个:一是+0不等于-0,二是NaN等于自身。
  • assign(target, source...):将源对象(source)的所有可枚举属性,复制到目标对象(target)。

对象方法的扩展

  • observe(object, callback):监听对象变化。
  • unobserve(object, callback):取消对象变化的监听。

参考资料

  1. ECMAScript® 2015 Language Specification
  2. ECMAScript 6 Features 中文版
  3. ECMAScript 6 在线运行环境(需要翻墙)
  4. ES6 in Depth
  5. ECMAScript 6 模块简介
  6. ECMAScript 6 入门


网易云新用户大礼包:https://www.163yun.com/gift

本文来自网易实践者社区,经作者董桦授权发布。