Detailed explanation of JS function currying

The first time I saw the word currying, I was reading an algorithm-related blog that mentioned currying functions. At that time, I felt that this word was very high-end. In fact, when you understand it, you realize that it is actually It is a special use of higher-order functions.

As expected, no matter what the function is, it must have a high-end name to be useful.

First, let’s look at what exactly currying is?

Wikipedia says: Currying, English: Currying (really full of sense of déjà vu in English translation), is to transform a function that accepts multiple parameters into one that accepts a single parameter (the first parameter of the original function) function and return a new function that accepts the remaining parameters and returns the result.

Seeing that this explanation is a bit abstract, let's take the add function, which has been demonstrated countless times, to make a simple implementation.

 

// 普通的add函数
function add(x, y) {
    return x + y
}

// Currying后
function curryingAdd(x) {
    return function (y) {
        return x + y
    }
}

add(1, 2)           // 3
curryingAdd(1)(2)   // 3

In fact, the two parameters of x and y of the add function are changed into using a function to receive x and then returning a function to process the y parameter. Now the idea should be clearer, which is to call the function by passing only part of the parameters and let it return a function to process the remaining parameters.

But the question arises, what is the use of spending so much effort to encapsulate it? It is impossible for us programmers to do more things without benefit, even in this life.

Let’s list the benefits of Currying?

1. Parameter reuse

 

// 正常正则验证字符串 reg.test(txt)

// 函数封装后
function check(reg, txt) {
    return reg.test(txt)
}

check(/\d+/g, 'test')       //false
check(/[a-z]+/g, 'test')    //true

// Currying后
function curryingCheck(reg) {
    return function(txt) {
        return reg.test(txt)
    }
}

var hasNumber = curryingCheck(/\d+/g)
var hasLetter = curryingCheck(/[a-z]+/g)

hasNumber('test1')      // true
hasNumber('testtest')   // false
hasLetter('21212')      // false

The above example is a regular check. Normally, just call the check function directly. However, if I have to check whether there are numbers in many places, I actually need to reuse the first parameter reg, so that where you can directly call hasNumber, hasLetter and other functions, so that the parameters can be reused and the call is more convenient.

2. Confirm in advance

 

var on = function(element, event, handler) {
    if (document.addEventListener) {
        if (element && event && handler) {
            element.addEventListener(event, handler, false);
        }
    } else {
        if (element && event && handler) {
            element.attachEvent('on' + event, handler);
        }
    }
}

var on = (function() {
    if (document.addEventListener) {
        return function(element, event, handler) {
            if (element && event && handler) {
                element.addEventListener(event, handler, false);
            }
        };
    } else {
        return function(element, event, handler) {
            if (element && event && handler) {
                element.attachEvent('on' + event, handler);
            }
        };
    }
})();

//换一种写法可能比较好理解一点,上面就是把isSupport这个参数给先确定下来了
var on = function(isSupport, element, event, handler) {
    isSupport = isSupport || document.addEventListener;
    if (isSupport) {
        return element.addEventListener(event, handler, false);
    } else {
        return element.attachEvent('on' + event, handler);
    }
}

When we are working on projects, it is very common to encapsulate some DOM operations. The first way of writing above is also relatively common, but let's take a look at the second way of writing. It is relatively different. The first way of writing is to self-execute and then return a new function, this actually determines which method will be taken in advance to avoid making judgments every time.

3. Delayed operation

 

Function.prototype.bind = function (context) {
    var _this = this
    var args = Array.prototype.slice.call(arguments, 1)
 
    return function() {
        return _this.apply(context, args)
    }
}

Like bind, which is often used in our js, the implementation mechanism is Currying.

After talking about these advantages, I found that there is still a problem. Do I need to modify the underlying function every time I use Currying?

Is there any general packaging method?

 

// 初步封装
var currying = function(fn) {
    // args 获取第一个方法内的全部参数
    var args = Array.prototype.slice.call(arguments, 1)
    return function() {
        // 将后面方法里的全部参数和args进行合并
        var newArgs = args.concat(Array.prototype.slice.call(arguments))
        // 把合并后的参数通过apply作为fn的参数并执行
        return fn.apply(this, newArgs)
    }
}

Here is the preliminary encapsulation first, saving the preliminary parameters through closure, then getting the remaining arguments for splicing, and finally executing the function that requires currying.

But there seems to be some flaw. If you return like this, you can only expand one more parameter. Currying(a)(b)(c) seems to be not supported (multi-parameter calls are not supported). Generally, this kind of situation will come to mind. Use recursion to encapsulate another layer.

 

// 支持多参数传递
function progressCurrying(fn, args) {

    var _this = this
    var len = fn.length;
    var args = args || [];

    return function() {
        var _args = Array.prototype.slice.call(arguments);
        Array.prototype.push.apply(args, _args);

        // 如果参数个数小于最初的fn.length,则递归调用,继续收集参数
        if (_args.length < len) {
            return progressCurrying.call(_this, fn, _args);
        }

        // 参数收集完毕,则执行fn
        return fn.apply(this, _args);
    }
}

This is actually a preliminary basis, with the addition of recursive calls. As long as the number of parameters is less than the initial fn.length, the recursion will continue to be executed.

Now that we have finished talking about the benefits and general methods are available, let us focus on the performance of curry.

You only need to know the following four points about some performance issues of curry:

  • Accessing arguments objects is generally slower than accessing named parameters.
  • Some older browsers are quite slow in implementing arguments.length
  • Using fn.apply(…) and fn.call(…) is usually slightly slower than calling fn(…) directly.
  • Creating lots of nested scopes and closures comes with an expense, both in terms of memory and speed.

In fact, in most applications, the main performance bottleneck is operating DOM nodes. The performance loss of js is basically negligible, so curry can be used directly and safely.

Finally, expand on a classic interview question

 

// 实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;

function add() {
    // 第一次执行时,定义一个数组专门用来存储所有的参数
    var _args = Array.prototype.slice.call(arguments);

    // 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
    var _adder = function() {
        _args.push(...arguments);
        return _adder;
    };

    // 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
    _adder.toString = function () {
        return _args.reduce(function (a, b) {
            return a + b;
        });
    }
    return _adder;
}

add(1)(2)(3)                // 6
add(1, 2, 3)(4)             // 10
add(1)(2)(3)(4)(5)          // 15
add(2, 6)(1)                // 9





 

Guess you like

Origin blog.csdn.net/ljy_1024/article/details/114011654