持续创作,加速成长!这是我参与「掘金日新计划 · 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:处理函数context:this绑定
首先来处理一下用户传入的自定义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-vue3和javascript基础知识系列,希望能一直坚持下去,期待多多点赞🤗🤗,一起进步!🥳🥳