JavaScript ES6 新增特性整理 - 4.类和模块机制

 2016年10月23日    213     声明


ECMAScript 2015标准给JavaScript语言带来最大改变应该是增加了类(Class)和模块(Module)机制。在早期的JavaScript语言中,我们会使用原型链(prototype)实现基于对象的继承并使用new关键字来创建新对象。ES6中引入了的概念,虽然ES6中的类只是一个语法糖,但它让原型继续语法结构更加清晰,也更加接近面向对象编程的写法。在ES6之前主要有,主要有CommonJS和AMD两种模块规范,但这两种规范都由开源社区制定,而ES6中引入了模块(Module)体系,从语言层在实现了模块机制,为JavaScript开发大型的、复杂的项目扫清了障碍。

  1. 类(Class)
  2. 模块(Module)

1. 类(Class)

传统面向对象语言可以基于类或接口实现继承,而JavaScript不同,只能基于原型实现继承。为了弥补这一缺点,ES6 中新增了类,通过class关键字,你可以实现传统面向对象的继承。class可以认为是对象的模板,它只是一种语法糖,其本质还是原型链继承。

1.1 类定义

实际上是一个特殊的函数。函数有函数声明函数表达式两种,同样类定义也有类声明类表达式两种。

类声明

类声明的语法结构如下:

// 类语法结构
class name [extends] {
  // 类体
}

我们可以像下面这样定义一个Polygon类:

class Polygon {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

在定义函数时,会有函数变量的提升,我们可以先引用函数然后在定义函数。而类声明不同,必须先定义类然后再引用,否则会引发ReferenceError异常:

var p = new Polygon(); // ReferenceError

class Polygon {}

类表达式

类表达式语法结构如下:

// 类表达式语法结构
var MyClass = class [className] [extends] {
  // 类体
};

在类表达式中,类名是可有可无的。如果定义了类名,则该类名只有在类体内部才能访问到:

// 匿名的
var Polygon = class {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
};

// 命名的
var Polygon = class Polygon {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
};

类表达式和类声明一样,也不会有变量提升的现象。


1.2 类体与类方法定义

在定义类时,类的成员要定义在一对花括号{}中,花括号中的代码和花括号本身构成了类体

类成员包括类构造器和类方法,而类方法又分为静态方法和实例方法。

严格模式

类体中的代码都强制在严格模式中执行。

构造器

构造器即构造函数,是一个特殊的类方法,用于创建和初始化对象。一个类只能拥有一个名为constructor的方法,该方法即为构造器。未实现该方法时,会抛出SyntaxError异常。

在子类的构造器中可以使用super关键字调用父类的构造器。

方法定义

定义类时,支持ES6新增的对象方法简写形式:

class Polygon {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
  
  get area() {
    return this.calcArea()
  }

  calcArea() {
    return this.height * this.width;
  }
}
const square = new Polygon(10, 10);

// 100
console.log(square.area);

静态方法

静态方法是指那些不需要对类进行实例化,通过类名就可以直接访问的方法,但静态方法不能在对象实例中调用。静态方法经常用来作为工具函数调用。定义类时,可以通过static关键字用来定义静态方法。

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

  static distance(a, b) {
    const dx = a.x - b.x;
    const dy = a.y - b.y;

    return Math.sqrt(dx*dx + dy*dy);
  }
}

const p1 = new Point(5, 5);
const p2 = new Point(10, 10);

console.log(Point.distance(p1, p2));


1.3 extends与子类定义

在传统的面向对象语言中,可通过继承来实现类复用。在ES6规范的类定义中,可以通过extends关键字在类声明或类表达式中来创建一个继承了某个类的子类。

class Animal { 
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    console.log(this.name + ' makes a noise.');
  }
}

class Dog extends Animal {
  speak() {
    console.log(this.name + ' barks.');
  }
}

var d = new Dog('Mitzie');
// 'Mitzie barks.'
d.speak();

extends关键字同样适用于传统的原型继承的"类"中:

function Animal (name) {
  this.name = name;  
}
Animal.prototype.speak = function () {
  console.log(this.name + ' makes a noise.');
}

class Dog extends Animal {
  speak() {
    super.speak();
    console.log(this.name + ' barks.');
  }
}

var d = new Dog('Mitzie');
d.speak();

但是,extends关键字不能用于非构造对象中。如果要创建的类继承自某个一般对象(非构造对象),要使用Object.setPrototypeOf()

var Animal = {
  speak() {
    console.log(this.name + ' makes a noise.');
  }
};

class Dog {
  constructor(name) {
    this.name = name;
  }
  speak() {
    super.speak();
    console.log(this.name + ' barks.');
  }
}
Object.setPrototypeOf(Dog.prototype, Animal);

var d = new Dog('Mitzie');
d.speak();


1.4 Symbol.species与子构造器重写

Symbol.species属性用于返回创建派生类对象的构造函数。

如,现在MyArray继承自Array对象,在调用.map()等实例方法时,我们希望Array的实例方法而不是返回MyArray的实例方法。这时可以使用Symbol.species重写子类的构造方法:

class MyArray extends Array {
  // 重写 species 为父 Array 的构造器
  static get [Symbol.species]() { return Array; }
}
var a = new MyArray(1,2,3);
var mapped = a.map(x => x * x);

console.log(mapped instanceof MyArray); // false
console.log(mapped instanceof Array);   // true


1.5 使用super引用父类

通过super关键字,可以调用其父类的构造器或者类方法:

class Cat { 
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    console.log(this.name + ' makes a noise.');
  }
}

class Lion extends Cat {
  speak() {
    super.speak();
    console.log(this.name + ' roars.');
  }
}


1.6 Mix-ins多类混入

一个 ECMAScript 类只能有一个父类,所以想要实现多重继承的是不可能的,子承的只能类继父类提供的功能。

抽象子类或Mix-ins是类的模板,可以通过将父类作为输入且将其子类作为输出的函数来实现Mix-ins

var calculatorMixin = Base => class extends Base {
  calc() { }
};

var randomizerMixin = Base => class extends Base {
  randomize() { }
};

使用Mix-ins的类可以像下面这样写:

class Foo { }
class Bar extends calculatorMixin(randomizerMixin(Foo)) { }


2. 模块(Module)

在开发大型、复杂应用时,必不可少的就是模块化。通过模块机制,可以将一个大型的程序拆分成多个小型的模块,再通过一定的规则将多个子模块组装起来。

在ES6 之前,主要有开源社区制定的CommonJSAMD两种模块规划。而在,ES6中通过importexport两处关键字在语言面实现了模块规范。

2.1 ES6的模块机制

ECMAScript 2015基于exportimport,定义了模块的导出和导入规范,从而在语言标准层面实现了模块机制。该标准的目标是创建一种能够兼容CommoneJSAMD两标准的规范,即可以像CommoneJS一样语法简洁、使用单一的接口且支持循环依赖,又可以像AMD支持异步加载和可配置的模块加载机制。

该模块规范由以下两部分组成:

  • 声明语法(定义引入与导出)
  • 编程式加载接口(API):用于配置如何加载模块和按条件加载模块

ES6的模块规范有以下特点:

  • 简洁的语法。语法将比CommoneJS更简单,只使用exportimport实现模块的导出和导入
    • 使用export关键字定义导出对象,这个关键字可以无限次使用
    • 使用import关键字引入导入对象,这个关键字可导入任意数量的模块
  • 模块结构可以做静态分析。这使得在编译时就能确定模块的依赖关系,以及输入和输出的变量
  • 模块支持异步加载
  • 为加载模块提供编程支持,可以按需加载
  • CommonJS更优秀的循环依赖处理


2.2 export与模块导出

export语法声明用于导出函数、对象、指定文件(或模块)的原始值。export有两种模块导出方式:命名式导出(名称导出)和定义式导出(默认导出),命名式导出每个模块可以多个,而默认导出每个模块仅一个。

export可能会有以下几种形式的语法结构:

export { name1, name2, …, nameN };
export { variable1 as name1, variable2 as name2, …, nameN };
export let name1, name2, …, nameN; // 也可以是 var
export let name1 = …, name2 = …, …, nameN; // 也可以是 var, const

export default expression;
export default function (…) { … } // 也可以是 class, function*
export default function name1(…) { … } // 也可以是 class, function*
export { name1 as default, … };

export * from …;
export { name1, name2, …, nameN } from …;
export { import1 as name1, import2 as name2, …, nameN } from …;

命名式导出

模块可以通过export前缀关键词声明导出对象,导出对象可以是多个。这些导出对象用名称进行区分,称之为命名式导出

export { myFunction }; // 导出一个已定义的函数
export const foo = Math.sqrt(2); // 导出一个常量

我们可以使用*from关键字来实现的模块的继承:

export * from 'article';

导出成员与重命名

模块导出时,可以指定模块的导出成员。导出成员可以认为是类中的公有对象,而非导出成员可以认为是类中的私有对象:

var name = 'IT笔录';
var domain = 'http://itbilu.com';

export {name, domain};

模块导出时,可以使用as关键字对导出成员进行重命名:

var name = 'IT笔录';
var domain = 'http://itbilu.com';

export {name as siteName, domain};

默认导出

默认导出也被称做定义式导出。命名式导出可以导出多个值,但在在import引用时,也要使用相同的名称来引用相应的值。而默认导出每个导出只有一个单一值,这个输出可以是一个函数、类或其它类型的值,这样在模块import导入时也会很容易引用。

export default function() {}; // 可以导出一个函数
export default class(){}; // 也可以出一个类

默认导出可以理解为另一种形式的命名导出,默认导出可以认为是使用了default名称的命名导出。

下面两种导出方式是等价的:

const D = 123;

export default D;
export { D as default };


2.3 import与模块导入

import语法声明用于从已导出的模块、脚本中导入函数、对象、指定文件(或模块)的原始值。import模块导入与export模块导出功能相对应,也存在两种模块导入方式:命名式导入(名称导入)和定义式导入(默认导入)。

import可能会有以下几种形式的导入语法:

import defaultMember from "module-name";
import * as name from "module-name";
import { member } from "module-name";
import { member as alias } from "module-name";
import { member1 , member2 } from "module-name";
import { member1 , member2 as alias2 , [...] } from "module-name";
import defaultMember, { member [ , [...] ] } from "module-name";
import defaultMember, * as name from "module-name";
import "module-name";

命名式导入

与导出命名类似,在模块导入时也可以指定导入模块的名称,并将这些成员插入到当作用域中。导入时,可以导入单个成员或多个成员:

import {myMember} from "my-module";
import {foo, bar} from "my-module";

通过*符号,可以导入模块中的全部属性和方法。当导入模块的全部导出内容时,就是将导出模块(如:'my-module.js')所有的导出内容绑定,并插入到当前模块('myModule')的作用域中:

import * as myModule from "my-module";

导入成员与重命名

导入模块对象时,也可以使用as对导入成员重命名,以方便在当前模块内使用:

import {reallyReallyLongModuleMemberName as shortName} from "my-module";

导入多个成员时,同样可以使用别名:

import {reallyReallyLongModuleMemberName as shortName, anotherLongModuleName as short} from "my-module";

默认导入

在模块导出时,可能会存在默认导出。同样的,在导入时可以使用import指令导入这些默认值。

直接导入默认值:

import myDefault from "my-module";

也可以在命名空间导入和名称导入中,同时使用默认导入:

import myDefault, * as myModule from "my-module"; // myModule 做为命名空间使用

import myDefault, {foo, bar} from "my-module"; // 指定成员导入