高阶函数篇:
高阶函数是接受函数作为参数并且/或者返回函数作为输出的函数.
一般而言,高阶函数通常用于抽象通用的问题.换句话讲,高阶汉书就是定义抽象.
抽象定义: 在软件工程和计算机科学中,抽象是一种管理计算机系统复杂性的技术.它通过建立一个人与系统进行交互的复杂程度,把更复杂的细节抑制在当前水平之下.
例如,一个编写涉及数值操作代码的程序员可能并不会对底层硬件中的数字表现方式感兴趣(例如,不在乎它们是16位还是32位整数),包括这些细节在哪里被屏蔽.可以说,它们被抽象出来了,只留下简单的数字给程序员处理.
every
函数
我们经常需要检查数组的内容是否为数字,自定义对象或其他类型.我们通常编写典型的循环方法来解决这些问题.
下面将这些抽象到every函数中, 它接受两个参数:一个数组和一个函数.
它使用传入的函数检查数组的所有元素是否为true.
_.every = (arr,fn)=>{
let result = true;
for(const value of arr){
result = result && fn(value);
if(!result){
break;
}
}
return result;
}
some
函数
本函数与上面的every相对应,
如果数组中的一个元素通过传入的函数返回true,some函数就将返回true.
_.some = (arr,fn)=>{
let result = false;
for(const value of arr){
result = result || fn(value);
if(result){
break;
}
}
return result;
}
sortBy
函数
sortBy
函数接受一个名为property的参数并返回一个接受两个参数的新函数 ,本函数适于配合原生sort函数使用
示例:
people = {
{firsname:'ccfirstNeme',lastname:'aafirstNeme'},
{firsname:'bbfirstNeme',lastname:'bbfirstNeme'},
{firsname:'aafirstNeme',lastname:'ccfirstNeme'},
}
people.sort(_.sortBy('lastname'))
函数定义为:
_.sortBy = (property)=>{
return (a,b)=>{
var result = (a[property] < b[property]) ? -1 : (a[property] > b[property]) ? 1 : 0;
return result;
}
}
显然,sortBy函数接受一个属性并返回另一个函数.返回函数作为compareFunction传递给sort函数.此处的问题是,持有着peoperty参数的返回函数是怎么得来的?
闭包的世界欢迎你.
在winter大大,重学前端中,大大如此解释闭包:
闭包翻译自英文单词closure,在计算机领域,它有三个完全不相同的意义:
- 编译原理中,它是处理语法产生式的一个步骤;
- 计算几何中,它表示包裹平面点集的凸多边形(翻译为凸包);
- 在编程语言领域,它表示一种函数.
我们可以简单理解闭包为一个绑定了执行环境的函数,这个函数并不是印在书本里的一条简单的表达式,闭包与普通函数的区别是,它携带了执行环境,就像人在外星中需要自带吸氧的装备一样,这个函数也带有在程序中生存的环境.
在古典的闭包定义中,闭包包括两个部分:
- 环境部分
- 环境
- 标识符列表
- 表达式部分
对应于js,则是
- 环境部分
- 环境: 函数的词法环境(执行上下文的一部分)
- 标识符列表: 函数中用到的未声明的变量
- 表达式部分: 函数体
至此,我们可以认为,JS中的函数完全符合闭包的定义.它的环境部分是函数词法环境部分组成,它的标识符是函数中用到的未声明变量,它的表达式部分就是函数体.
回到我们的高阶函数来, 闭包有3个可访问的作用域:
- 在它自身声明之内声明的变量
- 对全局变量的访问
- 对外部函数变量的访问(也即上述函数用到的)
上面说的第三个作用域体现了闭包的一个重要概念,闭包可以记住它的上下文!
下面继续我们的高阶函数吧.
tap()
假设你在遍历一个来自服务器的数组,并发现数据错了.因此你想调试一下,看看你数组里究竟包含了什么.
不要使用命令式的方法,要用函数式的方法.我们需要一种调试方式.
_.tap = (value)=>
(fn)=>{
typeof(fn) === 'function' && fn(value);
console.log(value);
}
我们可以这样用:
_.forEach([1,2,3],(a)=>
_.tap(a)(()=>{
console.log(a);//1 1 2 2 3 3
})
)
unary函数
一个js里比较经典的坑 ['1','2','3'].map(parseInt)
的值是什么?
[1,NaN,NaN]
显然这个结果不是我们所期望的,造成这个的原因是parseInt
接受两个参数:
parseInt(string,radix)
- string : 要被解析的值,如果参数不是字符串,则将其转换为字符串(使用ToString抽象操作).字符串开头的空白符将会被忽略.
- radix : 一个介于2和36之间的整数(数字系统的基础),表示上述字符串的基数.比如参数'10'表示使用我们通常使用的十进制数值系统.始终指定此参数可以消除阅读该代码时的困惑并且保证转换结果可预测.当未指定基数时,不同的实现会产生不同的结果,通常认为其默认值为10,但是请在使用时总是显示的指定它.
map 三个参数调用了函数,分别是element,index和 arr. 如意如果把parseInt 传给map 函数,map会把index的值传给parseInt的radix参数,从而导致了上面的结果.
于是,到了我们的unary函数出场的时候了:
_.unary = (fn) =>
fn.length === 1
? fn
: (arg) => fn(arg);
我们检查传入的fn是否有一个长度为1的参数列表(可以通过length属性查看).如果有,就什么都不做,如果没有,就返回一个新数组,它只接受一个参数arg,并用该参数调用fn.
示例: ['1','2','3'].map(unary(parseInt)); // [1,2,3]
once函数
在很多情况下,我们只需要运行一次给定的函数. 比如只想设置一次第三方库,或初始化一次支付设置.
_.once = (fn) =>{
let done = false;
return function (){
return done ? undefined : ((done = true),fn.apply(this,arguments));
}
}
上面的once函数接受一个参数fn,并通过调用它的apply方法返回结果.此处注意的重点是,我们声明了一个名为done的变量,初始值为false.返回的函数会形成一个覆盖它的函数作用域.因此,返回的函数会访问并检查done是否为true,如果是,则返回undefined,否则将done设为true(这样就阻止了下一次执行)并用必要的参数调用函数fn
示例:
var doPayment = _.once(()=>{
console.log("Payment is done");
})
doPayment();//Payment is done
doPayment();//undefined
这里我们要打个岔好好说说this了
memoized 函数
知道算法里的动态规划不?
动态规划
动态规划的基本思想是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解,其经过分解的子问题往往不是相互独立的
- 其解依赖于子问题
- 子问题的求解是重复的
- 采用重递归出现的大量重复运算
如 Fib 数列
采用动态规划方法
- 构造所有子问题的一个表格
- 采用自底向上的方法依次求解子问题
- 也可以采用自上而下,为避免重复运算,采用 备忘录 或称为 记忆递归
memoized
函数使得函数能够记住其计算结果,避免大量重复计算.
_.memoized = (fn,hasher) => {
//这里依旧利用闭包可以记录上下文的特性
const lookupTable = {};
return (key)=>{
//为了应对多个参数的情况,可接受传入hasher自定义对象的key
var address = hasher ? hasher.apply(this,arguments) : key;
return lookupTable[address] || (lookupTable[address] = fn(address));
}
}
示例:
let fc = _.memoized((n)=>{
if(n===0){
return 1;
}
return n*fc(n-1);
})
console.time(1)
console.log(fc(10));//3628800
console.timeEnd(1);//3.876ms
console.time(2);
console.log(fc(11));//39916800
console.timeEnd(2);//0.896ms
数组的函数式编程
forEach
遍历给定数组并使用当前索引作为参数调用传入的函数
_.forEach = (array,fn)=>{
for(const value of array){
fn(value);
}
}
map
map与forEach非常相似,区别只是用一个新的数组捕获了结果。
_.map = (array,fn)=>{
let results = [];
for(const value of array){
results.push(fn(value));
}
return results;
}
reduce
设置累加器并遍历数组(记住累加器上一个值)以生成一个单一元素的过程称为归约数组
reduce 函数会记录以前的运算结果,然后与后面传入的参数一起运算,最后返回一个结果
注意:
如果不传入初始值时,只可以遍历数组,但是如果传入初试值,可以遍历数组或者对象
所以推荐统一传入初始值
_.reduce = (array,fn,initialValue)=>{
//判断是否传入初始值
let accumlator = initialValue === undefined ? array[0] : initialValue;
if(initialValue === undefined){
for(let i = 1,len = array.length;i<len;i++){
accumlator = fn(accumlator,array[i]);
}
}else{
for(const value of array){
accumlator = fn(accumlator,value);
}
}
return accumlator;
}
示例:
let a = [1,2,3,4];
console.log(_.reduce(a,(acc,next)=>acc+next,0));//10
filter
一个简单的过滤器, 因为 for of ,使得其可以适用于数组和对象
_.filter = (array,fn)=>{
let results = [];
for(const value of array){
(fn(value)) ? results.push(value) : undefined;
}
return results;
}
flatten
把所有嵌套数组连接到一个数组中,也即数组扁平化
数组扁平化小课堂开课啦:
- 完全扁平化:
-
如果数组的元素都是数字,那么我们可以考虑使用toString方法,
[1,[2,[3,4]]].toString().split(',').map((c)=>Number(c));
-
利用reduce+递归
flatten = (arr)=>arr.reduce((prev,next)=>prev.concat(Array.isArray(next)?flatten(next):next),[]);
-
利用...运算符,
flatten = (arr)=>{while(arr.some(item=>Array.isArray(item))){
arr = [].concat(...arr);
}
return arr;
}
-
只去除一层:
- 一个很神奇的方法
Function.apply.bind([].concat,[]);
解释:
flatten = Function.apply.bind([].concat,[]);
= (arr)=>[].concat.apply([],arr);
= [].concat(...arr)
[].concat(...arr);
解释:a = [1,[3,2]] => [].concat(1,[3,2]) => [1].concat([3,2]) =>[1,3,2]
因为一层去除结合es6...
很容易实现,我们这里只支持完全扁平化
_.flatten = (arr)=>{
while(_.some(arr,item=>Array.isArray(item))){
arr = [].concat(...arr);
}
return arr;
}
zip
按照相应传入的fn规则合并对应数组
_.zip = (leftArr,rightArr,fn)=>{
let index,len,result = [];
let [leftArrLen,rightArrLen] = [leftArr.length,rightArr.length];
for(index = 0,len = Math.min(leftArrLen,rightArrLen);index<len;index++){
result.push(fn(leftArr[index],rightArr[index]));
}
//如果两个数组长度不同,直接在result后面加上多出部分数组
if(leftArrLen > rightArrLen){
result = result.concat(leftArr.slice(rightArrLen,leftArrLen));
}else if(leftArrLen < rightArrLen){
result = result.concat(rightArr.slice(leftArrLen,rightArrLen));
}
return result;
}
示例:
let a = [1,2,3,4];
let b = [2,1,2];
console.log(_.zip(a,b,(l,r)=>l+r)); // [ 3, 3, 5, 4 ]
escape和unescape
推荐看讶羽的这篇 underscore 系列之字符实体与 _.escape
这两个函数是为了转义以及反转义html字符串,替换&,<,>,',",和`字符为字符实体,防止浏览器误将其解释为标签,以避免XXS攻击.
我们要转义哪些字符呢?
- < 和 > 因为浏览器会把它看成一个标签的开始或结束.
- & 浏览器会认为&是一个字符实体的开始.例如
<
浏览器会把它解释成< - ' 和 " 也需要注意, 例如
function reder(input){
return '<input type="name" value=" '+input+' ''>'
}
//输入 "> <script>alert(1)</script>"
//return -> <input type="name" value=""><script>alert(1)</scripe>
- ` 在IE低版本中(<=8),反引号可以用于关闭标签.
var escapeMap = {
'&' : '&',
'<' : '<',
'>' : '>',
'"' : '"',
"'" : ''',
'`' : '`'
}
//将对象的key和value反转
_.invert = (obj)=>{
var result = {};
var keys = Object.keys(obj);
for(var i = 0,len = keys.length ;i < len;i++){
result[obj[keys[i]]] = keys[i];
}
return result;
}
var unescapeMap = _.invert(escapeMap);
//为了防止XXS攻击
//转义HTML字符串,替换&,<,>,',"和`字符为字符实体
//以及反转义
//_.escape('Curly , Larry & Moe');
//=>'Curly , Larry & Moe'
const createEscaper = (map) =>{
var escaper = (match)=>map[match];
var source = '(?:'+Object.keys(map).join('|')+')';
//source = "(?:&|<|>|"|'|`)"
//或者"(?:&|<|>|"|'|`)"
var testRegexp = RegExp(source);
//全局搜索,查找所有匹配项,而不是在第一个匹配项后停止
var replaceRegexp = RegExp(source,'g');
return (string)=>{
string = string == null ? '' : '' + string;
//replace 中第二个参数可以是个函数
return testRegexp.test(string) ? string.replace(replaceRegexp,escaper):string;
}
}
_.escape = createEscaper(escapeMap);
_.unescape = createEscaper(unescapeMap);