JS中的函数种类

函数

记录这篇文章目的主要深入了解js中的函数,虽然js是一个弱类型语言,但是我们也要弄明白函数的种类和功能作用;函数与其他数据类型一样,处于平等地位,可以赋值其他变量,也可以作为参数,传入另一个函数,或者作为其他函数的返回值

一 高阶函数

*在数学和计算机科学中,高阶函数至少满足下列一个条件:

1. 接受一个或者多个函数作为输入。
2. 输出一个函数。

接受一个或多个函数作为输入,即函数作为参数传递。

   // Array.prototype.map 高阶函数
const array = [1, 2, 3, 4];
const map = array.map(x => x * 2); // [2, 4, 6, 8]

// Array.prototype.filter 高阶函数
const words = ['semlinker', 'kakuqo', 'lolo', 'abao'];
const result = words.filter(word => word.length > 5); // ["semlinker", "kakuqo"]

输出一个函数。调用高阶函数之后,会返回一个新的函数。我们日常工作中,常见的debounce和 throttle函数就满足这个条件,因此他们也可以被称为高阶函数。

//防抖
function debounce (fn,wait) {
        let timer = null;
        return function() {
            if(timer) {
                clearTimeout(timer)
            }
            let _this = this;
            let args = arguments;
            timer = setTimeout(() => {
                fn.apply(_this, args)
            }, wait);
        }
    }
//节流
 function thorttle(fn,wait) {
        let previous = 0;
        return function() {
            let _this = this;
            let args = arguments;
            let now = new Date().getTime();
            if(now - previous > wait) {
                fn.apply(_this, args);
                previous = now;
            }
        }
    }

二 函数组合

函数组合就是将两个或两个以上的函数组合生成一个新函数的过程:

    const composeFn = function (f, g) {
        return function (x) {
            return f(g(x));
        };
    };
    //在以上代码中,f 和 g 都是函数,而 x 是组合生成新函数的参数。
  • 函数组合的作用
    在项目开发过程中,为了实现函数的复用,我们通常会尽量保证函数的职责单一,比如我们定义了以下功能函数:

function lowerCase(input) {
 return input && typeof input === "string" ? input.toLowerCase() : input;
}

function upperCase(input) {
 return input && typeof input === "string" ? input.toUpperCase() : input;
}

function trim(input) {
 return typeof input === "string" ? input.trim() : input;
}

function split(input, delimiter = ",") {
 return typeof input === "string" ? input.split(delimiter) : input;
}

const trimLowerCaseAndSplit = compose(trim, lowerCase, split); // 参考下面compose的实现
trimLowerCaseAndSplit(" a,B,C "); // ["a", "b", "c"]



我们通过 compose 函数实现了一个 trimLowerCaseAndSplit 函数,该函数会对输入的字符串,先执行去空格处理,然后在把字符串中包含的字母统一转换为小写,最后在使用 , 分号对字符串进行拆分。利用函数组合的技术,我们就可以很方便的实现一个 trimUpperCaseAndSplit 函数。

//在以上的代码中,我们通过 Array.prototype.reduce 方法来实现组合函数的调度,对应的执行顺序是从左到右。这个执行顺序与 Linux 管道或过滤器的执行顺序是一致的。
 function compose(...funcs) {
        return function(x) {
            return funcs.reduce((arg,fn) => {
                return fn(arg);
            },x);
        }
    }

fn.jpg

其实每当看到 compose 函数,阿宝哥就情不自禁想到 “如何更好地理解中间件和洋葱模型” 这篇文章中介绍的 compose 函数:

function compose(middleware) {
  // 省略部分代码
  return function (context, next) {
    let index = -1;
    return dispatch(0);
    function dispatch(i) {
      if (i <= index)
        return Promise.reject(new Error("next() called multiple times"));
      index = i;
      let fn = middleware[i];
      if (i === middleware.length) fn = next;
      if (!fn) return Promise.resolve();
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err);
      }
    }
  };
}
//利用上述的 compose 函数,我们就可以实现以下通用的任务处理流程:

fn2.jpg

三 函数柯里化

柯里化(Currying)是一种处理函数中含有多个参数的方法,并在只允许单一参数的框架中使用这些函数。这种转变是现在被称为 “柯里化” 的过程,在这个过程中我们能把一个带有多个参数的函数转换成一系列的嵌套函数。它返回一个新函数,这个新函数期望传入下一个参数。当接收足够的参数后,会自动执行原函数。
在理论计算机科学中,柯里化提供了简单的理论模型,比如:在只接受一个单一参数的 lambda 演算中,研究带有多个参数的函数的方式。与柯里化相反的是 Uncurrying,一种使用匿名单参数函数来实现多参数函数的方法。比如:

const func = function(a) {
  return function(b) {
    return a * a + b * b;
  }
}

func(3)(4); // 25

Uncurrying 不是本文的重点,接下来我们使用 Lodash 提供的 curry 函数来直观感受一下,对函数进行 “柯里化” 处理之后产生的变化:


const abc = function(a, b, c) {
  return [a, b, c];
};
 
const curried = _.curry(abc);
 
curried(1)(2)(3); // => [1, 2, 3]
curried(1, 2)(3); // => [1, 2, 3]
curried(1, 2, 3); // => [1, 2, 3]
//这里需要特别注意的是,在数学和理论计算机科学中的柯里化函数,一次只能传递一个参数。而对于 JavaScript 语言来说,在实际应用中的柯里化函数,可以传递一个或多个参数。好的,介绍完柯里化的相关知识,接下来我们来介绍柯里化的作用。
  • 柯里化的作用
  1. 参数复用
function buildUri(scheme,domain,path) {
    return `$(scheme)://${domain}/${path} `
}

const Path1 = buildUri("https","gihub.com","aaa/bbb");
const Path2 = buildUri("https","gihub.com","ccc/ddd");

以上代码中,首先定义了一个builderUri 函数,主要用户构建uri地址,但是这样我们复制同样的相关变量,代码有冗余,我可以通过柯里化来复用相同的参数

const _ = require("lodash")
//通过柯里化方法,先构建一个相同的参数 柯里化函数
const buildCurry = _.curry(buildUri);//让普通的函数 转变成柯里化函数

const commonPath = buildCurry("https","github.com");
const Path1 = commonPath("aaa/bbb");
const Path2 = commonPath("ccc/dd");

//例如 出现一个参数一样的,2参数不一样的
const commonPath = buildCurry("https");
const Path1 = commonPath("xxxx.com","aaa/bbb");
const Path2 = commonPath("yyy.com","ccc/dd");

  1. 延迟计算/运行
const add = function (a, b) {
  return a + b;
};

const curried = _.curry(add);
const plusOne = curried(1);

在以上代码中,通过对 add 函数执行 “柯里化” 处理,我们可以实现延迟计算。好的,简单介绍完柯里化的作用,我们来动手实现一个柯里化函数。

  1. 柯里化的实现
    原理: 当柯里化后的函数接收到足够的参数后,就会执行原函数。而如果接受到参数不足的话,就会返回一个新的函数,用来接受余下的参数。
//把多参数的函数 变成 柯里化函数
    function curry(func) {
        return function curried(...args) {
            //当真实接受的参数 args 个数 和 真实定义的形参个数 func
            if(args.length >= func.length) {
                return func.apply(this, args)
            }else {
                return function (...args2) {
                    return curried.apply(this, args.concat(args2));
                }
            }
        }
    }

四 偏函数

在计算机科学中,偏函数应用(Partial Application)是指固定一个函数的某些参数,然后产生另一个更小元的函数。而所谓的元是指函数参数的个数,比如含有一个参数的函数被称为一元函数。
偏函数应用(Partial Application)很容易与函数柯里化混淆,它们之间的区别是:
偏函数应用是固定一个函数的一个或多个参数,并返回一个可以接收剩余参数的函数;
柯里化是将函数转化为多个嵌套的一元函数,也就是每个函数只接收一个参数。
了解完偏函数与柯里化的区别之后,我们来使用 Lodash 提供的 partial 函数来了解一下它如何使用。

function buildUri(scheme, domain, path) {
  return `${scheme}://${domain}/${path}`;
}

const myGithubPath = _.partial(buildUri, "https", "github.com");
const profilePath = myGithubPath("semlinker/semlinker");
const awesomeTsPath = myGithubPath("semlinker/awesome-typescript");
  • 偏函数实现
    原理: 偏函数用于固定一个函数的一个或多个参数,并返回一个可以接收剩余参数的函数。基于上述的特点,我们就可以自己实现一个 partial 函数:
function partial(fn) {
  let args = [].slice.call(arguments, 1);
  return function () {
    const newArgs = args.concat([].slice.call(arguments));
    return fn.apply(this, newArgs);
  };
}

五 惰性函数

由于不同浏览器之间存在一些兼容性问题,这导致了我们在使用一些 Web API 时,需要进行判断,

//判断 根据浏览器不同 则绑定事件的方法不一样
function addHandler(element, type, handler) {
  if (element.addEventListener) {
    element.addEventListener(type, handler, false);
  } else if (element.attachEvent) {
    element.attachEvent("on" + type, handler);
  } else {
    element["on" + type] = handler;
  }
}

以上代码中,我们实现了不同浏览器 添加事件监听的处理,代码实现起来很简单,就是多写点判断条件,但是当 每个元素都要绑定事件 需要 进入 判断逻辑后台才能进行绑定,这样设计的明显不合理的。对于上述问题 我们可以采用 惰性函数 来解决

  • 惰性载入函数

所谓的惰性载入函数 当第一次根据判断后,第二次调用时,就不再检索条件,直接执行函数。要实现这个功能,我们可以在第一次判断时候,在满足条件后直接覆盖所有分支的所调用函数。

function addHandler(element, type, handler) {

  if (element.addEventListener) {
      addHandler = function(element, type, handler) {
        element.addEventListener(type, handler, false);
      }
  } else if (element.attachEvent) {
       addHandler = function(element, type, handler) {
        element.attachEvent("on" + type, handler);

      }
  } else {
       addHandler = function(element, type, handler) {
           element["on" + type] = handler;
      }
  }

  return addHandler(element, type, handler)
}

除了以上的实现方式,由于函数作用就是检测 浏览器的,我们可以使用匿名函数自执行

const addHandler = (function(element, type, handler) {

  if (element.addEventListener) {
      return function(element, type, handler) {
        element.addEventListener(type, handler, false);
      }
  } else if (element.attachEvent) {
        return function(element, type, handler) {
        element.attachEvent("on" + type, handler);

      }
  } else {
        return function(element, type, handler) {
           element["on" + type] = handler;
      }
  }

  return addHandler(element, type, handler)
})()

通过自执行函数,在代码加载阶段就会执行一次条件判断,然后在对应的条件分支中返回一个新的函数,用来实现对应的处理逻辑。

六 缓存函数

缓存函数 是 将 函数的计算结果缓存起来,当下次以同样的参数调用函数,直接返回已缓存的结果,而无需执行函数。这是一种常见的以空间换取时间的性能优化手段
要实现缓存函数的功能,我们可以把经过序列化的参数作为 key,在把第 1 次调用后的结果作为 value 存储到对象中。在每次执行函数调用前,都先判断缓存中是否含有对应的 key,如果有的话,直接返回该 key 对应的值。分析完缓存函数的实现思路之后,接下来我们来看一下具体如何实现:

//缓存函数
    function memorize(fn) {
        const cache = Object.create(null);
        return function (...args) {
            //缓存 json数据
            const _args = JSON.stringify(args);
            return cache[_args] || (cache[_args] = fn.apply(fn, args))
        }
    }

    //使用 

    let complexCalc = (a, b) => {
  // 执行复杂的计算
};

let memoCalc = memorize(complexCalc);
memoCalc(666, 888);
memoCalc(666, 888); // 从缓存中获取
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,049评论 5 473
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,478评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,109评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,097评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,115评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,280评论 1 279
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,748评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,398评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,553评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,440评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,487评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,176评论 3 317
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,750评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,821评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,049评论 1 257
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,559评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,150评论 2 341

推荐阅读更多精彩内容