1
0
mirror of https://github.com/ruanyf/es6tutorial.git synced 2025-05-24 18:32:22 +00:00

edit class/decorator

This commit is contained in:
Ruan Yifeng 2015-07-02 08:43:53 +08:00
parent 5c73894118
commit 6339feb01b
5 changed files with 218 additions and 118 deletions

View File

@ -41,6 +41,18 @@ class Point {
Point类除了构造方法还定义了一个toString方法。注意定义“类”的方法的时候前面不需要加上function这个保留字直接把函数定义放进去了就可以了。
ES6的类完全可以看作构造函数的另一种写法。
```javascript
Class Point{
// ...
}
typeof Point // "function"
```
上面代码表明,类的数据类型就是函数。
构造函数的prototype属性在ES6的“类”上面继续存在。事实上除了constructor方法以外类的方法都定义在类的prototype属性上面。
```javascript
@ -161,7 +173,7 @@ p1.__proto__ === p2.__proto__
上面代码中p1和p2都是Point的实例它们的原型都是Point所以\_\_proto\_\_属性是相等的。
这也意味着,可以通过\_\_proto\_\_属性为Class添加方法。
这也意味着,可以通过实例的\_\_proto\_\_属性为Class添加方法。
```javascript
var p1 = new Point(2,3);
@ -183,11 +195,8 @@ p3.printName() // "Oops"
由于本质上ES6的Class只是ES5的构造函数的一层包装所以函数的许多特性都被Class继承包括name属性。
```javascript
class Point {}
Point.name // "Point"
```
name属性总是返回紧跟在class关键字后面的类名。
@ -197,23 +206,19 @@ name属性总是返回紧跟在class关键字后面的类名。
与函数一样Class也可以使用表达式的形式定义。
```javascript
const MyClass = class Me {
getClassName() {
return Me.name;
}
};
```
上面代码使用表达式定义了一个类。需要注意的是这个类的名字是MyClass而不是MeMe只在Class的内部代码可用指代当前类。
```javascript
let inst = new MyClass();
inst.getClassName() // Me
Me.name // ReferenceError: Me is not defined
```
上面代码表示Me只在Class内部有定义。
@ -221,9 +226,7 @@ Me.name // ReferenceError: Me is not defined
如果Class内部没用到的话可以省略Me也就是可以写成下面的形式。
```javascript
const MyClass = class { /* ... */ };
```
**6不存在变量提升**
@ -231,23 +234,18 @@ const MyClass = class { /* ... */ };
Class不存在变量提升hoist这一点与ES5完全不同。
```javascript
new Foo(); // ReferenceError
class Foo {}
```
上面代码中Foo类使用在前定义在后这样会报错因为ES6不会把变量声明提升到代码头部。这种规定的原因与下文要提到的继承有关必须保证子类在父类之后定义。
```javascript
{
let Foo = class {};
class Bar extends Foo {
}
}
```
如果存在Class的提升上面代码将报错因为let命令也是不提升的。
@ -263,36 +261,31 @@ class Foo {}
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); // 等同于parent.constructor(x, y)
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 等同于parent.toString()
return this.color + ' ' + super.toString(); // 调用父类的toString()
}
}
```
上面代码中constructor方法和toString方法之中都出现了super关键字它指代父类的实例即父类的this对象
子类必须在constructor方法中调用super方法否则新建实例时会报错。
子类必须在constructor方法中调用super方法否则新建实例时会报错。这是因为子类没有自己的this对象而是继承父类的this对象然后对其进行加工。如果不调用super方法子类就得不到this对象。
```javascript
class Point { /* ... */ }
class ColorPoint extends Point {
@ -301,23 +294,21 @@ class ColorPoint extends Point {
}
let cp = new ColorPoint(); // ReferenceError
```
上面代码中ColorPoint继承了父类Point但是它的构造函数没有调用super方法导致新建实例时报错。
如果子类没有定义constructor方法这个方法会被默认添加代码如下。也就是说不管有没有显式定义任何一个子类都有constructor方法。
```javascript
constructor(...args) {
super(...args);
}
```
另一个需要注意的地方是在子类的构造函数中只有调用super之后才可以使用this关键字否则会报错。这是因为没有调用父类的构造函数,就无法子类实例的构建
另一个需要注意的地方是在子类的构造函数中只有调用super之后才可以使用this关键字否则会报错。这是因为子类实例的构建是基于对父类实例加工只有super方法才能返回父类实例
```javascript
class Point {
constructor(x, y) {
this.x = x;
@ -332,7 +323,6 @@ class ColorPoint extends Point {
this.color = color; // 正确
}
}
```
上面代码中子类的constructor方法没有调用super之前就使用this关键字结果报错而放在super方法之后就是正确的。
@ -340,12 +330,10 @@ class ColorPoint extends Point {
下面是生成子类实例的代码。
```javascript
let cp = new ColorPoint(25, 8, 'green');
cp instanceof ColorPoint // true
cp instanceof Point // true
```
上面代码中实例对象cp同时是ColorPoint和Point两个类的实例这与ES5的行为完全一致。
@ -359,85 +347,61 @@ cp instanceof Point // true
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
class B extends A {
constructor() {
return A.call(this);
}
}
// 等同于
class B extends A {
constructor() {
return B.__proto__.call(this);
}
}
```
第二条继承链,实质如下。
```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的复制A的实例就是Object的实例。
这种情况下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属性。
这种情况下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`,即实质上执行了下面的代码。
这种情况与第二种情况非常像。A也是一个普通函数所以直接继承`Funciton.prototype`。但是A调用后返回的对象不继承任何方法所以它的`__proto__`指向`Function.prototype`,即实质上执行了下面的代码。
```javascript
class C extends null {
@ -450,46 +414,39 @@ class C extends null {
Object.getPrototypeOf方法可以用来从子类上获取父类。
```javascript
Object.getPrototypeOf(ColorPoint) === Point
// true
```
### 实例的\_\_proto\_\_属性
父类和子类的\_\_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\_\_属性可以修改父类。
通过子类实例的\_\_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);
@ -498,20 +455,17 @@ class MyArray extends Array {
var arr = new MyArray();
arr[1] = 12;
```
上面代码定义了一个MyArray类继承了Array构造函数因此就可以从MyArray生成数组的实例。这意味着ES6可以自定义原生数据结构比如Array、String等的子类这是ES5无法做到的。
上面这个例子也说明extends关键字不仅可以用来继承类还可以用来继承构造函数。下面是一个自定义Error子类的例子。
上面这个例子也说明extends关键字不仅可以用来继承类还可以用来继承原生的构造函数。下面是一个自定义Error子类的例子。
```javascript
class MyError extends Error {
}
throw new MyError('Something happened!');
```
## class的取值函数getter和存值函数setter
@ -519,7 +473,6 @@ throw new MyError('Something happened!');
与ES5一样在Class内部可以使用get和set关键字对某个属性设置存值函数和取值函数。
```javascript
class MyClass {
get prop() {
return 'getter';
@ -536,7 +489,6 @@ inst.prop = 123;
inst.prop
// 'getter'
```
上面代码中prop属性有对应的存值函数和取值函数因此赋值和读取行为都被自定义了。
@ -546,7 +498,6 @@ inst.prop
如果某个方法之前加上星号(*就表示该方法是一个Generator函数。
```javascript
class Foo {
constructor(...args) {
this.args = args;
@ -563,7 +514,6 @@ for (let x of new Foo('hello', 'world')) {
}
// hello
// world
```
上面代码中Foo类的Symbol.iterator方法前有一个星号表示该方法是一个Generator函数。Symbol.iterator方法返回一个Foo类的默认遍历器for...of循环会自动调用这个遍历器。
@ -573,7 +523,6 @@ for (let x of new Foo('hello', 'world')) {
类相当于实例的原型所有在类中定义的方法都会被实例继承。如果在一个方法前加上static关键字就表示该方法不会被实例继承而是直接通过类来调用这就称为“静态方法”。
```javascript
class Foo {
static classMethod() {
return 'hello';
@ -585,7 +534,6 @@ Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod()
// TypeError: undefined is not a function
```
上面代码中Foo类的classMethod方法前有static关键字表明该方法是一个静态方法可以直接在Foo类上调用`Foo.classMethod()`而不是在Foo类的实例上调用。如果在实例上调用静态方法会抛出一个错误表示不存在该方法。
@ -593,7 +541,6 @@ foo.classMethod()
父类的静态方法,可以被子类继承。
```javascript
class Foo {
static classMethod() {
return 'hello';
@ -604,7 +551,6 @@ class Bar extends Foo {
}
Bar.classMethod(); // 'hello'
```
上面代码中父类Foo有一个静态方法子类Bar可以调用这个方法。
@ -612,7 +558,6 @@ Bar.classMethod(); // 'hello'
静态方法也是可以从super对象上调用的。
```javascript
class Foo {
static classMethod() {
return 'hello';
@ -626,7 +571,145 @@ class Bar extends Foo {
}
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

View File

@ -674,7 +674,7 @@ function f(x){
上面代码中函数f的最后一步是调用函数g这就叫尾调用。
以下种情况,都不属于尾调用。
以下种情况,都不属于尾调用。
```javascript
// 情况一
@ -687,9 +687,21 @@ function f(x){
function f(x){
return g(x) + 1;
}
// 情况三
function f(x){
g(x);
}
```
上面代码中情况一是调用函数g之后还有别的操作所以不属于尾调用即使语义完全一样。情况二也属于调用后还有操作即使写在一行内。
上面代码中情况一是调用函数g之后还有别的操作所以不属于尾调用即使语义完全一样。情况二也属于调用后还有操作即使写在一行内。情况三等同于下面的代码。
```javascript
function f(x){
g(x);
return undefined;
}
```
尾调用不一定出现在函数尾部,只要是最后一步操作即可。

View File

@ -38,7 +38,9 @@ ECMA的第39号技术专家委员会Technical Committee 39简称TC39
各大浏览器的最新版本对ES6的支持可以查看[kangax.github.io/es5-compat-table/es6/](http://kangax.github.io/es5-compat-table/es6/)。随着时间的推移支持度已经越来越高了ES6的大部分特性都实现了。
Node.js和io.js一个部署新功能更快的Node分支对ES6的支持度比浏览器更高。通过它们可以体验更多ES6的特性。建议使用版本管理工具[nvm](https://github.com/creationix/nvm)来安装Node.js和io.js。不过nvm不支持Windows系统下面的操作可以改用[nvmw](https://github.com/hakobera/nvmw)或[nvm-windows](https://github.com/coreybutler/nvm-windows)代替。
Node.js和io.js一个部署新功能更快的Node分支是JavaScript语言的服务器运行环境。它们对ES6的支持度比浏览器更高。通过它们可以体验更多ES6的特性。
建议使用版本管理工具[nvm](https://github.com/creationix/nvm)来安装Node.js和io.js。不过nvm不支持Windows系统下面的操作可以改用[nvmw](https://github.com/hakobera/nvmw)或[nvm-windows](https://github.com/coreybutler/nvm-windows)代替。
安装nvm需要打开命令行窗口运行下面的命令。
@ -105,6 +107,13 @@ $ node --v8-options | grep harmony
上面命令的输出结果,会因为版本的不同而有所不同。
我写了一个[ES-Checker](https://github.com/ruanyf/es-checker)模块用来检查各种运行环境对ES6的支持情况。访问[ruanyf.github.io/es-checker](http://ruanyf.github.io/es-checker)可以看到您的浏览器支持ES6的程度。运行下面的命令可以查看本机支持ES6的程度。
```bash
$ npm install -g es-checker
$ es-checker
```
## Babel转码器
[Babel](https://babeljs.io/)是一个广泛使用的ES6转码器可以ES6代码转为ES5代码从而在浏览器或其他环境执行。这意味着你可以用ES6的方式编写程序又不用担心现有环境是否支持。它的安装命令如下。

View File

@ -58,12 +58,10 @@ a[6](); // 6
let不像var那样会发生“变量提升”现象。
```javascript
function do_something() {
console.log(foo); // ReferenceError
let foo = 2;
}
```
上面代码在声明foo之前就使用这个变量结果会抛出一个错误。
@ -71,12 +69,10 @@ function do_something() {
这也意味着typeof不再是一个百分之百安全的操作。
```javascript
if (1) {
typeof x; // ReferenceError
let x;
}
```
上面代码中由于块级作用域内typeof运行时x还没有值所以会抛出一个ReferenceError。
@ -84,14 +80,12 @@ if (1) {
只要块级作用域内存在let命令它所声明的变量就“绑定”binding这个区域不再受外部的影响。
```javascript
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
```
上面代码中存在全局变量tmp但是块级作用域内let又声明了一个局部变量tmp导致后者绑定这个块级作用域所以在let声明变量前对tmp赋值会报错。
@ -101,7 +95,6 @@ ES6明确规定如果区块中存在let和const命令这个区块对这些
总之在代码块内使用let命令声明变量之前该变量都是不可用的。这在语法上称为“暂时性死区”temporal dead zone简称TDZ
```javascript
if (true) {
// TDZ开始
tmp = 'abc'; // ReferenceError
@ -113,7 +106,6 @@ if (true) {
tmp = 123;
console.log(tmp); // 123
}
```
上面代码中在let命令声明变量tmp之前都属于变量tmp的“死区”。
@ -121,13 +113,11 @@ if (true) {
有些“死区”比较隐蔽,不太容易发现。
```javascript
function bar(x=y, y=2) {
return [x, y];
}
bar(); // 报错
```
上面代码中调用bar函数之所以报错是因为参数x默认值等于另一个参数y而此时y还没有声明属于”死区“。
@ -135,7 +125,6 @@ bar(); // 报错
需要注意的是函数的作用域是其声明时所在的作用域。如果函数A的参数是函数B那么函数B的作用域不是函数A。
```javascript
let foo = 'outer';
function bar(func = x => foo) {
@ -144,7 +133,6 @@ function bar(func = x => foo) {
}
bar();
```
上面代码中函数bar的参数func默认是一个匿名函数返回值为变量foo。这个匿名函数的作用域就不是bar。这个匿名函数声明时是处在外层作用域所以内部的foo指向函数体外的声明输出outer。它实际上等同于下面的代码。
@ -167,7 +155,6 @@ bar();
let不允许在相同作用域内重复声明同一个变量。
```javascript
// 报错
{
let a = 10;
@ -179,13 +166,11 @@ let不允许在相同作用域内重复声明同一个变量。
let a = 10;
let a = 1;
}
```
因此,不能在函数内部重新声明参数。
```javascript
function func(arg) {
let arg; // 报错
}
@ -195,7 +180,6 @@ function func(arg) {
let arg; // 不报错
}
}
```
## 块级作用域
@ -259,7 +243,6 @@ function f() { console.log('I am outside!'); }
const也用来声明变量但是声明的是常量。一旦声明常量的值就不能改变。
```javascript
const PI = 3.1415;
PI // 3.1415
@ -268,7 +251,6 @@ PI // 3.1415
const PI = 3.1;
PI // 3.1415
```
上面代码表明改变常量的值是不起作用的。需要注意的是,对常量重新赋值不会报错,只会默默地失败。
@ -276,24 +258,20 @@ PI // 3.1415
const的作用域与let命令相同只在声明所在的块级作用域内有效。
```javascript
if (true) {
const MAX = 5;
}
// 常量MAX在此处不可得
```
const命令也不存在提升只能在声明的位置后面使用。
```javascript
if (true) {
console.log(MAX); // ReferenceError
const MAX = 5;
}
```
上面代码在常量MAX声明之前就调用结果报错。
@ -301,20 +279,17 @@ if (true) {
const声明的常量也与let一样不可重复声明。
```javascript
var message = "Hello!";
let age = 25;
// 以下两行都会报错
const message = "Goodbye!";
const age = 30;
```
由于const命令只是指向变量所在的地址所以将一个对象声明为常量必须非常小心。
```javascript
const foo = {};
foo.prop = 123;
@ -322,7 +297,6 @@ foo.prop
// 123
foo = {} // 不起作用
```
上面代码中常量foo储存的是一个地址这个地址指向一个对象。不可变的只是这个地址即不能把foo指向另一个地址但对象本身是可变的所以依然可以为其添加新属性。
@ -360,6 +334,27 @@ var constantize = (obj) => {
};
```
## 跨模块常量
上面说过const声明的常量只在当前代码块有效。如果想设置跨模块的常量可以采用下面的写法。
```javascript
// constants.js 模块
export const A = 1;
export const B = 3;
export const C = 4;
// test1.js 模块
import * as constants from './constants';
console.log(constants.A); // 1
console.log(constants.B); // 3
// test2.js 模块
import {A, B} from './constants';
console.log(A); // 1
console.log(B); // 3
```
## 全局对象的属性
全局对象是最顶层的对象在浏览器环境指的是window对象在Node.js指的是global对象。在JavaScript语言中所有全局变量都是全局对象的属性。

View File

@ -113,6 +113,7 @@
- Dave Herman, [Static module resolution](http://calculist.org/blog/2012/06/29/static-module-resolution/): ES6模块的静态化设计思想
- Axel Rauschmayer, [ECMAScript 6: new OOP features besides classes](http://www.2ality.com/2014/12/es6-oop.html)
- Axel Rauschmayer, [Classes in ECMAScript 6 (final semantics)](http://www.2ality.com/2015/02/es6-classes-final.html): Class语法的详细介绍和设计思想分析
- Maximiliano Fierro, [Declarative vs Imperative](http://elmasse.github.io/js/decorators-bindings-es7.html): Decorators介绍
## 工具