mirror of
https://github.com/ruanyf/es6tutorial.git
synced 2025-05-28 21:32:20 +00:00
edit class
This commit is contained in:
parent
3503aae7f6
commit
a9714aef53
264
docs/async.md
264
docs/async.md
@ -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语句后面。
|
||||
|
@ -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 + ')';
|
||||
// }
|
||||
|
||||
```
|
||||
|
||||
**(2)constructor方法**
|
||||
@ -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模块高。
|
||||
|
Loading…
x
Reference in New Issue
Block a user