首页 > 代码库 > underscore学习总结,献给晦涩的函数式编程之美
underscore学习总结,献给晦涩的函数式编程之美
underscore.js 越看越美,如果在项目中不断尝试underscore的方法,将会事半功倍
underscore 体现出 functionial javascript的思想,采用函数式编程的思路来解决日常生活中的一些 util的小问题
javascript 属于弱语言,对象类型用得最多的就是 array和object,underscore是基于js 封装一些对象和数组方法的库,使用起来非常便捷
这里推荐一本函数式编程的书,Functionial Javascript ,这本书对于函数式编程艺术表现形式发挥得淋漓精致,实在是越看越美
collection function 部分强烈建议看完,后半部分的array function有点水,想复习的可以看看,不想看的就别看了,略水(其中数组的flatten方法建议还是看下)
Functional Javascript
源码一开始就给出了非常实用的 创建 通用回调函数的方法
_.iteratee = function(value, context, argCount) { if (value =http://www.mamicode.com/= null) return _.identity; if (_.isFunction(value)) return createCallback(value, context, argCount); if (_.isObject(value)) return _.matches(value); return _.property(value); };
iteratee :产生通用回调函数callback的内部入口
1、如果 value是 null 就直接返回 identity函数
_.identity = function(value) { return value; };
看似没用的identity方法,在函数式编程中属于一个通用的接口(该想法纯属个人理解,如有更好的理解方式,欢迎告知)
2、如果value是 函数
即返回一个 由 creatCallback 创建的 callback
var createCallback = function(func, context, argCount) { if (context === void 0) return func; switch (argCount == null ? 3 : argCount) { case 1: return function(value) { return func.call(context, value); }; case 2: return function(value, other) { return func.call(context, value, other); }; case 3: return function(value, index, collection) { return func.call(context, value, index, collection); }; case 4: return function(accumulator, value, index, collection) { return func.call(context, accumulator, value, index, collection); }; } return function() { return func.apply(context, arguments); }; };
1、如果上下文是 void 0(查资料void 0 实际就是等于undefined),那么直接返回 func
2、argCount 核心方法,underscore 中很多的方法的callback都是基于 这个switch实现的
譬如 map,each,find,filter ,argCount 对应的是3,对应的callback的参数为 value, index, list
tips:这里要讲讲这3个参数,参数名字不是随便起的,是有意义的
value代表一组list中的一个值,index代表索引值,list(collection)代表那个list集合数组或对象,理解这些变量的含义对于理解作者的程序有很大的帮助_each:
_.each = _.forEach = function(obj, iteratee, context) { if (obj == null) return obj; iteratee = createCallback(iteratee, context); var i, length = obj.length; if (length === +length) { for (i = 0; i < length; i++) { iteratee(obj[i], i, obj); } } else { var keys = _.keys(obj); for (i = 0, length = keys.length; i < length; i++) { iteratee(obj[keys[i]], keys[i], obj); } } return obj; };
each 方法,argCount为undefined,那么undefined==null,那么他就是 argCount为3的回调函数
这里有2个分支,如果是数组,如果是对象,采用内部方法keys来实现,underscore很多的方法都是基于这个判断
_.keys = function(obj) { if (!_.isObject(obj)) return []; if (nativeKeys) return nativeKeys(obj); var keys = []; for (var key in obj) if (_.has(obj, key)) keys.push(key); return keys; };
如果对象不存在,那么返回空数组,如果存在内部keys方法,则使用内部keys方法
那么什么是内部keys方法呢 nativeKeys = Object.keys,
由于现代浏览器支持 ecmascript5的规范,浏览器本身支持keys方法的话,则直接使用浏览器内部方法,加速执行效率
_.has方法,判断是否是对象的自有属性(hasOwnProperty),默认不使用继承的prototype的属性
最后返回数组keys,这里需要注意的是,为了接口的一致性,数组和对象经过处理之后全部可以使用相同的方法接口的 数组
_.map :
map方法并没有特殊的地方,只是在内部创建了一个新的result数组,然后逐个编辑数组对象func求值后插入result数组,最终返回这个数组
_.find:
_.find = _.detect = function(obj, predicate, context) { var result; predicate = _.iteratee(predicate, context); _.some(obj, function(value, index, list) { if (predicate(value, index, list)) { result = value; return true; } }); return result; };
_.some = _.any = function(obj, predicate, context) { if (obj == null) return false; predicate = _.iteratee(predicate, context); var keys = obj.length !== +obj.length && _.keys(obj), length = (keys || obj).length, index, currentKey; for (index = 0; index < length; index++) { currentKey = keys ? keys[index] : index; if (predicate(obj[currentKey], currentKey, obj)) return true; } return false; };
这里需要提一下的是这个predicate,什么是predicate ,predicate是一个函数,返回 true or false,这对于查找和判断非常有用
some是如果有一个 值满足 predicate 那么就是返回true,some结合 find方法 可以找到 第一个满足 predicate的 值
相当于 sql中的 limit one
_.filter:
内部使用 each遍历+predicate判断的方法 ,创建 result 数组,然后符合predicate的true值就插入push返回,没啥多说的
_.reject:
filter的 reverse方法,返回的result 是 false的情况
_.every:
some函数的升级版本,如果所有的集合元素和符合predicate,那么返回true,如果有一个不符合条件就返回false
argCount=4的情况
_reduce:
_.reduce = _.foldl = _.inject = function(obj, iteratee, memo, context) { if (obj == null) obj = []; iteratee = createCallback(iteratee, context, 4); var keys = obj.length !== +obj.length && _.keys(obj), length = (keys || obj).length, index = 0, currentKey; if (arguments.length < 3) { if (!length) throw new TypeError(reduceError); memo = obj[keys ? keys[index++] : index++]; } for (; index < length; index++) { currentKey = keys ? keys[index] : index; memo = iteratee(memo, obj[currentKey], currentKey, obj); } return memo; };
这里多了一个参数 memo,对应于 createCallback中就是 accumulator
accumulator顾名思义就是一个累加器,这里有一个分支,如果累加器不存在,默认取数组的第一个值作为memo的初始值
然后循环执行iteratee后,返回最终的memo
_reduceRight
reduceRight的内部实现原理和reduce一致,只是循环的顺序从数组最后一个开始, while (index--) 的方式实现
相关扩展函数
_.pluck = function(obj, key) { return _.map(obj, _.property(key)); };
_.where = function(obj, attrs) { return _.filter(obj, _.matches(attrs)); };
_.findWhere = function(obj, attrs) { return _.find(obj, _.matches(attrs)); };
这里开始体现函数式编程的强大优美之处,扩展现有的函数,实现新的函数,这就是函数式编程,
pluck基于 map,map使用property
where基于filter,filter使用matches,matches使用pairs,pairs使用keys,一切皆是函数
_.property,_matches属于基础函数
_.property = function(key) { return function(obj) { return obj[key]; }; };
propery是锁定了key值,可以被map用来获取每个元素指定的key值,这里是用到js中强大的闭包功能
_.matches = function(attrs) { var pairs = _.pairs(attrs), length = pairs.length; return function(obj) { if (obj == null) return !length; obj = new Object(obj); for (var i = 0; i < length; i++) { var pair = pairs[i], key = pair[0]; if (pair[1] !== obj[key] || !(key in obj)) return false; } return true; }; };
_.pairs = function(obj) { var keys = _.keys(obj); var length = keys.length; var pairs = Array(length); for (var i = 0; i < length; i++) { pairs[i] = [keys[i], obj[keys[i]]]; } return pairs; };
pairs是一个 数组,每一个元素又是一个数组,每个数组有2个值,index 0为key值,index 1为 value值
这里要注意 matches 返回的也是一个function,这个function是一个predicate,这里再次强调predicate的重要性,他的重要性就是这个思想,返回true or false
用来 筛选 结果,一个会经常使用的功能。
下面说一说其他的 collection function ,由于缺少关联性,所以就逐一列举说明
_.contains = _.include = function(obj, target) { if (obj == null) return false; if (obj.length !== +obj.length) obj = _.values(obj); return _.indexOf(obj, target) >= 0; };
如果说keys是返回对象的key值数组的话,那么values函数就是返回对象的 value值数组,用法基本是一致的
_indexOf:
_.indexOf = function(array, item, isSorted) { if (array == null) return -1; var i = 0, length = array.length; if (isSorted) { if (typeof isSorted == ‘number‘) { i = isSorted < 0 ? Math.max(0, length + isSorted) : isSorted; } else { i = _.sortedIndex(array, item); return array[i] === item ? i : -1; } } for (; i < length; i++) if (array[i] === item) return i; return -1; };
这里的 isSorted有3个分支
1. 为undefined, 这个情况说明 查找的数组不需要跳过几个元素进行查找,从下标为0开始查找
2.为 数值,这个跟undefined严格来说是属于一个分支,从下表为 isSorted 开始查找,如果是负值,先求max再循环查找
3.为 true ,那么使用 binary search (二进制查找,本人算法需要勤加修炼,如翻译不对,欢迎拍砖)
_.sortedIndex = function(array, obj, iteratee, context) { iteratee = _.iteratee(iteratee, context, 1); var value =http://www.mamicode.com/ iteratee(obj); var low = 0, high = array.length; while (low < high) { var mid = low + high >>> 1; if (iteratee(array[mid]) < value) low = mid + 1; else high = mid; } return low; };
这里一起过一遍这个算法 low代表 0, high 代表最后一个元素 >>>运算符相当于 除以2
mid是 高位的一半+低位
如果 中间mid 小于这个 value,那么 低位就要变成 mid+1位 ,然后继续循环,让high位除以2
如果中间mid大于这个value,那么高位high变成mid位,继续循环
直到low位大于high位
此时就找到了 low 这个 数组 索引 index的值,如果这个index对应的值跟item的值相同,则返回相关的索引
_invoke
_.invoke = function(obj, method) { var args = slice.call(arguments, 2); var isFunc = _.isFunction(method); return _.map(obj, function(value) { return (isFunc ? method : value[method]).apply(value, args); }); };
使用指定的上下文 来触发函数的执行
method可以使 函数,或者是字符串
_max 和 _min 函数
2种情况,有判断函数和没有判断函数
有判断函数就求值判断,没有就遍历数组或对象,然后使用内置方法比较返回
_shuffle
_.random = function(min, max) { if (max == null) { max = min; min = 0; } return min + Math.floor(Math.random() * (max - min + 1)); };
_.shuffle = function(obj) { var set = obj && obj.length === +obj.length ? obj : _.values(obj); var length = set.length; var shuffled = Array(length); for (var index = 0, rand; index < length; index++) { rand = _.random(0, index); if (rand !== index) shuffled[index] = shuffled[rand]; shuffled[rand] = set[index]; } return shuffled; };
这个函数其实没有什么特别的作用,就是重新排列集合中的元素
这里的算法还是比较精妙的,一开始没看懂,看几遍又懂了,过几天再看,又晦涩难懂了,有一种被智商压制的感觉
这里的精妙之处就在于 _.random(0,index)
第一次进入循环,index为0,那么random值肯定是0 shuffled[0]=set[0];
第二次进入循环 index为 1 那么 random值为0或1
什么情况会出现 rand !==index , 那么 index为 1, rand=0 ,这个时候 shuffled[1]=shuffled[0], shuffled[0]=set[1],互换了位置
如果 rand!==index不成立,那么 shuffed[1]=set[1];
下面看第三次循环 index为2 ,random值为 0,1,2 ,他要shuffle几次,还是一次,原理就是index互换
这个shuffle每次只换了一个元素,效率奇高,而且也做到了shuffle 可能存在的所有结果,非常高效,不管你有几个元素,每次shuffle只做了一个切换,
自己写不出这种感觉有木有,实在是牛x。
_.sample
_.sample = function(obj, n, guard) { if (n == null || guard) { if (obj.length !== +obj.length) obj = _.values(obj); return obj[_.random(obj.length - 1)]; } return _.shuffle(obj).slice(0, Math.max(0, n)); };
guard官方说是一个保护值,如果有guard,那么无论n给多少值,都是取一个值
多个值的情况使用了shuffle函数
_.sortBy
_.sortBy = function(obj, iteratee, context) { iteratee = _.iteratee(iteratee, context); return _.pluck(_.map(obj, function(value, index, list) { return { value: value, index: index, criteria: iteratee(value, index, list) }; }).sort(function(left, right) { var a = left.criteria; var b = right.criteria; if (a !== b) { if (a > b || a === void 0) return 1; if (a < b || b === void 0) return -1; } return left.index - right.index; }), ‘value‘); };
乍一看比较吓人,现在肢解一下
iteratee 回调函数
pluck 接受2个参数,一个集合,一个 key值,这里的key值就是"value"
map 返回的是一个 数组 集合对象,每个对象包含3个 键值对,然后使用 含有 sort function 方法的进行最后返回一个 经过排序的 values 数组,分解之后思路就比较清晰了。
_.toArray:
_.toArray = function(obj) { if (!obj) return []; if (_.isArray(obj)) return slice.call(obj); if (obj.length === +obj.length) return _.map(obj, _.identity); return _.values(obj); };
官方的解释是可以将任何 可迭代的集合对象转换为数组
obj.length === +obj.length 可以表示一个带有length属性的伪数组对象
关于伪数组对象的概念,欢迎告知更为精确的理解方式
_.size:
_.size = function(obj) { if (obj == null) return 0; return obj.length === +obj.length ? obj.length : _.keys(obj).length; };
返回集合对象或者数组的长度
_.partition
_.partition = function(obj, predicate, context) { predicate = _.iteratee(predicate, context); var pass = [], fail = []; _.each(obj, function(value, key, obj) { (predicate(value, key, obj) ? pass : fail).push(value); }); return [pass, fail]; };
从英文来看是分区的概念,并没有什么特别之处,就是将filter和reject 2个函数的结果放入到一个数组中去,true的数组叫pass,fail的数组为fail
最后说一下集合函数中又一个重要的group函数
内部groupby 函数
var group = function(behavior) { return function(obj, iteratee, context) { var result = {}; iteratee = _.iteratee(iteratee, context); _.each(obj, function(value, index) { var key = iteratee(value, index, obj); behavior(result, value, key); }); return result; }; };
behavior本身是一个函数,可以理解为group 传递一个函数,然后返回一个函数,返回的函数将使用到这个behavior函数
这个包含3个group使用到的函数
_.groupBy = group(function(result, value, key) { if (_.has(result, key)) result[key].push(value); else result[key] = [value]; });
_.indexBy = group(function(result, value, key) { result[key] = value; });
_.countBy = group(function(result, value, key) { if (_.has(result, key)) result[key]++; else result[key] = 1; });
groupBy:
groupBy的 key值其实就是 iteratee 函数的 return 结果值,每个分组由一个数组保存,最后的result根据group的定义可知,是一个map对象
然后根据 结果值 分组
indexBy:
默认传入的集合中的元素是唯一的
countBy:
countBy的结果是 进行统计,跟 result push不同,但是原理是相同的
自此所有的集合函数都已经复习完毕
******************华丽的分割线*************************
Array Functions 数组函数
下面都是一些工具函数,没啥新意,就当一起复习下吧
_first:
_.first = _.head = _.take = function(array, n, guard) { if (array == null) return void 0; if (n == null || guard) return array[0]; if (n < 0) return []; return slice.call(array, 0, n); };
返回数组的第一个元素,或者slice(0,n)个元素,如果n为负值,那么会返回空数组,如果存在guard值为true,无论n为多少都只会返回第一个元素
_.initial:
_.initial = function(array, n, guard) { return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n))); };
返回数组中 length-n个元素,如果guard值存在,就剔除最后一个元素返回
_.last:
_.last = function(array, n, guard) { if (array == null) return void 0; if (n == null || guard) return array[array.length - 1]; return slice.call(array, Math.max(array.length - n, 0)); };
返回数组的最后一个元素,或者slice(n)到数组结尾个元素,如果n为负值,那么会返回全数组,如果存在guard值为true,无论n为多少都只会返回最后一个元素
_rest:
_.rest = _.tail = _.drop = function(array, n, guard) { return slice.call(array, n == null || guard ? 1 : n); };
剔除数组中的第一个元素,如果n存在,返回n到数组结尾的数组,如果guard存在,剔除第一个数组中的元素返回
_.compact
_.compact = function(array) { return _.filter(array, _.identity); };
剔除 所以会判断为false的 数组值,内部使用filter实现,这里就能看出identity函数的作用了
下面是比较重要的一个flatten 函数,先来看内部实现
var flatten = function(input, shallow, strict, output) { if (shallow && _.every(input, _.isArray)) { return concat.apply(output, input); } for (var i = 0, length = input.length; i < length; i++) { var value =http://www.mamicode.com/ input[i]; if (!_.isArray(value) && !_.isArguments(value)) { if (!strict) output.push(value); } else if (shallow) { push.apply(output, value); } else { flatten(value, shallow, strict, output); } } return output; };
shallow 控制是否是深度 迭代
譬如 数组 a=[1,2,[2,3,[1,2]]]
shallow=true的情况
flatten之后的结果为 [1,2,2,3,[1,2]]
shallow 为假的情况
flatten[1,2,2,3,1,2]
strict 控制是否 字符串也可以 作为 数组来 扁平化(flatten)
具体的实现函数如下:
_.flatten
_.flatten = function(array, shallow) { return flatten(array, shallow, false, []); };
flatten 函数默认 strict为false
_.difference
_.difference = function(array) { var rest = flatten(slice.call(arguments, 1), true, true, []); return _.filter(array, function(value){ return !_.contains(rest, value); }); };
可以看到这里使用了filter函数,内部又使用了contains函数,这里还要注意的是 strict为true,那么如果你传入字符串的话,是没有用,不会去重
_.difference([2,3],"23",[2]);
result: [3]
_.union
_.union = function() { return _.uniq(flatten(arguments, true, true, [])); };
_.uniq = _.unique = function(array, isSorted, iteratee, context) { if (array == null) return []; if (!_.isBoolean(isSorted)) { context = iteratee; iteratee = isSorted; isSorted = false; } if (iteratee != null) iteratee = _.iteratee(iteratee, context); var result = []; var seen = []; for (var i = 0, length = array.length; i < length; i++) { var value =http://www.mamicode.com/ array[i]; if (isSorted) { if (!i || seen !== value) result.push(value); seen = value; } else if (iteratee) { var computed = iteratee(value, i, array); if (_.indexOf(seen, computed) < 0) { seen.push(computed); result.push(value); } } else if (_.indexOf(result, value) < 0) { result.push(value); } } return result; };
if (!_.isBoolean(isSorted)) { context = iteratee; iteratee = isSorted; isSorted = false;}
这一段代码是典型的修正变量的代码,在jquery的实现里面大量的出现
函数传入的参数非常灵活
如果isSorted函数不传递的话
那么 isSorted这个位置的参数值,实际是 iteratee函数
那么每个参数都需要进行一次修正
context=iteratee 3->4
iteratee=isSorted 2->3
isSorted=false false->2
isSorted 由用户自行判断是否是已经排序过的数组,如果是已经排序过的数组,内部使用简洁快速的方法进行排序
如果iteratee 自定义函数存在,那么 执行运算后判断是否是新值
如果不存在,直接取 array中元素的值进行 indexOf的判断
_.without
_.without = function(array) { return _.difference(array, slice.call(arguments, 1)); };
difference的扩展方法,用法基本于difference一致
_.intersection
_.intersection = function(array) { if (array == null) return []; var result = []; var argsLength = arguments.length; for (var i = 0, length = array.length; i < length; i++) { var item = array[i]; if (_.contains(result, item)) continue; for (var j = 1; j < argsLength; j++) { if (!_.contains(arguments[j], item)) break; } if (j === argsLength) result.push(item); } return result; };
本方法不支持深层递归
a=[[1,2],3]
b=[[1,2],3]
_.intersect(a,b) result=> [3]
根据arguments的长度来判断,只要一个元素不在任何一个数组中,那么就剔除这个元素,该元素的引用地址必须相同
如果对象的引用地址相同,则返回那个共有的对象
a={} b=a
arr1=[a,1]
arr2=[b,2]
_.intersect(a,b) result=> {}
_.zip
_.zip = function(array) { if (array == null) return []; var length = _.max(arguments, ‘length‘).length; var results = Array(length); for (var i = 0; i < length; i++) { results[i] = _.pluck(arguments, i); } return results; };
根据 数组长度的最大值判断 新的数组中每个一个元素数组的长度, max内部是使用_.property函数获取了length这个属性
Example
_.zip([1,2,3],[2,3],[3]);
result [[1,2,3],[2,3,undefined],[3,undefined,undefined]]
一般使用的方法为 每个数组长度都是相同的
_.object
_.object = function(list, values) { if (list == null) return {}; var result = {}; for (var i = 0, length = list.length; i < length; i++) { if (values) { result[list[i]] = values[i]; } else { result[list[i][0]] = list[i][1]; } } return result; };
如果values存在 将 2个数组 结合为一个对象
如果values不存在 则使用list中每一个item[0]作为 key值,item[1] 作为 value值
最后返回这个合成的对象
_.lastIndexOf
_.lastIndexOf = function(array, item, from) { if (array == null) return -1; var idx = array.length; if (typeof from == ‘number‘) { idx = from < 0 ? idx + from + 1 : Math.min(idx, from + 1); } while (--idx >= 0) if (array[idx] === item) return idx; return -1; };
并没有什么特别的地方,判断了小于0和大于边界的情况,一如既往的向前遍历 使用 while循环
_.range:
_.range = function(start, stop, step) { if (arguments.length <= 1) { stop = start || 0; start = 0; } step = step || 1; var length = Math.max(Math.ceil((stop - start) / step), 0); var range = Array(length); for (var idx = 0; idx < length; idx++, start += step) { range[idx] = start; } return range; };
如果只有一个参数, start的值给到stop, start 默认从0开始
step如果不存在,就使用1
length值代表每一个step的元素个数
实际的效果就是 给到一个 基于step 断点的效果
_.range(1,30,4)
result=>[1,5,9,13,17,21,25,29]
自此 数组方法也已经复习完毕,写的有点累,相关的 oop、util Function、object function、函数式函数,将在以后进行复习
underscore学习总结,献给晦涩的函数式编程之美