学习underscore源码之内部函数createAssigner、cb和optimizeCb

795 阅读4分钟

前言

学习的underscore.js 源码版本为1.13.1

_.extend

_.extend(destination, *sources)
将source对象中的所有属性简单地覆盖到destination对象上,并且返回 destination 对象. 复制是按顺序的, 所以后面的对象属性会把前面的对象属性覆盖掉(如果有重复)

_.extend(void 0, {a: 1});   // undefined
_.extend(null, {a: 1});  // null

_.extend({}, {a: 'b'});   // {a: 'b'}
_.extend({a: 'x'}, {a: 'b'});   // {a: 'b'}

_.extend({x: 'x'}, {a: 'a', x: 2}, {a: 'b'});   // {x: 2, a: 'b'}

var F = function() {};
F.prototype = {a: 'b'};
var subObj = new F();
subObj.c = 'd';

_.extend({}, subObj);   // {c: 'd', a: 'b'}

如何实现?

// 获取obj拥有的和继承的所有属性的名称
function allKeys(obj) {
  if (!isObject(obj)) return [];
  var keys = [];
  for (var key in obj) keys.push(key);
  return keys;
}

/**
 * 
 * @param {object} obj 
 */
function extend (obj) {
  var length = arguments.length;
  // 只有一个参数或者 obj 为 null 、 undefined 直接返回
  if (length < 2 || obj == null) return obj;
  // 从第二个参数开始遍历
  // 其实也可以对参数进行过滤一遍(过滤掉非对象的参数)
  for (var index = 1; index < length; index++) {
    var source = arguments[index],
        keys = allKeys(source),
        l = keys.length;
    for (var i = 0; i < l; i++) {
      var key = keys[i];
      obj[key] = source[key];
    }
  }
  return obj;
}

_.extendOwn

_.extendOwn(destination, *sources)
与extend区别是只复制自己的属性覆盖到目标对象

// 与extend不一样, 只复制subObj自身属性覆盖到目标对象{}
_.extendOwn({}, subObj);    // {c: 'd'}

有了上面的extend,实现起来就容易了

// 判断对象自身属性中是否具有指定的属性
function has$1(obj, key) {
  return obj != null && hasOwnProperty.call(obj, key);
}
var nativeKeys = Object.keys;
// 获取obj上所有可枚举属性的名称
function keys(obj) {
  if (!isObject(obj)) return [];
  if (nativeKeys) return nativeKeys(obj);
  var keys = [];
  for (var key in obj) if (has$1(obj, key)) keys.push(key);
  return keys;
}

/**
 * 
 * @param {object} obj 
 */
function extendOwn (obj) {
  var length = arguments.length;
  // 只有一个参数或者 obj 为 null 、 undefined 直接返回
  if (length < 2 || obj == null) return obj;
  // 从第二个参数开始遍历
  // 其实也可以对参数进行过滤一遍(过滤掉非对象的参数)
  for (var index = 1; index < length; index++) {
    var source = arguments[index],
        // 就是此处与extend不一样
        keys = keys(source),
        l = keys.length;
    for (var i = 0; i < l; i++) {
      var key = keys[i];
      obj[key] = source[key];
    }
  }
  return obj;
}

既然两者如此接近,那么可以直接使用高阶函数(是一个接收函数作为参数或将函数作为输出返回的函数)来进行复用。在实现该高阶函数前再看一个_.defaults函数

_.defaults

_.defaults(object, *defaults)
用defaults对象填充object 中的undefined属性。 并且返回这个object。一旦这个属性被填充,再使用defaults方法将不会有任何效果。

// 与extend区别一是会对object 中的undefined属性或不存在的属性进行填充
_.defaults({name: undefined, age: 18}, null, void 0, {name: 'zxx', age: 20, sex: 'male'});   // {name: 'zxx', age: 18, sex: 'male'}

// 区别二 是可以传null
_.defaults(null, {a: 1});   // {a: 1}

由上可以看出这三个函数其实很相似,看下如何实现利用传参的不同,返回不同的函数的高阶函数

createAssigner

/**
 * 利用传参的不同,返回不同的函数
 * @param {function} keysFunc 处理obj上属性名称的函数
 * @param {?boolean} defaults 是否是defaults函数
 */
function createAssigner(keysFunc, defaults) {
  return function(obj) {
    var length = arguments.length;
    // 对defaults 函数 obj可以为null,通过Object 构造函数为给定的参数创建一个包装类对象
    if (defaults) obj = Object(obj);
    if (length < 2 || obj == null) return obj;
    for (var index = 1; index < length; index++) {
      var source = arguments[index],
          keys = keysFunc(source),
          l = keys.length;
      for (var i = 0; i < l; i++) {
        var key = keys[i];
        // 兼容性处理
        // 对extend、extendOwn来说!defaults为true
        // 对defaults函数来说 obj 中的不存在该属性或属性值为undefined
        if (!defaults || obj[key] === void 0) obj[key] = source[key];
      }
    }
    return obj;
  };
}

var extend = createAssigner(allKeys);

var extendOwn = createAssigner(keys);

var defaults = createAssigner(allKeys, true);

_.map

_.map(list, iteratee, [context])
通过对 list 里的每个元素调用转换函数(iteratee迭代器)生成一个与之相对应的数组。iteratee传递三个参数:value,然后是迭代 index(或 key ),最后一个是引用指向整个list
使用:

var arr = [1, 2, 3];
// 只有一个对象, 返回一个值组成的数组
_.map(arr);   // [1, 2, 3]

_.map({1: 'one', 2: 'two', 3: 'three'});    // ['one', 'two', 'three']

_.map(undefined);   // []

// 当 iteratee 为一个函数,正常处理
_.map(arr, function(num){ return num * 3; });   // [3, 6, 9]


var obj = [{name:'xman', age: 20}, {name: 'zxx', age: 18}];
// 当 iteratee 为一个对象,返回元素是否匹配指定的对象
_.map(obj, {name: 'zxx', age: 18});   // [false, true]


// 当 iteratee 为字符串或数组,返回元素对应的属性值的集合
_.map(obj, 'name');     // ['xman', 'zxx']
_.map(obj, ['age']);    // [20, 18]

var obj1 = {
  name: 'zxx',
  info: {
    age: 18
  }
};
// // 会取出深层次的值
_.map(obj1, ['age']);    // [undefined, 18]


// 传入context
_.map([1, 2, 3], function(item){
    return item + this.value;
}, {value: 100});     // [101, 102, 103]

看起来这功能真的是丧心病狂,下面自己来写一版

实现第一版

var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
// 判断是否是类数组
var isArrayLike = function (obj) {
  let length = obj == null ? void 0 : obj.length;
  return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
}

function isFunction (func) {
  return Object.prototype.toString.call(func) === '[object Function]'
};
var nativeIsArray = Array.isArray;
var isArray = nativeIsArray || function(obj) {
  return Object.prototype.toString.call(obj) === '[object Array]';
};

function isObject(obj) {
  var type = typeof obj;
  return type === 'function' || type === 'object' && !!obj;
}

function map (obj, iteratee, context) {
  var _keys = !isArrayLike(obj) && keys(obj),
      length = (_keys || obj).length,
      results = Array(length), iterateeFn;
  // iteratee不存在
  if (iteratee == null) {
    // 迭代时直接返回原值
    iterateeFn = function (value) {
      return value;
    }
  } else if (isFunction(iteratee)) {  // iteratee为函数情况
    iterateeFn = function () {
      return iteratee.apply(context, arguments);
    }
  } else if (isObject(iteratee) && !isArray(iteratee)) {  // iteratee为对象且不是数组
    iterateeFn = (function () {
      var _keys = keys(iteratee), length = _keys.length;
      return function (obj) {
        for (var i = 0; i < length; i++) {
          var key = _keys[i];
          if (iteratee[key] !== obj[key] || !(key in obj)) return false;
        }
        return true;
      }
    })()
  } else {  // iteratee是数组或字符串、数字等
    iterateeFn = (function () {
      var path = isArray(iteratee) ? iteratee: [iteratee], length = path.length;
      return function (obj) {
        for (var i = 0; i < length; i++) {
          if (obj == null) return void 0;
          // 根据路径取出深层次的值
          obj = obj[path[i]];
        }
        return length ? obj : void 0;
      }
    })()
  }

  for (var index = 0; index < length; index++) {
    var currentKey = _keys ? _keys[index] : index;
    
    results[index] = iterateeFn(obj[currentKey], currentKey, obj);
  }
  return results;
}

以上代码基本上可以满足使用,接下来我们来看下源码:

function map(obj, iteratee, context) {
  iteratee = cb(iteratee, context);
  var _keys = !isArrayLike(obj) && keys(obj),
      length = (_keys || obj).length,
      results = Array(length);
  for (var index = 0; index < length; index++) {
    var currentKey = _keys ? _keys[index] : index;
    results[index] = iteratee(obj[currentKey], currentKey, obj);
  }
  return results;
}

cb

cb 函数使用了 _.iteratee 函数,如果你修改这个函数,其实会影响多个函数,这些函数基本都属于集合函数,具体包括 map、find、filter、reject、every、some、max、min、sortBy、groupBy、indexBy、countBy、sortedIndex、partition、和 unique等

function cb(value, context, argCount) {
  // 修改了 _.iteratee 函数, 就使用我们自定义的 _.iteratee 函数来处理 value 和 context
  if (_$1.iteratee !== iteratee) return _$1.iteratee(value, context);
  return baseIteratee(value, context, argCount);
}

iteratee

function iteratee(value, context) {
  return baseIteratee(value, context, Infinity);
}

baseIteratee

function baseIteratee(value, context, argCount) {
  // 处理map函数中只传入一个参数的情况
  if (value == null) return identity;
  // 处理函数
  if (isFunction(value)) return optimizeCb(value, context, argCount);
  // 处理对象且不是数组
  if (isObject(value) && !isArray(value)) return matcher(value);
  return property(value);
}

identity

// 返回传入的参数
function identity(value) {
  return value;
}

该函数直接看可能感觉没什么用,使用在迭代函数中就比较容易理解

optimizeCb

/**
 * underscore 内部方法
 * 根据 this 指向(context 参数)、 argCount 参数二次操作返回一些回调、迭代方法
 * @param {function} func 
 * @param {?*} context 
 * @param {?number} argCount 
 */
function optimizeCb(func, context, argCount) {
  // 如果没有指定 this 指向,则返回原函数
  if (context === void 0) return func;
  switch (argCount == null ? 3 : argCount) {
    case 1: return function(value) {
      return func.call(context, value);
    };
    // 如果有指定 this,但没有传入 argCount 参数
    // _.each、_.map等方法会走这里
    case 3: return function(value, index, collection) {
      return func.call(context, value, index, collection);
    };
    // _.reduce、_.reduceRight等方法会走这里
    case 4: return function(accumulator, value, index, collection) {
      return func.call(context, accumulator, value, index, collection);
    };
  }
  return function() {
    return func.apply(context, arguments);
  };
}

matcher

返回一个断言函数,这个函数会给你一个断言可以用来辨别给定的对象是否匹配attrs指定键/值属性

function matcher(attrs) {
  attrs = extendOwn({}, attrs);
  return function (obj) {
    return isMatch(obj, attrs);
  };
}

isMatch

_.isMatch(object, properties)
告诉你properties中的键和值是否包含在object中

/**
 * 判断properties中的键和值是否包含在object中
 * @param {object} object 
 * @param {?object} attrs 
 */
function isMatch(object, attrs) {
  var _keys = keys(attrs), length = _keys.length;
  if (object == null) return !length;
  var obj = Object(object);
  for (var i = 0; i < length; i++) {
    var key = _keys[i];
    if (attrs[key] !== obj[key] || !(key in obj)) return false;
  }
  return true;
}

var moe = {name: 'Moe Howard', hair: true};
var curly = {name: 'Curly Howard', hair: false};

isMatch(moe, {hair: true});   // true
isMatch(curly, {name: 'zxx'});  // false
isMatch(null, {});    // true

function Prototest() {}
Prototest.prototype.x = 1;
var specObj = new Prototest;

isMatch({x: 2}, specObj);  // true

property

返回一个函数,该函数将返回任何传入对象的指定属性。 path 可以指定为简单 key(键),或者指定为对象键或数组索引的数组,用于深度属性提取

function toPath$1(path) {
  return isArray(path) ? path : [path];
}
_$1.toPath = toPath$1;

/**
 * 内部方法,深层次获取对象中属性值
 * @param {object} obj 
 * @param {array|string|number} path 对象键或数组索引的数组或简单 key
 */
function deepGet(obj, path) {
  var length = path.length;
  for (var i = 0; i < length; i++) {
    if (obj == null) return void 0;
    obj = obj[path[i]];
  }
  return length ? obj : void 0;
}

function toPath(path) {
  return _$1.toPath(path);
}


function property(path) {
  path = toPath(path);
  return function(obj) {
    return deepGet(obj, path);
  };
}

总结

要学习 underscore 的源码,在分析集合相关的函数时一定会接触 cb 和 optimizeCb 函数,先掌握这两个函数,会帮助你更好更快的解读源码

参考资料:
github.com/mqyqingfeng…