【源码漫游】underscore.js | 如何手写一个each函数?

275 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第10天,点击查看活动详情

hi, 我是小黄瓜没有刺。一枚菜鸟瓜🥒,期待关注➕ 点赞,共同成长~

本系列会精读一些小而美的开源作品或者框架的核心源码。

引子 👾

如果让你手写一个js原生数组中的forEach函数,你会怎么写?相信很多人都能写出来:

Array.prototype.myForEach = function(fn, context = window){
    // 获取调用对象的长度
    let len = this.length
    for(let i = 0; i < len; i++){
        // 判断用户传入的参数是否为对象,指定this指向
        typeof fn === 'function' && fn.call(context, this[i], i)
    }
}

forEach功能的实现主要有以下几个要点:

  • 接受一个函数,遍历所有属性,使用函数对每个属性进行处理
  • 可以自定义this指向

但是对于大多数情况来说,我们面对的业务逻辑和要考虑的情况比这要多得多,比如:

  • 如果我想处理对象怎么办?
  • 如果用户传入的不是对象怎么办?
  • 怎么才能精确的判断用户传入的是数组还是其他对象?
  • 怎样使用户可以自定义接受不同数量的参数?

其实一个成熟的工具函数库,不仅仅是要实现功能,良好的扩展性和健壮性也是必不可少的。 那门来看一下underscore.js是如何处理这些问题的 👇

each函数 🎉

each函数接受三个参数

  • obj:需处理的原对象
  • iteratee:处理函数
  • contextthis绑定

首先来处理一下用户传入的自定义this指向的函数值:

// 
function each(obj, iteratee, context) {
  // this绑定?
  iteratee = optimizeCb(iteratee, context)

  // ......
}


// argCount为自定义参数个数
// 默认为三个:index(下标), item(具体值), obj(原对象)
function optimizeCb(fn, context, argCount) {
   // 如果没有context值,不需要处理,直接返回原函数
  if(context == void 0) return fn

  // 处理传递参数个数
  // 对原函数进行包装
  // 此处知识包装了函数,并没有执行
  switch(argCount == null ? 3 : argCount) {
    case 1: 
      return function(value) {
        return fn.call(context, value)
      }
    case 3:
       // collection指原对象
      return function(value, index, collection) {
        // 使用call进行函数作用域绑定
        return fn.call(context, value, index, collection)
      }
  }
}

接下来判断是否为数组:

// 如果原数据为具有length属性的对象结构直接遍历每个值调用包装函数
if(isArrayLike(obj)) {
    for(i = 0;len = obj.length, i < len; i++) {
       // 传入item,index,obj
      iteratee(obj[i], i, obj)
    }
}


// isArrayLike的实现
// isArrayLike(判断是否为数组包括类数组)的实现稍微复杂了点,但是核心是使用length属性

// 定义数组的index最大长度
let MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;

// 抽离出来的方法,核心是获取对象中某个属性的值
let shallowProperty = function(key) {
  // 返回一个函数
  return function(obj) {
     // 返回对象中对应属性的值
    return obj == null ? void 0 : obj[key];
  };
}
// 通过调用对象的length的值来确定是否为数组及类数组
// 对象无length属性
let getLength = shallowProperty('length')


let createSizePropertyCheck = function(getSizeProperty) {
  return function(obj) {
    let sizeProperty = getSizeProperty(obj)
    // 判断是否为数组及类数组:值为number类型,且index值的范围不超过合法范围
    return typeof sizeProperty === 'number' && sizeProperty >= 0 && sizeProperty <= MAX_ARRAY_INDEX

  }
}

增加对对象结构的支持:

if(isArrayLike(obj)) {
    for(i = 0;len = obj.length, i < len; i++) {
      iteratee(obj[i], i, obj)
    }
} else {
    // 非数组的取值?
    let _keys = keys(obj)
    // 使用key进行对应取值
    for(i = 0; len = _keys.length, i < len; i++) {
        iteratee(obj[_keys[i]], _keys[i], obj)
    }
}


// 判断是否为对象
let isObj = function(obj) {
  let type = typeof obj;
  return type === 'function' || (type === 'object' && !!obj)
}

// 判断是否为对象的私有属性
let has = function(obj, key) {
  return obj !== null && obj.hasOwnProperty(key)
}

// 取对象所有的key值,结果为数组
export default function keys(obj) {
  if(!isObj) return []
  // 兼容性判断,如果支持Object.keys方法,直接进行取keys值
  if(Object.keys) return Object.keys(obj);
  // 不支持通过for in 进行遍历所有的key
  let keys = []
  for(let k in obj) {
    if(has(obj, k)) {
      keys.push(k)
    }
  }
  return keys
}

最后完整实现:

export default function each(obj, iteratee, context) {
  // 处理this绑定
  iteratee = optimizeCb(iteratee, context)

  let i, len;
  // 处理不同数据结构
  if(isArrayLike(obj)) {
    for(i = 0;len = obj.length, i < len; i++) {
      iteratee(obj[i], i, obj)
    }
  } else {
   let _keys = keys(obj)
   for(i = 0; len = _keys.length, i < len; i++) {
    iteratee(obj[_keys[i]], _keys[i], obj)
   }
  }

  return obj
}

本文在整理的时候舍弃了一些对于低版本ie的兼容性代码,对于each函数的实现还是学到了很多,不只是代码功能的实现,还有对边界条件,代码的可扩展性,函数的拆分,都有很多可以在日常学习和工作中得以借鉴的地方。

写在最后 ⛳

源码系列第三篇!未来可能会更新实现mini-vue3javascript基础知识系列,希望能一直坚持下去,期待多多点赞🤗🤗,一起进步!🥳🥳