1
0
mirror of https://github.com/ruanyf/es6tutorial.git synced 2025-05-24 18:32:22 +00:00
es6tutorial/docs/class.md
2015-02-13 12:27:50 +08:00

14 KiB
Raw Blame History

Class和Module

Class

ES5通过构造函数定义并生成新对象。下面是一个例子。


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

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

ES6引入了Class这个概念作为对象的模板。通过class关键字可以定义类。基本上class可以看作只是一个语法糖没有引进任何ES5做不到的新功能只是让对象原型的写法更加清晰而已。上面的代码用“类”改写就是下面这样。


//定义类
class Point {

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

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

}

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

生成实例对象的写法与ES5完全一样也是使用new命令。


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') // false 

上面代码中x和y都是point自身的属性所以hasOwnProperty方法返回true而toString是原型对象的属性所以hasOwnProperty方法返回false。这些都与ES5的行为保持一致。


var p1 = new Point(2,3);
var p2 = new Point(3,2);

p1.__proto__ === p2.__proto__
//true

上面代码中p1和p2都是Point的实例它们的原型都是Point所以__proto__属性是相等的。

这也意味着可以通过__proto__属性为Class添加方法。


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。

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


class ColorPoint extends Point {}

上面代码定义了一个ColorPoint类该类通过extends关键字继承了Point类的所有属性和方法。但是由于没有部署任何代码所以这两个类完全一样等于复制了一个Point类。下面我们在ColorPoint内部加上代码。


class ColorPoint extends Point {

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

  toString() {
    return this.color+' '+super(); // 等同于parent.toString()
  }

}

上面代码中constructor方法和toString方法之中都出现了super关键字它指代父类的同名方法。在constructor方法内super指代父类的constructor方法在toString方法内super指代父类的toString方法。

父类和子类的__proto__属性指向是不一样的。


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__属性可以修改父类。


p2.__proto__.__proto__.printName = function () { 
  console.log('Ha'); 
};

p1.printName() // Ha

上面代码在ColorPoint的实例p2上向Point类添加方法结果影响到了Point的实例p1。

有一个地方需要注意,类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式。考虑到未来所有的代码其实都是运行在模块之中所以ES6实际上把整个语言升级到了严格模式。

Module的基本用法

JavaScript没有模块module体系无法将一个大程序拆分成互相依赖的小文件再用简单的方法拼装起来。其他语言都有这项功能比如Ruby的require、Python的import甚至就连CSS都有@import但是JavaScript任何这方面的支持都没有这对开发大型的、复杂的项目形成了巨大障碍。

在ES6之前社区制定了一些模块加载方案最主要的有CommonJS和AMD两种。前者用于服务器后者用于浏览器。ES6在语言规格的层面上实现了模块功能而且实现得相当简单完全可以取代现有的CommonJS和AMD规范成为浏览器和服务器通用的模块解决方案。

ES6模块的设计思想是尽量的静态化使得编译时就能确定模块的依赖关系以及输入和输出的变量。CommonJS和AMD模块都只能在运行时确定这些东西。比如CommonJS模块就是对象输入时必须查找对象属性。


var { stat, exists, readFile } = require('fs');

ES6模块不是对象而是通过export命令显式指定输出的代码输入时也采用静态命令的形式。


import { stat, exists, readFile } from 'fs';

所以ES6可以在编译时就完成模块编译效率要比CommonJS模块高。

1export命令import命令

模块功能主要由两个命令构成export和import。export命令用于用户自定义模块规定对外接口import命令用于输入其他模块提供的功能同时创造命名空间namespace防止函数名冲突。

ES6允许将独立的JS文件作为模块也就是说允许一个JavaScript脚本文件调用另一个脚本文件。该文件内部的所有变量外部无法获取必须使用export关键字输出变量。下面是一个JS文件里面使用export关键字输出变量。


// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;

上面代码是profile.js文件保存了用户信息。ES6将其视为一个模块里面用export命令对外部输出了三个变量。

export的写法除了像上面这样还有另外一种。


// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export {firstName, lastName, year};

上面代码在export命令后面使用大括号指定所要输出的一组变量。它与前一种写法直接放置在var语句前是等价的但是应该优先考虑使用这种写法。因为这样就可以在脚本尾部一眼看清楚输出了哪些变量。

使用export命令定义了模块的对外接口以后其他JS文件就可以通过import命令加载这个模块文件


// main.js

import {firstName, lastName, year} from './profile';

function sfirsetHeader(element) {
  element.textContent = firstName + ' ' + lastName;
}

上面代码属于另一个文件main.jsimport命令就用于加载profile.js文件并从中输入变量。import命令接受一个对象用大括号表示里面指定要从其他模块导入的变量名。大括号里面的变量名必须与被导入模块profile.js对外接口的名称相同。

如果想为输入的变量重新取一个名字import语句中要使用as关键字将输入的变量重命名。


import { lastName as surname } from './profile';

ES6支持多重加载即所加载的模块中又加载其他模块。


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。


// circle.js

export function area(radius) {
  return Math.PI * radius * radius;
}

export function circumference(radius) {
  return 2 * Math.PI * radius;
}

然后main.js输入circlek.js模块。


// main.js

import { area, circumference } from 'circle';

console.log("圆面积:" + area(4));
console.log("圆周长:" + circumference(14));

上面写法是逐一指定要输入的方法。另一种写法是整体输入。


import * as circle from 'circle';

console.log("圆面积:" + circle.area(4));
console.log("圆周长:" + circle.circumference(14));

module命令可以取代import语句达到整体输入模块的作用。


// main.js

module circle from 'circle';

console.log("圆面积:" + circle.area(4));
console.log("圆周长:" + circle.circumference(14));

module命令后面跟一个变量表示输入的模块定义在该变量上。

3export default命令

如果想要输出匿名函数可以使用export default命令。


// export-default.js

export default function () {
    console.log('foo');
}

其他模块输入该模块时import命令可以为该匿名函数指定任意名字。


// import-default.js

import customName from './export-default';
customName(); // 'foo'

上面代码的import命令可以用任意名称指向输出的匿名函数。需要注意的是这时import命令后面不使用大括号。

export default命令用在非匿名函数前也是可以的。


// export-default.js

export default function foo() {
  console.log('foo');
}

// 或者写成

function foo() {
  console.log('foo');
}

export default foo;

上面代码中foo函数的函数名foo在模块外部是无效的。加载的时候视同匿名函数加载。

export default命令用于指定模块的默认输出。如果模块加载时只能输出一个值或方法那就是export default所指定的那个值或方法。所以import命令后面才不用加大括号。显然一个模块只能有一个默认输出因此export deault命令只能使用一次。

有了export default命令输入模块时就非常直观了以输入jQuery模块为例。


import $ from 'jquery';

如果想在一条import语句中同时输入默认方法和其他变量可以写成下面这样。


import customName, { otherMethod } from './export-default';

如果要输出默认的值,只需将值跟在export default之后即可。


export default 42;  

export default也可以用来输出类。


// MyClass.js
export default class { ... }

// main.js
import MyClass from 'MyClass'
let o = new MyClass();

模块的继承

模块之间也可以继承。

假设有一个circleplus模块继承了circle模块。


// circleplus.js

export * from 'circle';
export var e = 2.71828182846;
export default function(x) {
    return Math.exp(x);
}

上面代码中的“export *”表示输出circle模块的所有属性和方法export default命令定义模块的默认方法。

这时也可以将circle的属性或方法改名后再输出。


// circleplus.js

export { area as circleArea } from 'circle';  

上面代码表示只输出circle模块的area方法且将其改名为circleArea。

加载上面模块的写法如下。


// 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是square公司开源的一个转码器可以将ES6模块转为CommonJS模块或AMD模块的写法从而在浏览器中使用。

首先,安装这个转玛器。


$ npm install -g es6-module-transpiler

然后,使用compile-modules convert命令将ES6模块文件转码。


$ compile-modules convert file1.js file2.js

o参数可以指定转码后的文件名。


$ compile-modules convert -o out.js file1.js

2SystemJS

另一种解决方法是使用SystemJS。它是一个垫片库polyfill可以在浏览器内加载ES6模块、AMD模块和CommonJS模块将其转为ES5格式。它在后台调用的是Google的Traceur转码器。

使用时先在网页内载入system.js文件。


<script src="system.js"></script>

然后,使用System.import方法加载模块文件。


<script>
  System.import('./app');
</script>

上面代码中的./app指的是当前目录下的app.js文件。它可以是ES6模块文件System.import会自动将其转码。

需要注意的是,System.import使用异步加载返回一个Promise对象可以针对这个对象编程。下面是一个模块文件。


// app/es6-file.js:

export class q {
  constructor() {
    this.es6 = 'hello';
  }
}


然后,在网页内加载这个模块文件。


<script>

System.import('app/es6-file').then(function(m) {
  console.log(new m.q().es6); // hello
});

</script>

上面代码中,System.import方法返回的是一个Promise对象所以可以用then方法指定回调函数。