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
+264
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语句后面。
+40 -19
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模块高。