Files
es6tutorial/docs/class.md
T
2015-07-02 08:43:53 +08:00

1085 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Class和Module
## Class基本语法
**1)概述**
JavaScript语言的传统方法是通过构造函数,定义并生成新对象。下面是一个例子。
```javascript
function Point(x,y){
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
}
```
上面这种写法跟传统的面向对象语言(比如C++和Java)差异很大,很容易让新学习这门语言的程序员感到困惑。
ES6提供了更接近传统语言的写法,引入了Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。基本上,ES6的class可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。上面的代码用ES6的“类”改写,就是下面这样。
```javascript
//定义类
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '('+this.x+', '+this.y+')';
}
}
```
上面代码定义了一个“类”,可以看到里面有一个constructor方法,这就是构造方法,而this关键字则代表实例对象。也就是说,ES5的构造函数Point,对应ES6的Point类的构造方法。
Point类除了构造方法,还定义了一个toString方法。注意,定义“类”的方法的时候,前面不需要加上function这个保留字,直接把函数定义放进去了就可以了。
ES6的类,完全可以看作构造函数的另一种写法。
```javascript
Class Point{
// ...
}
typeof Point // "function"
```
上面代码表明,类的数据类型就是函数。
构造函数的prototype属性,在ES6的“类”上面继续存在。事实上,除了constructor方法以外,类的方法都定义在类的prototype属性上面。
```javascript
Class Point {
constructor(){
// ...
}
toString(){
// ...
}
toValue(){
// ...
}
}
// 等同于
Point.prototype = {
toString(){},
toValue(){}
}
```
由于类的方法(除constructor以外)都定义在prototype对象上面,所以类的新方法可以添加在prototype对象上面。`Object.assign`方法可以很方便地一次向类添加多个方法。
```javascript
Class Point {
constructor(){
// ...
}
}
Object.assign(Point.prototype, {
toString(){},
toValue(){}
})
```
prototype对象的constructor属性,直接指向“类”的本身,这与ES5的行为是一致的。
```javascript
Point.prototype.constructor === Point // true
```
**2constructor方法**
constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。
```javascript
constructor() {}
```
constructor方法默认返回实例对象(即this),完全可以指定返回另外一个对象。
```javascript
class Foo {
constructor() {
return Object.create(null);
}
}
new Foo() instanceof Foo
// false
```
上面代码中,constructor函数返回一个全新的对象,结果导致实例对象不是Foo类的实例。
**3)实例对象**
生成实例对象的写法,与ES5完全一样,也是使用new命令。如果忘记加上new,像函数那样调用Class,将会报错。
```javascript
// 报错
var point = Point(2, 3);
// 正确
var point = new Point(2, 3);
```
与ES5一样,实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。
```javascript
//定义类
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)
point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
point.hasOwnProperty('toString') // false
point.__proto__.hasOwnProperty('toString') // true
```
上面代码中,x和y都是实例对象point自身的属性(因为定义在this变量上),所以hasOwnProperty方法返回true,而toString是原型对象的属性(因为定义在Point类上),所以hasOwnProperty方法返回false。这些都与ES5的行为保持一致。
```javascript
var p1 = new Point(2,3);
var p2 = new Point(3,2);
p1.__proto__ === p2.__proto__
//true
```
上面代码中,p1和p2都是Point的实例,它们的原型都是Point,所以\_\_proto\_\_属性是相等的。
这也意味着,可以通过实例的\_\_proto\_\_属性为Class添加方法。
```javascript
var p1 = new Point(2,3);
var p2 = new Point(3,2);
p1.__proto__.printName = function () { return 'Oops' };
p1.printName() // "Oops"
p2.printName() // "Oops"
var p3 = new Point(4,2);
p3.printName() // "Oops"
```
上面代码在p1的原型上添加了一个printName方法,由于p1的原型就是p2的原型,因此p2也可以调用这个方法。而且,此后新建的实例p3也可以调用这个方法。这意味着,使用实例的\_\_proto\_\_属性改写原型,必须相当谨慎,不推荐使用,因为这会改变Class的原始定义,影响到所有实例。
**4name属性**
由于本质上,ES6的Class只是ES5的构造函数的一层包装,所以函数的许多特性都被Class继承,包括name属性。
```javascript
class Point {}
Point.name // "Point"
```
name属性总是返回紧跟在class关键字后面的类名。
**5Class表达式**
与函数一样,Class也可以使用表达式的形式定义。
```javascript
const MyClass = class Me {
getClassName() {
return Me.name;
}
};
```
上面代码使用表达式定义了一个类。需要注意的是,这个类的名字是MyClass而不是Me,Me只在Class的内部代码可用,指代当前类。
```javascript
let inst = new MyClass();
inst.getClassName() // Me
Me.name // ReferenceError: Me is not defined
```
上面代码表示,Me只在Class内部有定义。
如果Class内部没用到的话,可以省略Me,也就是可以写成下面的形式。
```javascript
const MyClass = class { /* ... */ };
```
**6)不存在变量提升**
Class不存在变量提升(hoist),这一点与ES5完全不同。
```javascript
new Foo(); // ReferenceError
class Foo {}
```
上面代码中,Foo类使用在前,定义在后,这样会报错,因为ES6不会把变量声明提升到代码头部。这种规定的原因与下文要提到的继承有关,必须保证子类在父类之后定义。
```javascript
{
let Foo = class {};
class Bar extends Foo {
}
}
```
如果存在Class的提升,上面代码将报错,因为let命令也是不提升的。
**7)严格模式**
类和模块的内部,默认就是严格模式,所以不需要使用`use strict`指定运行模式。考虑到未来所有的代码,其实都是运行在模块之中,所以ES6实际上把整个语言升级到了严格模式。
## Class的继承
### 基本用法
Class之间可以通过extends关键字,实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多。
```javascript
class ColorPoint extends Point {}
```
上面代码定义了一个ColorPoint类,该类通过extends关键字,继承了Point类的所有属性和方法。但是由于没有部署任何代码,所以这两个类完全一样,等于复制了一个Point类。下面,我们在ColorPoint内部加上代码。
```javascript
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 调用父类的toString()
}
}
```
上面代码中,constructor方法和toString方法之中,都出现了super关键字,它指代父类的实例(即父类的this对象)。
子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。
```javascript
class Point { /* ... */ }
class ColorPoint extends Point {
constructor() {
}
}
let cp = new ColorPoint(); // ReferenceError
```
上面代码中,ColorPoint继承了父类Point,但是它的构造函数没有调用super方法,导致新建实例时报错。
如果子类没有定义constructor方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有constructor方法。
```javascript
constructor(...args) {
super(...args);
}
```
另一个需要注意的地方是,在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有super方法才能返回父类实例。
```javascript
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
this.color = color; // ReferenceError
super(x, y);
this.color = color; // 正确
}
}
```
上面代码中,子类的constructor方法没有调用super之前,就使用this关键字,结果报错,而放在super方法之后就是正确的。
下面是生成子类实例的代码。
```javascript
let cp = new ColorPoint(25, 8, 'green');
cp instanceof ColorPoint // true
cp instanceof Point // true
```
上面代码中,实例对象cp同时是ColorPoint和Point两个类的实例,这与ES5的行为完全一致。
### 类的prototype属性和\_\_proto\_\_属性
在ES5中,每一个对象都有`__proto__`属性,指向对应的构造函数的prototype属性。Class作为构造函数的语法糖,同时有prototype属性和`__proto__`属性,因此同时存在两条继承链。
1)子类的`__proto__`属性,表示构造函数的继承,总是指向父类。
2)子类prototype属性的`__proto__`属性,表示方法的继承,总是指向父类的prototype属性。
```javascript
class A {
}
class B extends A {
}
B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true
```
上面代码中,子类A的`__proto__`属性指向父类B,子类A的prototype属性的__proto__属性指向父类B的prototype属性。
这两条继承链,可以这样理解:作为一个对象,子类(B)的原型(`__proto__属性`)是父类(A);作为一个构造函数,子类(B)的原型(prototype属性)是父类的实例。
```javascript
B.prototype = new A();
// 等同于
B.prototype.__proto__ = A.prototype;
```
此外,考虑三种特殊情况。第一种特殊情况,子类继承Object类。
```javascript
class A extends Object {
}
A.__proto__ === Object // true
A.prototype.__proto__ === Object.prototype // true
```
这种情况下,A其实就是构造函数Object的复制,A的实例就是Object的实例。
第二种特性情况,不存在任何继承。
```javascript
class A {
}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // true
```
这种情况下,A作为一个基类(即不存在任何继承),就是一个普通函数,所以直接继承`Funciton.prototype`。但是,A调用后返回一个空对象(即Object实例),所以`A.prototype.__proto__`指向构造函数(Object)的prototype属性。
第三种特殊情况,子类继承null。
```javascript
class A extends null {
}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === null // true
```
这种情况与第二种情况非常像。A也是一个普通函数,所以直接继承`Funciton.prototype`。但是,A调用后返回的对象不继承任何方法,所以它的`__proto__`指向`Function.prototype`,即实质上执行了下面的代码。
```javascript
class C extends null {
constructor() { return Object.create(null); }
}
```
### Object.getPrototypeOf()
Object.getPrototypeOf方法可以用来从子类上获取父类。
```javascript
Object.getPrototypeOf(ColorPoint) === Point
// true
```
### 实例的\_\_proto\_\_属性
父类实例和子类实例的\_\_proto\_\_属性,指向是不一样的。
```javascript
var p1 = new Point(2, 3);
var p2 = new ColorPoint(2, 3, 'red');
p2.__proto__ === p1.__proto // false
p2.__proto__.__proto__ === p1.__proto__ // true
```
通过子类实例的\_\_proto\_\_属性,可以修改父类实例的行为。
```javascript
p2.__proto__.__proto__.printName = function () {
console.log('Ha');
};
p1.printName() // "Ha"
```
上面代码在ColorPoint的实例p2上向Point类添加方法,结果影响到了Point的实例p1。
### 原生构造函数的继承
下面是一个继承原生的Array构造函数的例子。
```javascript
class MyArray extends Array {
constructor(...args) {
super(...args);
}
}
var arr = new MyArray();
arr[1] = 12;
```
上面代码定义了一个MyArray类,继承了Array构造函数,因此就可以从MyArray生成数组的实例。这意味着,ES6可以自定义原生数据结构(比如Array、String等)的子类,这是ES5无法做到的。
上面这个例子也说明,extends关键字不仅可以用来继承类,还可以用来继承原生的构造函数。下面是一个自定义Error子类的例子。
```javascript
class MyError extends Error {
}
throw new MyError('Something happened!');
```
## class的取值函数(getter)和存值函数(setter
与ES5一样,在Class内部可以使用get和set关键字,对某个属性设置存值函数和取值函数。
```javascript
class MyClass {
get prop() {
return 'getter';
}
set prop(value) {
console.log('setter: '+value);
}
}
let inst = new MyClass();
inst.prop = 123;
// setter: 123
inst.prop
// 'getter'
```
上面代码中,prop属性有对应的存值函数和取值函数,因此赋值和读取行为都被自定义了。
## Class的Generator方法
如果某个方法之前加上星号(*),就表示该方法是一个Generator函数。
```javascript
class Foo {
constructor(...args) {
this.args = args;
}
* [Symbol.iterator]() {
for (let arg of this.args) {
yield arg;
}
}
}
for (let x of new Foo('hello', 'world')) {
console.log(x);
}
// hello
// world
```
上面代码中,Foo类的Symbol.iterator方法前有一个星号,表示该方法是一个Generator函数。Symbol.iterator方法返回一个Foo类的默认遍历器,for...of循环会自动调用这个遍历器。
## Class的静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
```javascript
class Foo {
static classMethod() {
return 'hello';
}
}
Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod()
// TypeError: undefined is not a function
```
上面代码中,Foo类的classMethod方法前有static关键字,表明该方法是一个静态方法,可以直接在Foo类上调用(`Foo.classMethod()`),而不是在Foo类的实例上调用。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。
父类的静态方法,可以被子类继承。
```javascript
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
}
Bar.classMethod(); // 'hello'
```
上面代码中,父类Foo有一个静态方法,子类Bar可以调用这个方法。
静态方法也是可以从super对象上调用的。
```javascript
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
static classMethod() {
return super.classMethod() + ', too';
}
}
Bar.classMethod();
```
## 修饰器
修饰器(Decorator)用于修改类的行为。这是ES7的一个[提案](https://github.com/wycats/javascript-decorators),目前Babel转码器已经支持。
```javascript
function testable(target) {
target.isTestable = true;
}
@testable
class MyTestableClass () {}
console.log(MyTestableClass.isTestable) // true
```
上面代码中,`@testable`就是一个修饰器。它修改了MyTestableClass这个类的行为,为它加上了静态属性isTestable。
修饰器函数的参数,就是所要修饰的目标对象。比如上面代码中,testable函数的参数target,就是所要修饰的对象。如果希望修饰器的行为,能够根据目标对象的不同而不同,就要在外面再封装一层函数。
```javascript
function testable(isTestable) {
return function(target) {
target.isTestable = isTestable;
}
}
@testable(true) class MyTestableClass () {}
console.log(MyTestableClass.isTestable) // true
@testable(false) class MyClass () {}
console.log(MyClass.isTestable) // false
```
上面代码中,修饰器testable可以接受参数,这就等于可以修改修饰器的行为。
如果想要为类的实例添加方法,可以在修饰器函数中,为目标类的prototype属性添加方法。
```javascript
function testable(target) {
target.prototype.isTestable = true;
}
@testable
class MyTestableClass () {}
let obj = new MyClass();
console.log(obj.isTestable) // true
```
上面代码中,修饰器函数testable是在目标类的prototype属性添加属性,因此就可以在类的实例上调用添加的属性。
下面是另外一个例子。
```javascript
// mixins.js
export function mixins(...list) {
return function (target) {
Object.assign(target.prototype, ...list)
}
}
// main.js
import { mixins } from './mixins'
const Foo = {
foo() { console.log('foo') }
}
@mixins(Foo)
class MyClass {}
let obj = new MyClass()
obj.foo() // 'foo'
```
上面代码通过修饰器mixins,可以为类添加指定的方法。
修饰器可以用`Object.assign()`模拟。
```javascript
const Foo = {
foo() { console.log('foo') }
}
class MyClass {}
Object.assign(MyClass.prototype, Foo);
let obj = new MyClass();
obj.foo() // 'foo'
```
修饰器不仅可以修饰类,还可以修饰类的属性。
```javascript
class Person {
@readonly
name() { return `${this.first} ${this.last}` }
}
```
上面代码中,修饰器readonly用来修饰”类“的name方法。
此时,修饰器函数一共可以接受三个参数,第一个参数是所要修饰的目标对象,第二个参数是所要修饰的属性名,第三个参数是该属性的描述对象。
```javascript
readonly(Person.prototype, 'name', descriptor);
function readonly(target, name, descriptor){
// descriptor对象原来的值如下
// {
// value: specifiedFunction,
// enumerable: false,
// configurable: true,
// writable: true
// };
descriptor.writable = false;
return descriptor;
}
Object.defineProperty(Person.prototype, 'name', descriptor);
```
上面代码说明,修饰器(readonly)会修改属性的描述对象(descriptor),然后被修改的描述对象再用来定义属性。下面是另一个例子。
```javascript
class Person {
@nonenumerable
get kidCount() { return this.children.length; }
}
function nonenumerable(target, name, descriptor) {
descriptor.enumerable = false;
return descriptor;
}
```
## Module
ES6的Class只是面向对象编程的语法糖,升级了ES5的对象定义的写法,并没有解决模块化问题。Module功能就是为了解决这个问题而提出的。
历史上,JavaScript一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能,比如Ruby的require、Python的import,甚至就连CSS都有@import,但是JavaScript任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。
在ES6之前,社区制定了一些模块加载方案,最主要的有CommonJS和AMD两种。前者用于服务器,后者用于浏览器。ES6在语言规格的层面上,实现了模块功能,而且实现得相当简单,完全可以取代现有的CommonJS和AMD规范,成为浏览器和服务器通用的模块解决方案。
ES6模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS和AMD模块,都只能在运行时确定这些东西。比如,CommonJS模块就是对象,输入时必须查找对象属性。
```javascript
var { stat, exists, readFile } = require('fs');
```
ES6模块不是对象,而是通过export命令显式指定输出的代码,输入时也采用静态命令的形式。
```javascript
import { stat, exists, readFile } from 'fs';
```
所以,ES6可以在编译时就完成模块编译,效率要比CommonJS模块高。
**1export命令,import命令**
模块功能主要由两个命令构成:export和import。export命令用于用户自定义模块,规定对外接口;import命令用于输入其他模块提供的功能,同时创造命名空间(namespace),防止函数名冲突。
ES6允许将独立的JS文件作为模块,也就是说,允许一个JavaScript脚本文件调用另一个脚本文件。该文件内部的所有变量,外部无法获取,必须使用export关键字输出变量。下面是一个JS文件,里面使用export关键字输出变量。
```javascript
// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
```
上面代码是profile.js文件,保存了用户信息。ES6将其视为一个模块,里面用export命令对外部输出了三个变量。
export的写法,除了像上面这样,还有另外一种。
```javascript
// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export {firstName, lastName, year};
```
上面代码在export命令后面,使用大括号指定所要输出的一组变量。它与前一种写法(直接放置在var语句前)是等价的,但是应该优先考虑使用这种写法。因为这样就可以在脚本尾部,一眼看清楚输出了哪些变量。
使用export命令定义了模块的对外接口以后,其他JS文件就可以通过import命令加载这个模块(文件)。
```javascript
// main.js
import {firstName, lastName, year} from './profile';
function sfirsetHeader(element) {
element.textContent = firstName + ' ' + lastName;
}
```
上面代码属于另一个文件main.js,import命令就用于加载profile.js文件,并从中输入变量。import命令接受一个对象(用大括号表示),里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同。
如果想为输入的变量重新取一个名字,import语句中要使用as关键字,将输入的变量重命名。
```javascript
import { lastName as surname } from './profile';
```
ES6支持多重加载,即所加载的模块中又加载其他模块。
```javascript
import { Vehicle } from './Vehicle';
class Car extends Vehicle {
move () {
console.log(this.name + ' is spinning wheels...')
}
}
export { Car }
```
上面的模块先加载Vehicle模块,然后在其基础上添加了move方法,再作为一个新模块输出。
**(2)模块的整体输入,module命令**
export命令除了输出变量,还可以输出方法或类(class)。下面是一个circle.js文件,它输出两个方法area和circumference。
```javascript
// circle.js
export function area(radius) {
return Math.PI * radius * radius;
}
export function circumference(radius) {
return 2 * Math.PI * radius;
}
```
然后,main.js输入circlek.js模块。
```javascript
// main.js
import { area, circumference } from 'circle';
console.log("圆面积:" + area(4));
console.log("圆周长:" + circumference(14));
```
上面写法是逐一指定要输入的方法。另一种写法是整体输入。
```javascript
import * as circle from 'circle';
console.log("圆面积:" + circle.area(4));
console.log("圆周长:" + circle.circumference(14));
```
module命令可以取代import语句,达到整体输入模块的作用。
```javascript
// main.js
module circle from 'circle';
console.log("圆面积:" + circle.area(4));
console.log("圆周长:" + circle.circumference(14));
```
module命令后面跟一个变量,表示输入的模块定义在该变量上。
**3export default命令**
为了给用户提供方便,有时我们希望,用户不用知道输入哪个方法,就能加载模块。这时就要用到`export default`命令,为所要加载的模块指定默认输出。
```javascript
// export-default.js
export default function () {
console.log('foo');
}
```
上面代码是一个模块文件`export-default.js`,它的默认输出是一个函数。
其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。
```javascript
// import-default.js
import customName from './export-default';
customName(); // 'foo'
```
上面代码的import命令,可以用任意名称指向`export-default.js`输出的方法。需要注意的是,这时import命令后面,不使用大括号。
```javascript
import crc32 from 'crc32';
// 对应的输出
export default function crc32(){}
import { crc32 } from 'crc32';
// 对应的输出
export function crc32(){};
```
上面代码的两组写法,第一组是使用`export default`时,对应的import语句不需要使用大括号;第二组是不使用`export default`时,对应的import语句需要使用大括号。
export default命令用在非匿名函数前,也是可以的。
```javascript
// export-default.js
export default function foo() {
console.log('foo');
}
// 或者写成
function foo() {
console.log('foo');
}
export default foo;
```
上面代码中,foo函数的函数名foo,在模块外部是无效的。加载的时候,视同匿名函数加载。
`export default`命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此`export deault`命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能对应一个方法。
有了`export default`命令,输入模块时就非常直观了,以输入jQuery模块为例。
```javascript
import $ from 'jquery';
```
如果想在一条import语句中,同时输入默认方法和其他变量,可以写成下面这样。
```javascript
import customName, { otherMethod } from './export-default';
```
如果要输出默认的值,只需将值跟在`export default`之后即可。
```javascript
export default 42;
```
`export default`也可以用来输出类。
```javascript
// MyClass.js
export default class { ... }
// main.js
import MyClass from 'MyClass'
let o = new MyClass();
```
## 模块的继承
模块之间也可以继承。
假设有一个circleplus模块,继承了circle模块。
```javascript
// circleplus.js
export * from 'circle';
export var e = 2.71828182846;
export default function(x) {
return Math.exp(x);
}
```
上面代码中的“export *”,表示输出circle模块的所有属性和方法,export default命令定义模块的默认方法。
这时,也可以将circle的属性或方法,改名后再输出。
```javascript
// circleplus.js
export { area as circleArea } from 'circle';
```
上面代码表示,只输出circle模块的area方法,且将其改名为circleArea。
加载上面模块的写法如下。
```javascript
// main.js
module math from "circleplus";
import exp from "circleplus";
console.log(exp(math.pi));
```
上面代码中的"import exp"表示,将circleplus模块的默认方法加载为exp方法。
## ES6模块的转码
浏览器目前还不支持ES6模块,为了现在就能使用,可以将转为ES5的写法。
**1ES6 module transpiler**
[ES6 module transpiler](https://github.com/esnext/es6-module-transpiler)是square公司开源的一个转码器,可以将ES6模块转为CommonJS模块或AMD模块的写法,从而在浏览器中使用。
首先,安装这个转玛器。
```bash
$ npm install -g es6-module-transpiler
```
然后,使用`compile-modules convert`命令,将ES6模块文件转码。
```bash
$ compile-modules convert file1.js file2.js
```
o参数可以指定转码后的文件名。
```bash
$ compile-modules convert -o out.js file1.js
```
**2SystemJS**
另一种解决方法是使用[SystemJS](https://github.com/systemjs/systemjs)。它是一个垫片库(polyfill),可以在浏览器内加载ES6模块、AMD模块和CommonJS模块,将其转为ES5格式。它在后台调用的是Google的Traceur转码器。
使用时,先在网页内载入system.js文件。
```html
<script src="system.js"></script>
```
然后,使用`System.import`方法加载模块文件。
```html
<script>
System.import('./app');
</script>
```
上面代码中的`./app`,指的是当前目录下的app.js文件。它可以是ES6模块文件,`System.import`会自动将其转码。
需要注意的是,`System.import`使用异步加载,返回一个Promise对象,可以针对这个对象编程。下面是一个模块文件。
```javascript
// app/es6-file.js:
export class q {
constructor() {
this.es6 = 'hello';
}
}
```
然后,在网页内加载这个模块文件。
```html
<script>
System.import('app/es6-file').then(function(m) {
console.log(new m.q().es6); // hello
});
</script>
```
上面代码中,`System.import`方法返回的是一个Promise对象,所以可以用then方法指定回调函数。