经过underscore系列的一和二,咱们掌握了一些underscore框架设计的思想以及基础的数据类型诊断,从这一节开始,咱们将慢慢进入underscore的难点和核心。这个系列的三和四,咱们会全方位的了解underscore中对迭代器的实现。javascript
javascript的迭代器咱们并不陌生。咱们能够这样理解,迭代器提供了一种方法按顺序访问集合中的每一项,而这种集合能够是数组,也能够是对象。咱们先回忆一下,在ES6以前,javascript有7种数组的迭代器,分别是java
var arr = [1,2,4]
for(var i=0;i<arr.length;i++) {
console.log(i)
}
复制代码
function add(num) {console.log(num + 1)}
var arr = [1,2,3]
arr.forEach(add)
复制代码
function judge(a) {return a > 1}
var arr = [1,23,4];
console.log(arr.every(judge)); // false
复制代码
function judge(a) {return a > 1}
var arr = [1,23,4];
console.log(arr.some(judge)); // true
复制代码
function filterNum(a) {return a> 2}
var arr = [1,45,56,2,5]
console.log(arr.filter(filterNum)) // [45, 56, 5]
复制代码
function add(a, b) {return a +b }
var arr = [1,3,4,5,6]
console.log(arr.reduce(add, 0)) //19
复制代码
function mapNum(a) {return a + 1 }
var arr = [2,4,56]
console.log(arr.map(mapNum)) // [3,5,57]
复制代码
针对对象 ,咱们常常使用for in进行迭代, 也可使用 Object.keys等方式进行迭代算法
var a = {b: 1, c: 2}
for(var i in a) {
console.log(i) //b, c
}
Object.keys(a) // b, c
复制代码
首先从最复杂的reduce入手,reduce的基本功能前面在数组的迭代器方法中已经介绍,咱们只重点关注实现的细节。其中iteratee 为迭代器函数,该函数有四个参数memo,value 和 迭代的index(或者 key)和最后一个引用的整个 list。数组
reduce 和 reduceRight 惟一的区别在于遍历顺序,一个从左往右, 一个从右往左,所以能够用同一个函数来设计reduce。其中 context 改变this的指向咱们稍后分析。浏览器
_.reduce = createReduce(1);
_.reduceRight = createReduce(-1);
var createReduce = function(dir) {
// dir 来区分
return function (obj, iteratee, meno, context) {
// 两种类型,对象和类数组须要区别处理方便for循环遍历, 对象咱们须要拿到全部的属性集合,数组,类数组咱们关注的是下标。
// 巧妙点:当为数组,类数组时 keys = false, 当为对象时 keys = 属性数组
var keys = !isArrayLike(obj) && _.keys(obj)
var lengths = (keys || obj).length;
// 处理遍历方向,即参数dir的值
var index = dir > 0 ? 0 : length-1;
for (; index >= 0 && index < lengths; index += dir) {
// 若是是数组,类数组则取下标,若是是对象则取属性值
var currentKey = keys ? keys[index] : index;
// 执行迭代器函数,并把返回值赋值给meno,继续循环迭代
meno = iteratee(meno, obj[currentKey], currentKey, obj);
}
return meno
}
}
复制代码
reduce 函数在使用的时候,meno是可选项,若是没有传递meno, 则自动会把list 中的第一个元素赋值给meno。所以咱们能够将处理的核心代码抽离为一个独立的函数,并将是否有meno的初始值作独立判断。bash
_.reduce = createReduce(1);
_.reduceRight = createReduce(-1);
var createReduce = function(dir) {
// dir 来区分
var reducer = function (obj, iteratee, meno, context, initial) {
···
// 增长meno 的初始值判断赋值
if (!initial) {
memo = obj[keys ? keys[index] : index];
index += dir;
}
···
}
return function (obj, iteratee, meno, context) {
// 记录是否有meno传值
var initial = arguments.length >=3;
return reducer(obj, iteratee, meno, context, initial)
}
}
复制代码
reduce的实现已经基本完成,然而依然留着一个悬念,那就是context能够改变this的指向。underscore源码中单独将context改变this指向的方法抽离成一个独立的函数optimizeCb,该方法能够兼容underscore中 全部须要改变this指向的过程, reduce 为其中一种情形,函数调用call方法改变this指向所传递的参数分别为 meno, value, index, list框架
var createReduce = function(dir) {
···
return function(obj, iteratee, meno, context) {
···
return reducer(obj, optimizeCb(iteratee, context, 4), initial) //optimizeCb优化
}
}
var optimizeCb = function(func, context, argCount) {
// 当没有特定的this指向时, 返回原函数
if(context == void 0) return func;
switch(argCount) {
// 针对reduce函数的context指向
case 4: return function(context, meno, value, index, list) {
return func.call(context, meno, value, index, list);
}
}
}
复制代码
underscore 中map 方法原理上会生成一个新的数组,该数组与目标源数组,类数组的length属性相同,或者与对象的自身可枚举属性个数相同,所以有了reduce 的基础,咱们能够简单的实现map 方法ide
_.map = function (obj, iteratee, context) {
var keys = !isArrayLike(obj) && _.keys(obj)
var lengths = (keys || obj).length;
// 生成一个个数和目标源数组个数相同,或者目标源对象自身可枚举属性个数相同的数组
var result = new Array(lengths);
for(var i=0; i<lengths;i++) {
var currentKey = keys ? keys[i] : i;
results = iteratee(obj[currentKey], currentKey, obj)
}
}
复制代码
对于map的使用,咱们能够不传递iteratee迭代器,能够传递一个函数类型的迭代器,也能够传递一个对象类型做为迭代器,所以须要在进入迭代过程以前作一层过滤,根据不一样的迭代器类型作不一样的操做。函数
_.map = function(obj, iteratee, context) {
// 迭代器类型分类
iteratee = cb(iteratee, context);
···
}
复制代码
var cb = function(iteratee, context) {
if(iteratee == null) return _.identity
}
_.identity = function(value) {
return value
}
复制代码
var cb = function(iteratee, context) {
if( _.isFunction(iteratee) ) return optimizeCb(iteratee, context, 3) // 此时类型为3
}
// 完善optimizeCb 函数
var optimizeCb = function(func, context, argCount) {
// 当没有特定的this指向时, 返回原函数
if(context == void 0) return func;
switch(argCount) {
case 3: return function(context, value, index, list) {
return func.call(context, value, index, list);
}
case 4: return function(context, meno, value, index, list) {
return func.call(context, meno, value, index, list);
}
}
}
复制代码
var cb = function(iteratee, context) {
if (_.isObject(value) && !_.isArray(value)) return _.matcher(value); // 返回断言函数
}
复制代码
_.times也是underscore提供的迭代器,它会调用迭代器n次,每次调用时传递index做为参数,最终结果返回一个执行结果的数组。例如:post
console.log(_.times(n, function(i) {return i * 2 })) // [0, 1, 4]
复制代码
简单的实现以下,关键点在注释中说明:
_.times = function(n, iteratee, context) {
// 必须保证n的值是比0 大的数字
var n = Math.max(0, n);
// 建立一个个数为n的新数组
var arr = new Array(n);
for(var i = 0; i<n;i++) {
arr[i] = iteratee(i)
}
return arr
}
复制代码
一样,涉及context 改变this指向,咱们一样能够经过optimizeCb进行优化,此时完善optimizeCb函数
_.times = function(n, iteratee, context) {
···
iteratee = optimizeCb(iteratee, context, 1)
}
// 完善optimizeCb 函数
var optimizeCb = function(func, context, argCount) {
// 当没有特定的this指向时, 返回原函数
if(context == void 0) return func;
switch(argCount) {
case 1: return function(context, value) {
return func.call(context, value)
}
case 3: return function(context, value, index, list) {
return func.call(context, value, index, list);
}
case 4: return function(context, meno, value, index, list) {
return func.call(context, meno, value, index, list);
}
}
}
复制代码
经过列举三种迭代器的设计,咱们不但掌握了underscore迭代器设计的基本思想,也对中间核心optimizeCb函数设计的可能进行了枚举,underscore中optimizeCb函数优化的类型只有三种,对应的数值分别为1,3,4(注意:并无2的类别)。咱们也对迭代器的三种类型进行了枚举,在cb函数中分别区分了不传递迭代器,迭代器自身为数组,对象时的处理。掌握了optimizeCb和cb两个函数的设计思想,在设计剩余的迭代器时难度便小了不少。 因为篇幅过长,underscore中其余迭代器的实现咱们放到下一节阐述。