时间:2019-08-08
编辑:网站制作公司
5075
0
在本教程中,我们将介绍构建JavaScript API的一些技巧。我们将链接用作面向对象的方法,并将组合用作功能方法。
最后,长春做网站我们将构建一个帮助我们跨越两个世界的帮助器,并拥有一个满足各种用途的API。
可以使用面向对象的方法在JavaScript中实现Fluent接口。你有一个构造函数,其原型包含总是返回实例的方法,因此我们可以创建一个操作链,从而“流畅”。换一种说法:
1 2 3 4 五 6 7 8 9 10 11 12 | function Constructor(){} Constructor.protototype.method = function(x) { // do something return this } Constructor.prototype.otherMethod = function(x) { // do something else return this } // Usage: var instance = new Constructor instance.method().otherMethod() |
我们已经可以看到一种模式:return this。我们可以通过创建一个为我们完成工作的函数装饰器来进一步抽象它,并且它以更具声明性的方式告诉我们关于我们流畅的界面。函数装饰器是高阶函数; 一个函数,它接受一个函数作为参数,并返回该函数的修改版本,在这种情况下,我们只想添加始终的能力return this,这就是我们的“装饰”:
1 2 3 4 五 6 | var fluent = function(f) { return function() { f.apply(this, arguments) return this } } |
现在我们的意图可以更具表现力:
1 2 3 | Constructor.prototype.method = fluent(function(x) { // do something and `return this` }) |
为了改进我们以前的代码,并避免将变量泄漏到全局范围,最后一步是将所有内容包装在一个立即调用的函数表达式(IIFE)中:
1 2 3 4 五 6 | var Constructor = (function(){ function Constructor(){} Constructor.prototype.method = function(){} ... return Constructor }()) |
好,现在我们有一个很好的起点。我们在实践中使用它。让我们创建一个简单的计算器。首先,我们设置IIFE和构造函数:
1 2 3 4 五 6 7 | var Calc = (function(){ function Calc(num) { this.num = num } // methods here return Calc }()) |
接下来,我们需要一些操作。让我们保持它的简单,只需4个操作:add,subtract,multiply和divide:
1 2 3 4 | Calc.prototype.add = fluent(function(x){this.num += x}) Calc.prototype.sub = fluent(function(x){this.num -= x}) Calc.prototype.mul = fluent(function(x){this.num *= x}) Calc.prototype.div = fluent(function(x){this.num /= x}) |
现在我们有一个流畅的计算器界面。我们创建一个传递数字的新实例,然后我们调用链中的方法:
1 2 3 | var calc = new Calc(2) calc.add(1).mul(2).num //=> 6 |
那很简单!这种模式是众所周知的; 诸如jQuery,D3等库已成功使用它。接下来,我们将看到针对同一问题的替代功能方法。
在函数式编程中,数据必须是预先的,这意味着我们不应该将事物封装在对象后面,而是尽可能地将事物公之于众。我们需要容纳我们的函数来处理数据结构,而不是在闭合状态框中交互的模型对象。
让我们创建相同的计算器示例,但这次我们将构建一个功能API。使用组合类似于链接。通过一点一厢情愿的想法,我们可以看到他们如何相互比较:
1 2 3 4 五 | // chaining new Calc(2).add(1).mul(2) // composition compose(mul(2), add(1))(2) |
正如你所看到的,组合是从右到左表达的,这是因为这就是函数自然构成的方式。但好消息是,如果我们愿意,我们也可以反过来表达它。给定一个compose帮助器(例如,你可以使用Underscore),我们可以翻转它的参数。翻转是函数式编程中非常常见的事情,因为有时参数的顺序不正确而且函数无法组合。让我们创建一个flip帮助程序,它将适用于可变函数(带有任意数量参数的函数):
1 2 3 4 五 | var flipN = function(f) { return function() { return f.apply(this, [].slice.call(arguments).reverse()) } } |
在上面,我们从arguments对象创建一个真实数组,我们只需将其反转,然后应用该函数。现在我们可以创建反向compose,我们称之为sequence:
1 2 3 4 五 6 | // composition compose(mul(2), add(1))(2) // sequencing var sequence = flipN(compose) sequence(add(1), mul(2))(2) |
两者都会产生相同的结果,但最后一个看起来更自然; 它更贴近一个适合人类的流畅API。
现在,这些add(1)和mul(2)函数调用是什么?答案在理论上比在实践中要复杂得多,所以让我们从困难的部分开始。在函数式编程中,我们努力实现高抽象,以及组合良好的事物。但是,组合仅适用于一个参数的函数,因为函数只能返回一个参数。那么我们如何解决这个问题呢?解决方案是curry。一个带有2个或更多参数的函数可以转换为一次接受一个参数的函数,并返回一个带有下一个参数的函数,直到所有参数都被采用,此时它将返回最终值。
我们可以使用add函数构建一个简单的示例:
1 2 3 4 五 6 7 8 9 10 11 12 13 14 15 | // no currying function add(x, y) { return x + y } add(1, 2) //=> 3 // currying function add(x) { return function(y) { return x + y } } add(1)(2) //=> 3 |
如您所见,现在调用该函数有点麻烦,因为它一次只接受一个参数。这在实践中并不是什么大问题,幸运的是,由于JavaScript具有可变参数,我们可以构建一个通用的curry帮助程序,它将为我们提供一个函数的curried版本,我们可以使用多个参数调用,或者一次调用一个参数。我们不会curry为了本教程的目的而实现,但您可以使用LoDash或我自己的实现:
1 2 3 4 五 | var add = curry(function add(x, y){return x + y}) // works both ways add(1, 2) //=> 3 add(1)(2) //=> 3 |
现在让我们停下来思考一下。正如您所知,组成高度依赖于参数的顺序。OO方法中的操作接收器始终是第一位的,例如:
1 | instance.add(1) // `instance` is the receiver |
但是通过组合,作为一般规则,我们希望最后有接收器(这只是一个参数):
1 | var add = curry(function add(x, y){return y + x}) |
当然,通过加法(和乘法),顺序无关紧要,因为它们不仅是关联的,而且是交换操作。
考虑到这一点,我们可以完成计算器API:
1 2 3 4 | var add = curry(function add(x, y){return y + x}) var sub = curry(function add(x, y){return y - x}) var mul = curry(function add(x, y){return y * x}) var div = curry(function add(x, y){return y / x}) |
到目前为止,我们创建的这些函数都存在于全局范围内,而封装了OO方法。要将所有函数分组到一个命名空间下,我们可以再次使用IIFE:
1 2 3 4 五 6 7 8 9 | var calc = (function(){ // code here return { add: add, sub: sub, mul: mul, div: div } }()) |
现在我们使用它像:
1 | sequence(calc.add(1), calc.mul(2))(2) //=> 6 |
您还可以使用extends帮助程序将函数导入全局作用域,例如jQuery提供的帮助程序:
1 | extend(window, calc) |
我们已经看到了如何使用链接和组合来构建API,但是如果我们能够拥有两全其美的呢?我们想要更高的抽象,以及FP方法给我们的可重用性,但是如果我们正在使用其他OO库,我们也可能想要使用链接来保持一致性。实际上这很简单。由于我们的FP方法将接收器放在最后,我们可以利用可变参数函数将该参数切片,并使其成为流畅接口的接收者:
1 2 3 4 五 6 7 8 9 10 | var Chainable = function (ctor, methods, val) { val = val || 'val' Object.keys(methods).forEach(function(k) { ctor.prototype[k] = fluent(function() { var as = [].slice.call(arguments) this[val] = methods[k].apply(this, as.concat([this[val]])) }) }) return ctor } |
有了这个帮助器,我们可以使用组合构建我们的API,然后,当一切正常时,我们只需动态生成流畅的API:
1 2 3 4 五 6 7 8 9 10 11 | var MyCalc = Chainable( function Calc(num) { this.num = num }, { add: calc.add, sub: calc.sub, mul: calc.mul, div: calc.div }, 'num' ) |
现在您可以像我们在OO方法中创建的版本一样使用它:
1 | new MyCalc(2).add(1).mul(2).num //=> 6 |
1 2 3 4 五 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 三十 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | // Helpers var fluent = function(f) { return function() { f.apply(this, arguments) return this } } var flipN = function(f) { return function() { return f.apply(this, [].slice.call(arguments).reverse()) } } var curryN = function(n, f, as) { as = as || [] return function() { var bs = as.concat([].slice.call(arguments)) if (bs.length < n) { return curryN(n, f, bs) } return f.apply(null, bs) } } var curry = function(f) { return function() { var as = [].slice.call(arguments) if (f.length > as.length) { return curryN(f.length, f, as) } return f.apply(null, as) } } var compose = function() { return [].reduce.call(arguments, function(f, g) { return function() { return f(g.apply(this, arguments)) } }) } var sequence = flipN(compose) // OO - Fluent Interface var Calc = (function(){ function Calc(num) { this.num = num } Calc.prototype.add = fluent(function(x){this.num += x}) Calc.prototype.sub = fluent(function(x){this.num -= x}) Calc.prototype.mul = fluent(function(x){this.num *= x}) Calc.prototype.div = fluent(function(x){this.num /= x}) return Calc }()) // FP - Composition var calc = (function(){ var add = curry(function add(x, y){return y + x}) var sub = curry(function add(x, y){return y - x}) var mul = curry(function add(x, y){return y * x}) var div = curry(function add(x, y){return y / x}) return { add: add, sub: sub, mul: mul, div: div } }()) // FP->OO Bridge var Chainable = function (ctor, methods, val) { val = val || 'val' Object.keys(methods).forEach(function(k) { ctor.prototype[k] = fluent(function() { var as = [].slice.call(arguments) this[val] = methods[k].apply(this, as.concat([this[val]])) }) }) return ctor } // Example console.log(new Calc(2).add(1).mul(2).num) //=> 6 console.log(sequence(calc.add(1), calc.mul(2))(2)) //=> 6 // Build a chainable interface var MyCalc = Chainable( function Calc(num) { this.num = num }, { add: calc.add, sub: calc.sub, mul: calc.mul, div: calc.div }, 'num' ) console.log(new MyCalc(2).add(1).mul(2).num) //=> 6 |
组合和流畅的接口是我们在JavaScript中构建API的两种方式。从组合到流畅界面的能力很容易使组合比链接更有优势,因为相反的做法并不是那么简单,尽管可能。
长春做网站
3
s后返回登录3
s后返回登录