mqyqingfeng / Blog

冴羽写博客的地方,预计写四个系列:JavaScript深入系列、JavaScript专题系列、ES6系列、React系列。

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

JavaScript专题之偏函数

mqyqingfeng opened this issue · comments

定义

维基百科中对偏函数 (Partial application) 的定义为:

In computer science, partial application (or partial function application) refers to the process of fixing a number of arguments to a function, producing another function of smaller arity.

翻译成中文:

在计算机科学中,局部应用是指固定一个函数的一些参数,然后产生另一个更小元的函数。

什么是元?元是指函数参数的个数,比如一个带有两个参数的函数被称为二元函数。

举个简单的例子:

function add(a, b) {
    return a + b;
}

// 执行 add 函数,一次传入两个参数即可
add(1, 2) // 3

// 假设有一个 partial 函数可以做到局部应用
var addOne = partial(add, 1);

addOne(2) // 3

个人觉得翻译成“局部应用”或许更贴切些,以下全部使用“局部应用”。

柯里化与局部应用

如果看过上一篇文章《JavaScript专题之柯里化》,实际上你会发现这个例子和柯里化太像了,所以两者到底是有什么区别呢?

其实也很明显:

柯里化是将一个多参数函数转换成多个单参数函数,也就是将一个 n 元函数转换成 n 个一元函数。

局部应用则是固定一个函数的一个或者多个参数,也就是将一个 n 元函数转换成一个 n - x 元函数。

如果说两者有什么关系的话,引用 functional-programming-jargon 中的描述就是:

Curried functions are automatically partially applied.

partial

我们今天的目的是模仿 underscore 写一个 partial 函数,比起 curry 函数,这个显然简单了很多。

也许你在想我们可以直接使用 bind 呐,举个例子:

function add(a, b) {
    return a + b;
}

var addOne = add.bind(null, 1);

addOne(2) // 3

然而使用 bind 我们还是改变了 this 指向,我们要写一个不改变 this 指向的方法。

第一版

根据之前的表述,我们可以尝试着写出第一版:

// 第一版
// 似曾相识的代码
function partial(fn) {
    var args = [].slice.call(arguments, 1);
    return function() {
        var newArgs = args.concat([].slice.call(arguments));
        return fn.apply(this, newArgs);
    };
};

我们来写个 demo 验证下 this 的指向:

function add(a, b) {
    return a + b + this.value;
}

// var addOne = add.bind(null, 1);
var addOne = partial(add, 1);

var value = 1;
var obj = {
    value: 2,
    addOne: addOne
}
obj.addOne(2); // ???
// 使用 bind 时,结果为 4
// 使用 partial 时,结果为 5

第二版

然而正如 curry 函数可以使用占位符一样,我们希望 partial 函数也可以实现这个功能,我们再来写第二版:

// 第二版
var _ = {};

function partial(fn) {
    var args = [].slice.call(arguments, 1);
    return function() {
        var position = 0, len = args.length;
        for(var i = 0; i < len; i++) {
            args[i] = args[i] === _ ? arguments[position++] : args[i]
        }
        while(position < arguments.length) args.push(arguments[position++]);
        return fn.apply(this, args);
    };
};

我们验证一下:

var subtract = function(a, b) { return b - a; };
subFrom20 = partial(subtract, _, 20);
subFrom20(5);

写在最后

值得注意的是:underscore 和 lodash 都提供了 partial 函数,但只有 lodash 提供了 curry 函数。

专题系列

JavaScript专题系列目录地址:https://github.com/mqyqingfeng/Blog

JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

大佬有公众号吗

@syzq 没有哎~

第二版 while(position < arguments.length) args.push(argumetns[position++]); 中后一个arguments拼成argumetns了

@SilenceZeng 非常感谢指出~ 已修改~ o( ̄▽ ̄)d

总结得很棒!

大佬开个公众号吧,这样可以有更大的动力~~ 赏你一杯咖啡,哈哈~~

@evanzlj 我现在维护三个平台就已经有点忙不过来啦,公众号就更麻烦了,而且我觉得还是 PC 端更适合学习一些,可以边看文章边动手试验代码。

可不可以这样理解,偏函数的一种特殊情况就是curry

那这样说来Function.prototype.bind也是偏函数咯

var _ = {};
args[i] === _
这样写是有什么奥妙么

大佬,这个偏函数是不是有点问题,subFrom20(5),subFrom20(4)都输出15,那个args被污染拉,是不是该拷贝一份

es6简化版

function partial(func, ...argsBound) {
  return function(...args) {
    return func.call(this, ...argsBound, ...args);
  };
}

大佬,这个偏函数是不是有点问题,subFrom20(5),subFrom20(4)都输出15,那个args被污染拉,是不是该拷贝一份

你写错了吧。没有被污染啊

占位符用在里面有什么用

 while(position < arguments.length) args.push(arguments[position++]);

直接换成 下面这样会不会好点

if(position < arguments.length) args = args.concat([].slice.call(arguments, position));

while(position < arguments.length) args.push(arguments[position++])
应该改为:
while(position < arguments.length) args[position]=(arguments[position++]);
@kashtian

@cyhwinner

 while(position < arguments.length) args.push(arguments[position++]);

直接换成 下面这样会不会好点

if(position < arguments.length) args = args.concat([].slice.call(arguments, position));

不一样吧,原文的意思是:将arguments[position]这个值放入到args中;你改后的意思是:将arguments从position位置截取到数组最后一项的所有值放入到args中;原文放入了一个值,你放入了从position位置到最后一个位置的好几个值

强烈建议大佬把博客文章整理一下搞个类似阮一峰老师的ES6微信小程序

您好,我最近一直在学习您编写的文章,在这一章中我实在分辨不出柯里化和偏函数的区别,还有能说明下这两者的应用场景吗?

commented
// 第一版
// 似曾相识的代码
function partial(fn) {
    var args = [].slice.call(arguments, 1);
    return function() {
        var newArgs = args.concat([].slice.call(arguments));
        return fn.apply(this, newArgs);
    };
};

这段就是bind的实现呀 所以可以理解成 Function.prototype.bind函数也是一个偏函数啦

@cyhwinner

 while(position < arguments.length) args.push(arguments[position++]);

直接换成 下面这样会不会好点

if(position < arguments.length) args = args.concat([].slice.call(arguments, position));

不一样吧,原文的意思是:将arguments[position]这个值放入到args中;你改后的意思是:将arguments从position位置截取到数组最后一项的所有值放入到args中;原文放入了一个值,你放入了从position位置到最后一个位置的好几个值

你可能没有注意到 第二个替换 while 改成了 if

使用ts写偏函数,不过类型出于考虑,只用了any

function partical(fn: Function, ...args: any[]): Function {
  return function (...moreArgs: any[]) {
    return fn(...args, ...moreArgs)
  }
}

更加函数式的实现:

const _ = Symbol('_')
const partial = (fn, ...outerArgs) => {
    return function (...innerArgs) {
        return fn.call(this, ...outerArgs.map(arg => arg === _ ? innerArgs.shift() : arg), ...innerArgs)
    }
}
partial._ = _
commented

函数式的实现:

const _ = Symbol('_')
const partial = (fn, ...outerArgs) => {
    return function (...innerArgs) {
        return fn.call(this, ...outerArgs.map(arg => arg === _ ? innerArgs.shift() : arg), ...innerArgs)
    }
}
partial._ = _

我认为应该这样比较好,占位符可以由编写成员自定义

function partial(fn, holder = partial, ...args) {
  return function (...params) {
    return fn.call(this, ...args.map((arg) => (arg === holder ? (arg = params.shift()) : arg)), ...params)
  }
}~~~
这样看起来就完美了

“局部应用”翻译也有问题。逻辑上讲应该是 applied partial function, 只是英语语法调整成了partially applied function。正确翻译应该是 “已被传递(applied)部分(partial)(参数)(被省略)的函数”,简称不如为“部分定参函数”

while(position < arguments.length) args.push(arguments[position++]);
大佬,这个有点看不懂可以解释一下嘛

commented

大佬 bind 和 局部应用 晕了 不太明白呢