diff --git a/docs/async.md b/docs/async.md index 40c5b9e..529b6e7 100644 --- a/docs/async.md +++ b/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语句后面。 diff --git a/docs/class.md b/docs/class.md index dfbde5c..9d869e1 100644 --- a/docs/class.md +++ b/docs/class.md @@ -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模块高。