《JavaScript 函数式编程》笔记
重要的理论
- 函数式的思想:输入值 —— 流水线式处理 —— 输出值
- 函数式编程一个关键:用较低级的函数逐步定义和使用离散功能。(就是尽量拆解、抽象,通用、相似的部分都抽象出来)
- 对数据集合的操作:最好对数据集合构建抽象关系运算符,就像 SQL 一样
underscore 常用函数使用场景和案例
只有想不到没有做不到,熟能生巧,多用用就知道该用什么函数解决问题了
reduce
identity
map
-
filter
chain
链式调用。- 建议写成链式调用,而不是声明多余的中间值。
-
配合
tap
,在 chain 的过程中可以获取中间值进行处理_.chain([]) .push('Take one down.') .tap(function(lyrics){ if(n > 1){ lyrics.push((n-1) + 'bottles of beer on the wall.'); } }) .value(); // 用 .value() 获取 _.chain 包装的数据的值
_.range
生成数字数组_.range([start], stop, [step])
-
使用场景:
// 配合 _.map, 指定重复次数 _.map(_.range(3), function(){return 'hello'});
-
通用技巧
不要怕,不过就是组合来组合去
方法变函数
-
优先使用函数而不是方法。把方法变成函数:
/** * 接收一个方法,在任何给定的对象上调用它 ⭐️ * 意义在于: * 把方法变成了函数,比如下面的 map,本来是 arr.map 现在变成了一个函数 * 并且做好了兼容,对象调用本身没有的方法时,就返回 undefined */ function invoker(name, method){ return function(target){ if(!existy(target)){ console.warn('must provide a target'); return ; } var targetMethod = target[name]; // 得到方法 var args = _.rest(arguments); // 调用对象就要放到参数中,而原来的参数就是 _.rest(arguments) return doWhen(existy(targetMethod) && method===targetMethod, function(){ return targetMethod.apply(target, args); }); }; } var rev = invoker('reverse', Array.prototype.reverse); rev([3, 4, 5]); // [5, 4, 3]
数组降维
-
场景:传入参数是数组,用 arguments 获取参数就变成了二维数组,这时需要把 arguments 降维。其实传入平面参数,arguments 就是一维的,但是如果参数一定要是数组呢?
// 没有降维 var addArrayElements = function(){ return _.reduce(arguments, add, 0); // arguments 是二维数组,[[1, 2, 3]] 要降维成 [1, 2, 3] }; addArrayElements([1, 2, 3, 4]); // 01,2,3,4
-
apply 封装函数降维
// 封装 func,用 apply 的方式调用 func,就会把参数降维 function splat(func){ return function(arr){ return func.apply(null, arr); // arr 就变成了平面元素 }; } // arguments 降维了,arguments 就是一维数组 var addArrayElements = splat(function(){ return _.reduce(arguments, add, 0); }); addArrayElements([1, 2, 3, 4]); //10
-
apply 配合 identity
var addArrayElements = function(){ var args = _.identity.apply(null, arguments); // arguments 是 [ [1, 2, 3, 4] ],降成了 [1, 2, 3, 4] return _.reduce(args, add, 0); }; addArrayElements([1, 2, 3, 4]); // 10
通用比较器
-
比较器跟谓词函数有关,独立定义比较器 的问题就是返回值是不确定的,只能作为一种约定。需要统一比较器的返回值。
// 比较器,返回 -1/0/1,可以传到 sort 之类的函数中 // 问题:这个比较器不够通用 function compareLessThanOrEqual(x, y){ if(x < y) return -1; // 万一返回值被别人修改成 -2 了呢,另外再写一个比较器,又要注意返回值的统一 if(x > y) return 1; return 0; }
-
解决思路:把比较器拆成 谓词 + 结果,变的是谓词的定义,根据谓词的执行结果(true/false)返回的结果是不变的(都是 -1/0/1)。这样,以后只需要定义一个谓词就可以生成一个新的比较器。
// 更好的比较器, pred 是谓词 function comparator(pred){ return function(x, y){ if(truthy(pred(x, y))) return -1; else if (truthy(pred(x,y))) return 1; else return 0; } } // 谓词,返回布尔值 function lessOrEqual(x, y){ return x <= y; } [1, -2, 11].sort( comparator(lessOrEqual) );
像操作数据库一样操作一张表
补集
-
从一个数组中获取数字组成新数组,补集:从一个数组中获取非数字组成新数组
-
低级写法
var a = ['a', 1, 'c']; _.filter(a, _.isNumber); // [1] // 补集。如果另外有一个 _.isObject 的补集,难道又要重新写一段类似的代码? _.filter(a, function(x){ return !_.isNumber(x); }); // ['a', 'c']
-
高级写法。抽象出一个补集函数
// 把这段抽象 // 这段的作用:返回谓词函数执行结果的相反值,这个谓词函数应该参数化 function(x){ return !_.isNumber(x); } // 变成 function complement(pred){ return function(){ return !pred.apply(null, _.toArray(arguments)); }; } _.filter(a, complement(_.isNumber); // ['a', 'c']
-
this
-
this 的值跟创建时的上下文有关,但会根据调用者改变,常常引起错误
- 改变 this 的值,用 apply/ call
fun.apply(obj, ...); // this 就指向 obj 了 fun.call(obj, ...); // this 就指向 obj 了
- 绑定 this 的值,用
_.bind
、_.bindAll
(绑定多个方法到它自己的对象上)
闭包、私有变量
-
用闭包实现私有变量,保护变量
var ball = function(){ var count = 0; // 私有变量 return { inc: function(n){ return count += n; } }; }; ball.inc(2); // 外部只能用 inc 访问 count
默认参数
-
有时传入的参数不对,比如是 null,就会破坏整个结果,例如
var nums = [1, 2, 3, null, 5]; console.log( '因为有个 null,所以乘积结果是错误的', _.reduce(nums, function(product, n){ return product * n; }) );
-
解决:需要设置默认参数
/** * 封装 func,提供默认参数 * 只能解决一级 null/undefined * 对于 {name: null, age: 18} 这种二级 null 无法解决 */ function fnull(func /*, defaults*/){ var defaults = _.rest(arguments); // 默认参数 return function(/*args*/){ // 返回包装后的守卫函数 // 只有在 守卫函数 被调用时才会遍历默认值,即只在需要的时候发生 // 其实:按低级的思想,就是在函数开头先把默认函数处理一遍,而这里把这个处理的过程抽象出来了,所以不用在定义单个函数时再去处理了,只要声明默认参数就可以了 var args = _.map(arguments, function(e, i){ return existy(e) ? e : defaults[i]; // 默认参数代替 null/undefined }); return func.apply(null, args); }; } var nums = [1, 2, 3, null, 5]; var saveMult = fnull(function(product, n){ return product * n; }, 1, 1); console.log( '使用 fnull 得到正确结果', _.reduce( nums, saveMult) );
/** * 解决二级 null * 只要遍历参数对象,把单个空值换成对应的默认值即可 * fnull(_.identity, defaultValue); 就可以把单个值转换成默认值 */ function defaults(d){ // 默认配置对象 return function(o, k){ // object key var val = fnull(_.identity, d[k]); // 守卫函数,会把空参数替换成默认参数 return o && val(o[k]); }; } function findName(person){ var lookup = defaults({name: 'jack'}); return lookup(person, 'name'); // 找到 person 的 name 属性 } findName({name: null}); // jack
对象校验器
-
场景:需要校验传入的参数是否符合要求。低级方法:在函数开头对传入参数进行一一校验,不通过就返回错误提示。升级:需要把校验抽象出来,验证器是验证器,函数是函数,区分开来!
-
解决:
/** * 参数:若干个验证器(谓词函数) * validators 是谓词函数,且带有一个 message 属性,表示错误信息 * return: 一个 checker 函数,他会用这些验证器进行验证,如果验证错误就会添加错误信息到数组中,最后返回该数组 * 所以返回空数组就表明通过了所有验证器 */ function checker(/*validators*/){ // validator 是 谓词函数 var validators = _.toArray(arguments); // 要 toArray,因为 arguments 不是数组 return function(obj){ // 用 reduce 是一个棒棒的选择!因为它可以记录 errs return _.reduce(validators, function(errs, check){ // check 就是当前的验证器 if(check(obj)){ return errs; // 通过 check, errs 不变 }else{ // check 是一个验证器,它是一个函数,他还有一个 message 属性,表示错误信息 return _.chain(errs).push(check.message).value(); // 不通过,添加 err } }, []); }; } // validators 要带有一个 message 属性,表示错误信息,每次都要设置很麻烦,所以抽象一个 validator 生成器, 不需要另外设置 message function validator(message, fun){ // 为什么不是直接 f = fun? // 应该是因为这一段: // message 是一个非常普通的属性名,如果给它设置值可能会抹掉正常的值,所以要新建一个 function var f = function(/* args */){ return fun.apply(fun, arguments); }; f.message = message; return f; } var gonnaFail = checker( validator('ZOMG!', always(false)) ); gonnaFail(100); // ["ZOMG!"]
头痛:高阶函数阶级多了就晕了。。。
链接
hide