1
0
mirror of https://github.com/ruanyf/es6tutorial.git synced 2025-05-28 21:32:20 +00:00

edit class

This commit is contained in:
Ruan Yifeng 2015-06-15 11:29:50 +08:00
parent 3503aae7f6
commit a9714aef53
2 changed files with 304 additions and 19 deletions

View File

@ -202,6 +202,270 @@ result.value.then(function(data){
可以看到,虽然 Generator 函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。
## Thunk函数
### 参数的求值策略
Thunk函数早在上个世纪60年代就诞生了。
那时,编程语言刚刚起步,计算机学家还在研究,编译器怎么写比较好。一个争论的焦点是"求值策略",即函数的参数到底应该何时求值。
```javascript
var x = 1;
function f(m){
return m * 2;
}
f(x + 5)
```
上面代码先定义函数f然后向它传入表达式`x + 5`。请问,这个表达式应该何时求值?
一种意见是"传值调用"call by value即在进入函数体之前就计算`x + 5`的值等于6再将这个值传入函数f 。C语言就采用这种策略。
```javascript
f(x + 5)
// 传值调用时,等同于
f(6)
```
另一种意见是"传名调用"call by name即直接将表达式`x + 5`传入函数体只在用到它的时候求值。Hskell语言采用这种策略。
```javascript
f(x + 5)
// 传名调用时,等同于
(x + 5) * 2
```
传值调用和传名调用,哪一种比较好?回答是各有利弊。传值调用比较简单,但是对参数求值的时候,实际上还没用到这个参数,有可能造成性能损失。
```javascript
function f(a, b){
return b;
}
f(3 * x * x - 2 * x - 1, x);
```
上面代码中函数f的第一个参数是一个复杂的表达式但是函数体内根本没用到。对这个参数求值实际上是不必要的。因此有一些计算机学家倾向于"传名调用",即只在执行时求值。
### Thunk函数的含义
编译器的"传名调用"实现往往是将参数放到一个临时函数之中再将这个临时函数传入函数体。这个临时函数就叫做Thunk函数。
```javascript
function f(m){
return m * 2;
}
f(x + 5);
// 等同于
var thunk = function () {
return x + 5;
};
function f(thunk){
return thunk() * 2;
}
```
上面代码中函数f的参数`x + 5`被一个函数替换了。凡是用到原参数的地方,对`Thunk`函数求值即可。
这就是Thunk函数的定义它是"传名调用"的一种实现策略,用来替换某个表达式。
### JavaScript语言的Thunk函数
JavaScript语言是传值调用它的Thunk函数含义有所不同。在JavaScript语言中Thunk函数替换的不是表达式而是多参数函数将其替换成单参数的版本且只接受回调函数作为参数。
```javascript
// 正常版本的readFile多参数版本
fs.readFile(fileName, callback);
// Thunk版本的readFile单参数版本
var readFileThunk = Thunk(fileName);
readFileThunk(callback);
var Thunk = function (fileName){
return function (callback){
return fs.readFile(fileName, callback);
};
};
```
上面代码中fs模块的readFile方法是一个多参数函数两个参数分别为文件名和回调函数。经过转换器处理它变成了一个单参数函数只接受回调函数作为参数。这个单参数版本就叫做Thunk函数。
任何函数只要参数有回调函数就能写成Thunk函数的形式。下面是一个简单的Thunk函数转换器。
```javascript
var Thunk = function(fn){
return function (){
var args = Array.prototype.slice.call(arguments);
return function (callback){
args.push(callback);
return fn.apply(this, args);
}
};
};
```
使用上面的转换器,生成`fs.readFile`的Thunk函数。
```javascript
var readFileThunk = Thunk(fs.readFile);
readFileThunk(fileA)(callback);
```
### Thunkify模块
生产环境的转换器建议使用Thunkify模块。
首先是安装。
```bash
$ npm install thunkify
```
使用方式如下。
```javascript
var thunkify = require('thunkify');
var fs = require('fs');
var read = thunkify(fs.readFile);
read('package.json')(function(err, str){
// ...
});
```
Thunkify的源码与上一节那个简单的转换器非常像。
```javascript
function thunkify(fn){
return function(){
var args = new Array(arguments.length);
var ctx = this;
for(var i = 0; i < args.length; ++i) {
args[i] = arguments[i];
}
return function(done){
var called;
args.push(function(){
if (called) return;
called = true;
done.apply(null, arguments);
});
try {
fn.apply(ctx, args);
} catch (err) {
done(err);
}
}
}
};
```
它的源码主要多了一个检查机制变量called确保回调函数只运行一次。这样的设计与下文的Generator函数相关。请看下面的例子。
```javascript
function f(a, b, callback){
var sum = a + b;
callback(sum);
callback(sum);
}
var ft = thunkify(f);
ft(1, 2)(console.log);
// 3
```
上面代码中由于thunkify只允许回调函数执行一次所以只输出一行结果。
### Generator 函数的流程管理
你可能会问, Thunk函数有什么用回答是以前确实没什么用但是ES6有了Generator函数Thunk函数现在可以用于Generator函数的自动流程管理。
以读取文件为例。下面的Generator函数封装了两个异步操作。
```javascript
var fs = require('fs');
var thunkify = require('thunkify');
var readFile = thunkify(fs.readFile);
var gen = function* (){
var r1 = yield readFile('/etc/fstab');
console.log(r1.toString());
var r2 = yield readFile('/etc/shells');
console.log(r2.toString());
};
```
上面代码中yield命令用于将程序的执行权移出Generator函数那么就需要一种方法将执行权再交还给Generator函数。
这种方法就是Thunk函数因为它可以在回调函数里将执行权交还给Generator函数。为了便于理解我们先看如何手动执行上面这个Generator函数。
```javascript
var g = gen();
var r1 = g.next();
r1.value(function(err, data){
if (err) throw err;
var r2 = g.next(data);
r2.value(function(err, data){
if (err) throw err;
g.next(data);
});
});
```
上面代码中变量g是Generator函数的内部指针表示目前执行到哪一步。next方法负责将指针移动到下一步并返回该步的信息value属性和done属性
仔细查看上面的代码可以发现Generator函数的执行过程其实是将同一个回调函数反复传入next方法的value属性。这使得我们可以用递归来自动完成这个过程。
### Thunk函数的自动流程管理
Thunk函数真正的威力在于可以自动执行Generator函数。下面就是一个基于Thunk函数的Generator执行器。
```javascript
function run(fn) {
var gen = fn();
function next(err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next);
}
next();
}
run(gen);
```
上面代码的run函数就是一个Generator函数的自动执行器。内部的next函数就是Thunk的回调函数。next函数先将指针移到Generator函数的下一步gen.next方法然后判断Generator函数是否结束result.done 属性如果没结束就将next函数再传入Thunk函数result.value属性否则就直接退出。
有了这个执行器执行Generator函数方便多了。不管有多少个异步操作直接传入run函数即可。当然前提是每一个异步操作都要是Thunk函数也就是说跟在yield命令后面的必须是Thunk函数。
```javascript
var gen = function* (){
var f1 = yield readFile('fileA');
var f2 = yield readFile('fileB');
// ...
var fn = yield readFile('fileN');
};
run(gen);
```
上面代码中函数gen封装了n个异步的读取文件操作只要执行run函数这些操作就会自动完成。这样一来异步操作不仅可以写得像同步操作而且一行代码就可以执行。
Thunk函数并不是Generator函数自动执行的唯一方案。因为自动执行的关键是必须有一种机制自动控制Generator函数的流程接收和交还程序的执行权。回调函数可以做到这一点Promise 对象也可以做到这一点。
## co函数库
如果并发执行异步操作可以将异步操作都放入一个数组跟在yield语句后面。

View File

@ -41,17 +41,50 @@ class Point {
Point类除了构造方法还定义了一个toString方法。注意定义“类”的方法的时候前面不需要加上function这个保留字直接把函数定义放进去了就可以了。
构造函数的prototype属性在ES6的“类”上面继续存在。除了constructor方法以外类的方法都定义在类的prototype属性上面。prototype对象的constructor属性直接指向“类”的本身。
构造函数的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
Point.prototype.toString
// function toString() {
// return '(' + this.x + ', ' + this.y + ')';
// }
```
**2constructor方法**
@ -82,19 +115,16 @@ new Foo() instanceof Foo
生成实例对象的写法与ES5完全一样也是使用new命令。如果忘记加上new像函数那样调用Class将会报错。
```javascript
// 报错
var point = Point(2, 3);
// 正确
var point = new Point(2, 3);
```
与ES5一样实例的属性除非显式定义在其本身即定义在this对象上否则都是定义在原型上即定义在class上
```javascript
//定义类
class Point {
@ -117,19 +147,16 @@ 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\_\_属性是相等的。
@ -137,7 +164,6 @@ p1.__proto__ === p2.__proto__
这也意味着,可以通过\_\_proto\_\_属性为Class添加方法。
```javascript
var p1 = new Point(2,3);
var p2 = new Point(3,2);
@ -148,7 +174,6 @@ p2.printName() // "Oops"
var p3 = new Point(4,2);
p3.printName() // "Oops"
```
上面代码在p1的原型上添加了一个printName方法由于p1的原型就是p2的原型因此p2也可以调用这个方法。而且此后新建的实例p3也可以调用这个方法。这意味着使用实例的\_\_proto\_\_属性改写原型必须相当谨慎不推荐使用因为这会改变Class的原始定义影响到所有实例。
@ -615,17 +640,13 @@ ES6的Class只是面向对象编程的语法糖升级了ES5的对象定义的
ES6模块的设计思想是尽量的静态化使得编译时就能确定模块的依赖关系以及输入和输出的变量。CommonJS和AMD模块都只能在运行时确定这些东西。比如CommonJS模块就是对象输入时必须查找对象属性。
```javascript
var { stat, exists, readFile } = require('fs');
```
ES6模块不是对象而是通过export命令显式指定输出的代码输入时也采用静态命令的形式。
```javascript
import { stat, exists, readFile } from 'fs';
```
所以ES6可以在编译时就完成模块编译效率要比CommonJS模块高。