码迷,mamicode.com
首页 > 其他好文 > 详细

underscore学习总结,献给晦涩的函数式编程之美

时间:2014-12-28 16:46:06      阅读:6905      评论:0      收藏:1      [点我收藏+]

标签:

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 == 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 = 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 = 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 = 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学习总结,献给晦涩的函数式编程之美

标签:

原文地址:http://www.cnblogs.com/horsefefe/p/underscore_review.html

(1)
(1)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!