JavaScript 组合函数

JavaScript compose functions

我正在阅读一本书,其中包含以下示例:

var composition1 = function(f, g) {
  return function(x) {
    return f(g(x));
  }
};

然后作者写道:“...天真的组合实现,因为它没有考虑执行上下文...”

所以首选函数是那个:

var composition2 = function(f, g) {
  return function() {
    return f.call(this, g.apply(this, arguments));
  }
}; 

后面是一个完整的例子:

var composition2 = function composition2(f, g) {
    return function() {
        return f.call(this, g.apply(this, arguments));
    }
};

var addFour = function addFour(x) {
    return x + 4;
};

var timesSeven = function timesSeven(x) {
    return x * 7;
};

var addFourtimesSeven2 = composition2(timesSeven, addFour);
var result2 = addFourtimesSeven2(2);
console.log(result2);

有人可以向我解释为什么 composition2 函数是首选函数吗(也许有一个例子)?

编辑:

在此期间,我尝试按照建议使用方法作为参数,但没有成功。结果是 NaN:

var composition1 = function composition1(f, g) {
    return function(x) {
        return f(g(x));
    };
};

var composition2 = function composition2(f, g) {
    return function() {
        return f.call(this, g.apply(this, arguments));
    }
};

var addFour = {
    myMethod: function addFour(x) {
        return x + this.number;
    },
    number: 4
};

var timesSeven = {
    myMethod: function timesSeven(x) {
        return x * this.number;
    },
    number: 7
};

var addFourtimesSeven1 = composition1(timesSeven.myMethod, addFour.myMethod);
var result1 = addFourtimesSeven1(2);
console.log(result1);

var addFourtimesSeven2 = composition2(timesSeven.myMethod, addFour.myMethod);
var result2 = addFourtimesSeven2(2);
console.log(result2);

composition1 不会将第一个参数以外的参数传递给 g()

如果你这样做:

var composition1 = function(f, g) {
  return function(x1, x2, x3) {
    return f(g(x1, x2, x3));
  }
};

该函数适用于前三个参数。但是,如果您希望它适用于任意数字,则需要使用 Function.prototype.apply.

f.call(...) 用于设置 this,如 Caramiriel 的回答所示。

这只是回答了 composition2 的实际作用:

composition2 当您想将 this 保留为函数本身的上下文时使用。以下示例显示使用 data.adata.b 结果为 60

'use strict';

var multiply = function(value) {
    return value * this.a;
}
var add = function(value) {
    return value + this.b;
}

var data = {
    a: 10,
    b: 4,
    func: composition2(multiply, add)
};

var result = data.func(2);
// uses 'data' as 'this' inside the 'add' and 'multiply' functions
// (2 + 4) * 10 = 60

但是,它仍然打破了下面的例子(不幸的是):

'use strict';

function Foo() {
    this.a = 10;
    this.b = 4;
}
Foo.prototype.multiply = function(value) {
    return value * this.a;
};
Foo.prototype.add = function(value) {
    return value + this.b;
};


var foo = new Foo();

var func = composition2(foo.multiply, foo.add);
var result = func(2); // Uncaught TypeError: Cannot read property 'b' of undefined

因为 composition2 (this) 的上下文是未定义的(并且没有以任何其他方式调用,例如 .apply.callobj.func()),你最终会得到 this 在函数中也是未定义的。

另一方面,我们可以使用以下代码为其提供另一个上下文:

'use strict';
var foo = new Foo();

var data = {
    a: 20,
    b: 8,
    func: composition2(foo.multiply, foo.add)
}

var result = data.func(2); 
// uses 'data' as 'this'
// (2 + 8) * 10 = 200 :)

或者通过显式设置上下文:

'use strict';

var multiply = function(value) {
    return value * this.a;
};
var add = function(value) {
    return value + this.b;
};


var a = 20;
var b = 8;

var func = composition2(multiply, add);

// All the same
var result1 = this.func(2);
var result2 = func.call(this, 2);
var result3 = func.apply(this, [2]);

我不同意作者的观点。

想想功能组合的用例。大多数时候,我将函数组合用于转换函数(纯函数;参数输入、结果输出和 this 无关紧要)。

第二。按照他的方式使用 arguments 会导致糟糕的 practice/dead 结局,因为这意味着函数 g() 可能依赖于多个参数。

这意味着,我创建的组合不再是可组合的,因为它可能无法获得所需的所有参数。
阻止组合的组合;失败

(作为副作用:将参数对象传递给任何其他函数是性能上的禁区,因为 JS 引擎无法再对其进行优化)

看看partial application的话题,在JS中通常被误引用为currying,基本上是:除非传递所有参数,否则函数returns另一个函数接受剩余的参数;直到我有我需要处理的所有参数。

那么您应该重新考虑实现参数顺序的方式,因为当您将它们定义为配置优先、数据最后时,这种方式效果最好。
示例:

//a transformer: value in, lowercased string out
var toLowerCase = function(str){
    return String(str).toLowerCase();
}

//the original function expects 3 arguments, 
//two configs and the data to process.
var replace = curry(function(needle, heystack, str){
    return String(str).replace(needle, heystack);
});

//now I pass a partially applied function to map() that only 
//needs the data to process; this is really composable
arr.map( replace(/\s[A-Z]/g, toLowerCase) );

//or I create another utility by only applying the first argument
var replaceWhitespaceWith = replace(/\s+/g);
//and pass the remaining configs later
arr.map( replaceWhitespaceWith("-") );

一种略有不同的方法是创建函数,这些函数在设计上并不是要一步传递所有参数,而是逐个(或有意义的组)传递

var prepend = a => b => String(a) + String(b);  //one by one
var substr = (from, to) => value => String(str).substr(from, to);  //or grouped

arr.map( compose( prepend("foo"), substr(0, 5) ) );
arr.map( compose( prepend("bar"), substr(5) ) );
//and the `to`-argument is undefined; by intent

我不打算用所有参数调用此类函数,我只想传递它们的配置,并获得一个函数来完成传递的 data/value.

而不是 substr(0, 5, someString),我总是写 someString.substr(0, 5),那么为什么要努力使最后一个参数(数据)在第一次调用中适用?