JS中的高阶函数

186 阅读4分钟

1. 高阶函数的特点

  1. 函数作为参数(回调)
function fn(callback) {
    callback()
}
  1. 函数作为返回值
function fn() {
    return function() {
        
    }
}

只要符合任一特点,此函数就可以称作高阶函数或者高级函数

2. 高级函数的使用场景

1.你可能使用这种方式判断过数据类型

// Array => [object Object]
// Object => [object Object]
// Function => [object Function]
// RegExp => [object RegExp]
// Date => [object Date]
// String => [object String]
// Number => [object Number]
// undefined => [object Undefined]
// null => [object null]
// boolean => [object Boolean]
// Symbol => [object Symbol]
function isArray(value) { // {1}
    return Object.prototype.toString.call(value) === '[object Array]';
}
// 或者
function checkType(value, type) { // {2}
    return Object.prototype.toString.call(value) === `[object ${type}]`;
}
const isString = checkType('abc', 'String');

使用上面的方式,每次检查类型,都要传递两个参数,而且第二个“类型”参数确实很多余,下面我们使用高级函数对检测类型函数进行优化。

function checkType(type) { // {3}
    return function(value) {
        return Object.prototype.toString.call(value) === `[object ${type}]`;
    }
}
const isString = checkType('String');
const isArray = checkType('Array');
isString('abc'); // true;

第{3}种方式,相较于{1}{2}种方式,在代码的复用性和适用性方面明显提高了很多。就是中间嵌套了一层有点难受。我将在函数柯里化章节说明进一步优化方案,主要解决函数嵌套问题。

2.AOP(Aspect-Oriented Programming)面向切片编程

主要是把跟核心无关的代码的抽离出来,其实就是在原函数外层嵌套一层,不用管原函数的实现。

比如,我们要在原函数之前或者之后执行一些逻辑。

function say() {
    console.log('Hello World!');
}
// 我们想要在执行say之前处理一些事情
Function.prototype.before = function(callback) {
    return ()=> {
        callback();
        this();
    }
}
const beforeFn = say.before(function() {
    console.log('say');
})
beforeFn(); // 1.say 2.hello world!;

3.异步代码计数器

在日常开发中,有时候我们需要拿到多个异步代码的结果进行统一归纳,但这些异步代码彼此之前又不相关联,所以为了避免代码难以阅读,我们不能去写一些异步嵌套代码,这个时候,我们可以使用计数器的方式来处理这种场景。

const fs = require('fs');

function after(times, callback) {
    const result = {};
    return function (key, data) {
    	result[key] = data;
    	if (--times === 0) {
    		callback(result);
    	}
    };
}
// after中的回调会在异步执行之后执行
const newFn = after(2, function (result) {
    console.log(result);
});
fs.readFile('./assets/name.txt', 'utf8', function (err, data) {
    if (err) return console.log(err);
    newFn('name', data);
});
fs.readFile('./assets/age.txt', 'utf8', function (err, data) {
    if (err) return console.log(err);
    newFn('age', data);
});

我们也不知道name.txt和age.text文件哪个先读完,所以做个计数器,只有当计数器times为零的时候,才会执行回调函数。将最终结果传入回调函数中。

当然,在ES6以后,完全可以用Promise.all去解决上述问题。其实Promise也是基于回调实现的。我会在后续文章中基于Promise A+规范实现Promise。

3. 函数柯里化

柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数

举个例子:

// js高级程序设计中的例子
function add(num1, num2) {
    return num1 + num2;
}
function curriedAdd(num2) {
    return add(5, num2);
}
console.log(2,3); // 5;
console.log(curriedAdd(3)); //8

好吧,我骗了你,上面的例子curriedAdd并不是柯里化函数,他只是设定了add函数的一个实参。但是我们柯里化函数就是上面例子中展示的减少传参的那种效果。

比如我要柯里化上面提到过的checkType函数:

function checkType(type, value) { // {2}
    return Object.prototype.toString.call(value) === `[object ${type}]`;
}
function currying(fn, arr=[]) {
    const len = fn.length; // 获取原函数参数的个数
    return (...args)=> {
        const params = [...arr, ...args]; //组合参数
        if (params.length < len) { 
            // 传递的参数已经达到原函数参数的个数,就可以执行函数了
            // 否则,递归
            return currying(fn, params)
        }
        return fn(...params);
    }
}
const isString = currying(checkType)('String')(1);
// 还可以这样使用
const isArray = currying(checkType)('Array');
isArray([]);

4. 惰性函数

因为浏览器之间行为存在差异,多数JavaScript代码包含了大量的if语句,将执行引导到正确的代码中。比如判断事件监听。

function addEvent (type, el, fn) {
    if (window.addEventListener) {
        addEvent = function (type, el, fn) {
            el.addEventListener(type, fn, false);
        }
    }
    else if(window.attachEvent){
        addEvent = function (type, el, fn) {
            el.attachEvent('on' + type, fn);
        }
    }
}

每次调用addEvent(),他都要对浏览器所支持的能力仔细检查。如果浏览器支持addEventListener,那么他就一直支持了,那么这种测试就变得没必要了。即使只有一个if语句每页肯定要比没有if语句的慢,所以如果if语句不必每次执行,那么代码可以运行的更快一些。解决方案就是称之为惰性载入的技巧。

惰性载入表示函数执行的分支仅会发生一次。有两种实现惰性载入的方式:

  1. 在函数被调用时再处理函数。在第一次调用的过程中,该函数会被覆盖为另外一个按合适方式执行的函数,这样任何对原函数的调用都不用经过执行的分支了。
function addEvent (type, el, fn) {
    if (window.addEventListener) {
        addEvent = function (type, el, fn) {
            el.addEventListener(type, fn, false);
        }
    }
    else if(window.attachEvent){
        addEvent = function (type, el, fn) {
            el.attachEvent('on' + type, fn);
        }
    }
}

  1. 第二种实现惰性载入的方式是在声明函数时就指定适当的函数。这样,第一次调用函数时就不会损失性能了,而在代码首次加载时会损失一点性能。
const addEvent = (function(){
    if (window.addEventListener) {
        return function (type, el, fn) {
            el.addEventListener(type, fn, false);
        }
    }
    else if(window.attachEvent){
        return function (type, el, fn) {
            el.attachEvent('on' + type, fn);
        }
    }
})();

5.参考文章

  1. JavaScript高级程序设计(第三版)
  2. JavaScript专题之惰性函数