ECMAScript 2015 语法分享

第一部分:前言

一、为什么要使用 ES6

ES6代表了未来,对未来理应拥抱。为从以下几个角度来看,ES6的推广势在必行:

  • 解放开发效率

    • 新特性的合理使用,优雅而简洁
    • 减少第三方库的依赖
    • 可维护性提升,代码量减少
  • 面向未来。

    • 向标准靠拢
    • 官方支持
    • 迟早要学
  • 其他方面

    • 提升技术先进性
    • 促进技术交流,提高技术氛围
    • 编程激情
    • 整合部分历史代码的好机会

二、Nodejs各版本对应的ES6支持情况

1、如果你想一览Node不同版本对所有ES6的特性支持情况,就可以参看node.green这个网站

可以看到
6.11.2 - 99%
6.4.0 - 95%
5.12.0 - 59%

2、可以安转es-checker工具,通过该工具查看node支持的es6语法

sudo npm install es-checker -g

三、实用特性使用情况

特性 推荐程度
arrows ★★★
enhanced object literals ★★★
template strings ★★★
destructuring ★★
default + rest + spread ★★★
promises ★★★
math + number + string + array + object APIs ★★★
let + const ★★★
iterators + for..of ★★
tail calls ★★
modules ★★
map + set + weakmap + weakset ★★
generators
classes
binary and octal literals
symbols
module loaders
proxies
subclassable built-ins
reflect api
unicode

第二部分 ES6 的实用特性

一、使用 let 和 const

let: 用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效

const: 声明一个只读的常量。一旦声明,变量指向的那个内存地址不得改动。而且,const一旦声明变量,该变量就必须立即初始化,不能留到以后赋值。同样,声明的变量,只在const命令所在的代码块内有效。一般用const声明常亮。

特点1:拥有块级作用域

letconst是一种新的变量申明方式,它允许你把变量作用域控制在块级里面。但是在ES5中,块级作用域起不了任何作用。下面列举两种常见的错误场景,然后你会发现会用ES6是这么轻松的就避免犯错!

  • 第一种常见场景:内层变量可能会覆盖外层变量

    var a = 2
    {
           var a = 1;
    }
    a // 1,一不小心发生了同名变量的覆盖
    
    let a = 2
    {
           let a = 1;
    }
    a // 2 ,es6通过块级作用域避免了覆盖的发生
    
  • 第二种常见场景:用来计数的循环变量泄露为全局变量

    for(var i = 0; i < 3; i++ ) {}
    console.log(i);//3,可见,此处的 i 已然成为了全局变量
    
    for(let i=0;i<3;i++) {}
    console.log(i);//使用ES6 抛出异常 ReferenceError: i is not defined
    

特点2:没有变量提升

ES6明确规定,如果区块中存在letconst命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。首先看一个看变量在同一个块作用域内的情况

// 发生了变量提升
if(true) {
  console.log(x); // undefined
  var x = 'hello';
}

// 不准许在变量声明之前使用
if(true) {
  console.log(x); // ReferenceError
  let x = 'hello';
}

这个特性杜绝了我们日常编码中随处声明变量的恶习,强制要求我们养成提前声明变量的习惯。变量的声明和使用在同一作用域下如此,在不同作用域下更是如此:

{{{{
      {
      	console.log(insane); // 报错,提前使用了变量
      }
      let insane = 'Hello World'
    }}}};

{{{{
    	let insane = 'Hello World'
      {
      	console.log(insane); // hello world
      }
    }}}};

特点3:不允许在相同作用域内,重复声明同一个变量

// 正常
function () {
  var a = 10;
  var a = 1;
  // a ,1
}
// 报错
function () {
  let a = 10;
  var a = 1;
}

// 报错
function () {
  let a = 10;
  let a = 1;
}

function func(arg) {
  let arg; // 报错
}

function func(arg) {
  {
    let arg; // 不报错
  }
}

二、解构赋值

ES6 允许按照一定模式,从数组、对象中提取值,对变量进行赋值,这被称为解构。如果等号的右边不是可遍历的结构,如 {}, undefined, 数字常量值等,那么将会报错。下面列举三种最常见的结构场景

1. 对象的解构赋值

如果变量名与属性名(key)一致,则会对应的赋值,不论位置顺序

let { bar, foo } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"

let { foo, bar } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb",即使顺序变了,还是赋值给了对应的key

如果变量名与属性名不一致,必须写成下面这样

// 希望将对象中的foo属性赋值给变量baz
var { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"

结构失败,则变量赋值为undefined

let { baz } = { foo: "aaa", bar: "bbb" };
baz // undefined

究其原理,其实对象的解构赋值是下面形式的简写:

let { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };  

也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者

let { foo: baz } = { foo: "aaa", bar: "bbb" };
baz // "aaa"
foo // error: foo is not defined

上面代码中,foo是匹配的模式,baz才是变量。真正被赋值的是变量baz,而不是模式foo。本段开始提到的{foo}其实是{foo:foo}的简写

在对象中使用解构赋值的好处很多,比如下文中,我们通过传递某个配置对象类完成某些赋值目的

function init(options) {
      var id = options.uid;
      var cid = options.cid;
      var timeout = options.timeout;
      var protocol = options.protocol

      // code to init
}

var options = {
      id: '101',
      cid: 'xxx',
      timeout: '60',
      protocol: 'http'
}

init(options)

这种方式实现起来很好,已经被许多JS开发者所采用。 只是我们必须看函数内部,才知道函数预期需要哪些参数。结合解构赋值,我们就可以在函数声明中清晰地表示这些参数:

function init(param, {id, cid, timeout, protocal}) {
    // code to init
}

var options = {
    id: '101',
    cid: 'xxx',
    timeout: '60',
    protocol: 'http'
}

init(param, options)

在该函数中,我们没有传入一个配置对象,而是以对象解构赋值的方式,给它传参数。这样做不仅使这个函数更加简明,可读性也更高。

注意如果函数调用时,参数被省略掉且没有设置默认值,则会抛出错误

函数解构和默认值组合使用的一个难点:再请问下面两种写法有什么差别?

// 写法一
function m1({x = 0, y = 0} = {}) {
  return [x, y];
}

// 写法二
function m2({x, y} = { x: 0, y: 0 }) {
  return [x, y];
}

上面两种写法都对函数的参数设定了默认值,区别是:
写法一函数参数的默认值是空对象,但是设置了对象解构赋值的默认值;
写法二函数参数的默认值是一个有具体属性的对象,但是没有设置对象解构赋值的默认值。

// 函数没有参数的情况
m1() // [0, 0]
m2() // [0, 0]

// x和y都有值的情况
m1({x: 3, y: 8}) // [3, 8]
m2({x: 3, y: 8}) // [3, 8]

// x有值,y无值的情况
m1({x: 3}) // [3, 0]
m2({x: 3}) // [3, undefined]

// x和y都无值的情况
m1({}) // [0, 0];
m2({}) // [undefined, undefined]

m1({z: 3}) // [0, 0]
m2({z: 3}) // [undefined, undefined]

本质上来说,如果实参有值的话,参数的默认值就不会生效。如果参数的默认值不生效,那么解构就无法发生。

2. 数组的解构赋值

const arr = [1, 2, 3, 4];

// bad
const first = arr[0];
const second = arr[1];

// good
const [first, second] = arr;

数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值
和对象解构一样,如果解构不成功,变量的值就等于undefined

3. 函数返回值的解构

函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。

function getVal() {
  return [ 1, 2 ];
}

let [x,y] = getVal();//函数返回值的解构
console.log(x ,y);// 1, 2

本质上,这种写法属于“模式匹配”,只要等号两边的模式相同或部分相同,左边的(部分)变量就会被赋予对应的值。下面是一些使用嵌套数组进行解构的例子。

// 嵌套
let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3

// ... 运算
let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]

let [head, ...tail, tailest] = [1, 2, 3, 4];
// SyntaxError: Rest element must be last element in array

4. 解构赋值使用默认值

解构赋值允许指定默认值。ES6 内部使用严格相等运算符===,判断一个位置是否有值。所以,如果一个数组成员不严格等于undefined,默认值是不会生效的

let [foo = true] = [];
foo // true

let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'
let [x, y = 'b'] = ['a', 'undefined']; // x='a', y='undefined'
let [x, y = 'b'] = ['a', null]; // x='a', y=null

var {x, y = 5} = {x: 1};
x // 1
y // 5

默认值可以引用解构赋值的其他变量,但该变量必须已经声明。

let [x = 1, y = x] = [];     // x=1; y=1
let [x = 1, y = x] = [2];    // x=2; y=2
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = [];     // ReferenceError

三、字符串扩展

1. 模板文本

模板字符串(template string)是增强版的字符串,用反引号()标识。它可以当作普通字符串使用,也可以用来定义多行字符串。在字符串中嵌入变量,需要将变量名写在${}`之中。

首先,让我们看看 ES5 中拼接字符串的方式

var name = 'feng' , age = '25';
var result = 'hello: ' + name + ', your name is ' + age;
// hello: feng, your name is 25

再看看 ES6 的实现方式

var name = 'feng' , age = '25';
var result = `hello: ${name}, your name is ${age}`
// hello: feng, your name is 25

模板中使用对象

let obj = {x:1,y:2};
console.log(`Your total is: ${obj.x + obj.y}`); // Your total is 3

模板中使用函数调用

function fn() {
    return "Hello World";
}

`foo ${fn()} bar`
// foo Hello World bar

这样做省略了很多影响阅读的+ ,,直接在反引号中使用变量书写,很美观和便利!

2. 多行字符串

ES6 的多行字符串是一个非常实用的功能。在 ES5 中,我们不得不使用以下方法来表示多行字符串

var multStr = 'Then took the other, as just as fair,\n\t'
    + 'And having perhaps the better claim\n\t'
    + 'Because it was grassy and wanted wear,\n\t'
    + 'Though as for that the passing there\n\t'
    + 'Had worn them really about the same,\n\t';

然而在 ES6 中,仅仅用反引号就可以解决了:

var multStr = `Then took the other, as just as fair,
    And having perhaps the better claim
    Because it was grassy and wanted wear,
    Though as for that the passing there
    Had worn them really about the same,`;

如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中

let tt = `
<ul>
    <li>first</li>
    <li>second</li>
</ul>
`;
console.log(tt)

输出成如下内容

<ul>
    <li>first</li>
    <li>second</li>
</ul>

四、函数扩展

1. 默认参数

ES6 之前,不能直接为函数的参数指定默认值,只能采用变通的方法。项目中通过||来实现默认参数

var link = function (height, color) {
    var height = height || 50;
    var color = color || 'red';
    console.log(height + ' : ' + color)
}

link();//50 red
link(10, 'blue');// 10 blue 1

目前来说是正常的,调用该函数时,没有传入实参,会用默认值。但是如果我们传入的参数本身会通过类型转换为false(比如0 或者null)就会有问题:系统忽略掉了我们的入参,反而使用了默认值!

link(0, 'blue');//50 blue

代码默认值是50,调用时希望设置为0,但是还是输出了默认值50

在 ES6 中,我们通过如下方式来完成默认参数的设置,即直接写在参数定义的后面。我们甚至可以让默认值是一个函数(惰性求值)

var link = function (height = 50, color = 'red') {
    console.log(height + ' : ' + color)
}

link(10, 'blue');//10: blue
link(0, blue);//0 blue
link();// 50 : red

还有一点需要着重介绍一下:默认参数可以和解构赋值默认值结合使用

function foo({x, y = 5}) {
  console.log(x, y);
}

foo({}) // undefined, 5
foo({x: 1}) // 1, 5
foo({x: 1, y: 2}) // 1, 2
foo() // TypeError: Cannot read property 'x' of undefined

上面代码使用了对象的解构赋值默认值,而没有使用函数参数的默认值。只有当函数foo的参数是一个对象时,变量xy才会通过解构赋值而生成。如果函数foo调用时参数不是对象(或者不能转换成对象),变量xy就不会生成,从而报错。并且如果参数对象没有y属性,y的默认值5才会生效。

请注意上面例子与下面两种写法的区别

function foo({x=1, y = 5}) {
  console.log(x, y);
}
foo() // TypeError: Cannot match against 'undefined' or 'null'.解构失败

function foo({x=1, y = 5} = {}) {
  console.log(x, y);
}
foo() // 1 5  双重默认值:首先因为实参为空,所以函数参数默认值{}生效。然后才是解构赋值的默认值生效

2. Rest 不定参数

ES6 引入 rest参数(形式为...变量名),用于获取函数的多余参数,rest参数搭配的变量是一个数组,该变量将多余的参数放入数组中。用来取代额外的arguments对象了。

// arguments变量的写法
function sortNumbers() {
    return Array.prototype.slice.call(arguments).sort();
}  
// rest参数的写法
let sortNumbers = (...numbers) => numbers.sort();

利用 rest参数,可以向该函数传入任意数目的参数。下面这个例子中,其中…x代表了所有传入add函数的参数。

//将所有参数相加的函数
function add(...x){
   return x.reduce((m, n)=> m + n);
} 
console.log(add(1,2,3));//输出:6
console.log(add(1,2,3,4,5));//输出:15                                              

rest参数中的变量代表一个数组,所以数组特有的方法都可以用于这个变量。
rest参数之后不能再有其他参数(即,只能是最后一个参数),否则会报错。
一个函数声明只能允许有一个 rest参数

3. 箭头函数

我们知道在JS中回调是经常的事,而一般回调又以匿名函数的形式出现,每次都需要写一个function(){}甚是繁琐。当引入箭头操作符=>后可以方便地写回调了。

它简化了函数的书写。操作符左边为输入的参数,而右边则是进行的操作以及返回的值。即, Inputs => outputs

let array = [1, 2, 3];

//传统写法
array.forEach(function(v) {
    console.log(v);
});

//ES6
//使用函数体形式
array.forEach(v => {
    console.log(v)
});
// 或者直接使用更简洁的表达式
array.forEach(v => console.log(v));    

如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。

var f = () => 5;
var sum = (num1, num2) => num1 + num2;

除了上面的写法的改变,剪头函数使用this时也和我们以前大不相同:以前我们使用闭包,this总是预期之外地产生改变。而箭头函数的迷人之处在于,this的指向是固定的。身处箭头函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。

举个例子,实现一个功能,点击某个按钮之后,调用当前模块的sendData()方法:

首先看看ES5中的处理方式

var polyglot = {
    name : "feng",
    fruits : ["apple", "orange", "watermelon"],
    introduce : function () {
        const self = this;
        this.fruits.forEach(function(item) {
            console.log(this)
            console.log("My name is " + self.name + ", I eat " + item + ".");
        });
    }
}

polyglot.introduce();

introduce里, this.nameundefined(浏览器环境中forEach的匿名回调中this指向window)。在回调函数外面,也就是forEach中, 它指向了polyglot对象。在这种情形下我们总是希望在函数内部this和函数外部的this指向同一个对象。在ES6中就不需要用 _this = this完成这个需求

let polyglot = {
    name : "feng",
    fruits : ["apple", "orange", "watermelon"],
    introduce : function () {
        this.fruits.forEach((item) => {
            console.log("My name is " + this.name + ", I eat " + item + ".");
        });
    }
}

再看个例子,来验证一下(需在浏览器环境验证)

function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}

var id = 21;

foo.call({ id: 42 });
// id: 42

上面代码中,setTimeout的参数是一个箭头函数,这个箭头函数的定义生效是在foo函数生成时,而它的真正执行要等到100毫秒后。如果是普通函数,执行时this应该指向全局对象window,这时应该输出21。但是,箭头函数导致this总是指向函数定义生效时所在的对象(本例是{id: 42}),所以输出的是42

本质上来说,this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数

1.简单的、单行的、不会复用的函数,建议采用箭头函数。如果函数体较为复杂,行数较多,还是应该采用传统的函数写法。而且如果箭头函数有多个参数,必须用圆括号包裹
2.不可以使用arguments对象,该对象在函数体内不存在,外层函数的对应变量
3.由于箭头函数没有自己的this,所以当然也就不能用call()apply()bind()这些方法去改变this的指向

五、数组扩展

1. 扩展运算符…

扩展运算符(spread)是三个点(…),它好比 rest参数的逆运算。rest参数将一个参数转换成数组,而本文中的扩展运算符则负责将一个数组转为用逗号分隔的参数序列。

...[1, 2, 3]
// 1 2 3
1, ...[2, 3, 4], 5
// 1 2 3 4 5
[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]

var arr1 = ['a', 'b'];
var arr2 = ['c'];
[...arr1, ...arr2]// [ 'a', 'b', 'c' ]

下面看几个实际使用案例

  • 使用扩展运算符拷贝数组

    // bad
    const len = items.length;
    const itemsCopy = [];
    let i;
    
    for (i = 0; i < len; i++) {
     itemsCopy[i] = items[i];
    }
    
    // good
    const itemsCopy = [...items];
    
  • 替代数组的 apply方法
    原来在ES5中,因为方法或者函数不支持数组参数, 如Math.max/Array.push(),而必须使用apply(array)的场景,都可以直接使用扩展运算符 …

    // ES5 的写法
    function f(x, y, z) {
      // ...
    }
    var args = [0, 1, 2];
    f.apply(null, args);
    
    // ES6的写法
    function f(x, y, z) {
      // ...
    }
    var args = [0, 1, 2];
    f(...args);
    
    var myArray = [1, 2, 3, 4];
    Math.max(myArray); //error
    Math.max.apply(Math, myArray);// 4,ES5写法
    Math.max(...myArray);//4,ES6写法
    
  • 与解构赋值结合

    let test = [1,2,3]
    let [a, ...rest] = test;
    a //1
    

如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错

2. Array.from

Array.from()可以将各种值转为真正的数组,并且还提供map功能。这实际上意味着,只要有一个原始的数据结构,你就可以先对它的值进行处理,然后转成规范的数组结构,进而就可以使用数量众多的数组方法。

let arrayLike = {
    '0': 'a',
    '1': 'b',
    '2': 'c',
    length: 3
};

// ES5的写法
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']

// ES6的写法
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']

还有,在查找一组DOM节点时

const foo = document.querySelectorAll('.foo');
const nodes = Array.from(foo);

还有,上文说过使用rest参数将参数转换成数组,从而避免使用arguments对象。因为arguments是类数组对象,所以还可以通过Array.from来完成转换工作。

function foo() {
  var args = Array.from(arguments);
  // ...
}

Array.from的第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。

Array.from(arrayLike, x => x * x);
// 等同于
Array.from(arrayLike).map(x => x * x);

Array.from([1, 2, 3], (x) => x * x)
// [1, 4, 9]


Array.from({ length: 2 }, () => 'jack')
// ['jack', 'jack']

类数组对象本质特征只有一点:任何有length属性的对象。
Array.from({ length: 3 });// [ undefined, undefined, undefined ]

3. fill

fill方法使用给定值,填充一个数组。该方法用于空数组的初始化非常方便。要注意,数组中已有的元素,会被全部抹去。

['a', 'b', 'c'].fill(7)
// [7, 7, 7]

new Array(3).fill(7)
// [7, 7, 7]

六、对象扩展

1. 对象属性的简写

如果对象的键值和变量名是一致的,ES6允许仅用变量名来初始化这个对象,而不是定义冗余的键值对。这时,属性名为变量名, 属性值为变量的值

var foo = 'foo';
var bar = 'bar';
var baz = {
    foo,
    bar
};
// 等同于
var baz = {
    foo: foo,
    bar: bar
};

2 对象方法的简写

var o = {
  method(name) {
    return "Hello!" + name;
  }
};

// 等同于
var o = {
  method: function(name) {
    return "Hello!" + name;
  }
};

3. 对象导出属性的简写

module.exports = { getItem, setItem, clear };
// 等同于
module.exports = {
  getItem: getItem,
  setItem: setItem,
  clear: clear
};

4 新增:object.assign()

用于对象的合并,将源对象的所有可枚举属性,复制到目标对象。方法的第一个参数是目标对象,后面的参数都是源对象。如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。

var target = { a: 1, b: 1 };
var source1 = { b: 2, c: 2 };
var source2 = { c: 3 };

Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

需要注意的是,这个方法施行的不是类似merge的操作,而是简单的同名key的的简单覆盖,而不是添加

var target = { 
    a: 1, 
    b: {
        key: {
            inner_key:0
        }
    }
};
var source1 = { 
    b: {
        key:{
            inner_key:1,
            inner_key2:2,
            inner_key3:3
        }
    } 
};

Object.assign(target, source1);
// target { a: 1, b: { key: { inner_key: 1, inner_key2: 2, inner_key3: 3 } } }

Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。这个对象的任何变化,都会反映到目标对象上面。

5. Object.setPrototypeOf

用来设置一个对象的prototype对象,返回参数对象本身。它是 ES6 正式推荐的设置原型对象的方法。

Object.setPrototypeOf(object, prototype)

举一个例子

let proto = {};
let obj = { x: 10 };
Object.setPrototypeOf(obj, proto); // 将proto对象设置为obj的原型

proto.y = 20;
proto.z = 40;

obj.x // 10
obj.y // 20
obj.z // 40

6. Object.getPrototypeOf

该方法与Object.setPrototypeOf方法配套,用于读取一个对象的原型对象。下面是一个例子。

function Rectangle() {
  // ...
}

var rec = new Rectangle();

Object.getPrototypeOf(rec) === Rectangle.prototype
// true

Object.setPrototypeOf(rec, Object.prototype);
Object.getPrototypeOf(rec) === Rectangle.prototype
// false

7. Object.keys(),Object.values(),Object.entries()

Object.keys方法,返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名。
Object.values方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值。
Object.entries方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组。

let {keys, values, entries} = Object;
let obj = { a: 1, b: 2, c: 3 };

for (let key of keys(obj)) {
  console.log(key); // 'a', 'b', 'c'
}

for (let value of values(obj)) {
  console.log(value); // 1, 2, 3
}

for (let [key, value] of entries(obj)) {
  console.log([key, value]); // ['a', 1], ['b', 2], ['c', 3]
}

七、Classes

JavaScript 语言中,生成实例对象的传统方法是通过构造函数。下面是一个例子。

function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')';
};

var p = new Point(1, 2);

ES6 提供了更接近传统面相对象语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。

//定义类
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  // 方法之间不需要逗号分隔,加了会报错
  toString() {
    return `${this.x}{this.y}`
  }
}

类必须使用new调用,否则会报错。

类的静态方法

所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

class Foo {
  static classMethod() {
    return 'hello';
  }
}

Foo.classMethod() // 'hello'

var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function

父类的静态方法,可以被子类继承

Class的继承

Class 可以通过extends关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

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

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

class ColorPoint extends Point {
  constructor(x, y, color) {
    // 调用父类的 constructor(x, y) 
    // 相当于Point.prototype.constructor.call(this,x,y)
    super(x, y); 
    this.color = color;
  }

  toString() {
    return this.color + ' ' + super.toString(); // 调用父类的toString()
  }
}

super关键字,它在这里表示父类的构造函数。子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。

ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。而 ES6 的继承机制完全不同,实质是先创造父类的实例对象thissuper()方法),然后再用子类的构造函数修改this

Object.getPrototypeOf方法可以用来从子类上获取父类。因此,可以使用这个方法判断,一个类是否继承了另一个类。

Object.getPrototypeOf(ColorPoint) === Point
// true

八、Modules(模块)

使用 import 取代 require

// bad
const moduleA = require('moduleA');
const func1 = moduleA.func1;
const func2 = moduleA.func2;

// good
import { func1, func2 } from 'moduleA';

使用 export 取代 module.exports

// point.js
module "point" {
    export class Point {
        constructor (x, y) {
            public x = x;
            public y = y;
        }
    }
}

// myapp.js
//声明引用的模块
module point from "/point.js";
//这里可以看出,尽管声明了引用的模块,还是可以通过指定需要的部分进行导入
import Point from "point";

var origin = new Point(0, 0);
console.log(origin);

参考文章

ES6编程规范
ES手册传送门

upload successful