函数
记录这篇文章目的主要深入了解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);
}
}
![](https://img.haomeiwen.com/i25041675/09c393b42653e03b.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 函数,我们就可以实现以下通用的任务处理流程:
![](https://img.haomeiwen.com/i25041675/a3b1ce16500b57e4.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 语言来说,在实际应用中的柯里化函数,可以传递一个或多个参数。好的,介绍完柯里化的相关知识,接下来我们来介绍柯里化的作用。
- 柯里化的作用
- 参数复用
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");
- 延迟计算/运行
const add = function (a, b) {
return a + b;
};
const curried = _.curry(add);
const plusOne = curried(1);
在以上代码中,通过对 add 函数执行 “柯里化” 处理,我们可以实现延迟计算。好的,简单介绍完柯里化的作用,我们来动手实现一个柯里化函数。
- 柯里化的实现
原理: 当柯里化后的函数接收到足够的参数后,就会执行原函数。而如果接受到参数不足的话,就会返回一个新的函数,用来接受余下的参数。
//把多参数的函数 变成 柯里化函数
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); // 从缓存中获取
网友评论