【拆拆源码】从omit.js和lodash里学怎么拷贝并剔除一个对象里的某几个key

883 阅读2分钟

我正在参加「掘金·启航计划」

这些内容会在本篇出现:

  1. Object.assign和解构的区别
  2. 如何获取对象中的所有key

omit.js

源码非常短,直接看实现:

function omit(obj, fields) {
  const shallowCopy = Object.assign({}, obj);
  for (let i = 0; i < fields.length; i += 1) {
    const key = fields[i];
    delete shallowCopy[key];
  }
  return shallowCopy;
}

简单易懂:

  1. 通过 Object.assign 对对象进行拷贝
  2. 遍历需要删掉的 key list
  3. 通过 delete 运算符删掉当前遍历中的key
  4. 返回拷贝+删除后的对象 值得一提的是,git记录里这里曾经用过…obj 而不是Object.assign,这两个最常用的首层深拷贝的方法在大多数点上都一致,唯一的区别在于Object.assign会使用setter而解构不会。 举例来说:
Object.assign({set a(v){this.b=v}, b:2}, {a:4}); 
// {b: 4}

{…{set a(v){this.b=v}, b:2}, …{a:4}};
 // {a: 4, b: 2}

lodash

lodash里的omit方法实现用了许多工具方法,并且相比起omitjs,它支持更多的入参类型。 23B82912-0B4D-4380-A972-E54F0842365D.png 从测试用例可以看出,甚至还包括第二个参数是function的选项。为了直取重点,这里会忽略掉一部分参数处理的逻辑,单纯考虑第二个参数为string[]的场景。

在4.17.15版本的lodash中,omit方法由 flatRest 方法包裹,这个方法会把超出2个的参数合并为2个,例如:

const origin = { a: 1, b: "2", c: 3 };
const result = omit(origin, “a”, “b”, [“c”, [“d”]]);
// omit方法实际接受到的参数为:
// { a: 1, b: "2", c: 3 } 和 [ 'a', 'b', 'c', [ 'd' ] ]
正如上文所说,这里不展开讲具体的实现逻辑,我们直接进入function内部,为每一行代码加上注释
function omit(object, paths) {
  var result = {};
  // 虽然这里没有判断undefined,但在copyObject的时候也做了容错
  if (object == null) {
    return result;
  }
  var isDeep = false;
  // 把['a', 'b', 'c']变成[[ 'a' ], [ 'b' ], [ 'c' ]]
  // arrayMap 相当于 map,只是用while写的循环
  paths = arrayMap(paths, function (path) {
    path = castPath(path, object);
    isDeep || (isDeep = path.length > 1);
    return path;
  });
  // 将object深拷贝给result
  copyObject(object, getAllKeysIn(object), result);
  // 只有一层的情况下,这里不走,--忽略开始--
  if (isDeep) {
    result = baseClone(
      result,
      CLONE_DEEP_FLAG | CLONE_FLAT_FLAG | CLONE_SYMBOLS_FLAG,
      customOmitClone
    );
  }
  // --忽略结束--,当前result为object的深拷贝
  var length = paths.length;
  while (length—) {
    // 通过修改result的方式,去掉path里对应的属性
    baseUnset(result, paths[length]);
  }
  // 返回处理完成的对象
  return result;
}

浅看下来,可以看到关键在于

copyObject(object, getAllKeysIn(object), result);

  var length = paths.length;
  while (length—) {
    // 通过修改result的方式,去掉path里对应的属性
    baseUnset(result, paths[length]);
  }

这两段代码,我们进入去看。

1. copyObject(object, getAllKeysIn(object), result)

1.1 getAllKeysIn(object)

首先看 getAllKeysIn(object) 是啥,从字面上猜测是获取所有的key,实际看也确实如此,这个方法合并了keysIn(创建一个 object 自身 和 继承的可枚举属性名为数组) 和 getSymbolsIn (内部方法,获取object 自身 和 继承的可枚举Symbol属性名为数组)。

keysIn的核心逻辑为:

var result = [];
// for...in: 按照原型链遍历对象上所有可枚举且key值不为Symbol的属性
for (var key in object) {
  // 忽略掉constructor
  if (
      key == "constructor" &&
      (isProto || !hasOwnProperty.call(object, key))
  ) {
    continue
  }
  result.push(key);
}
return result;

getSymbolsIn在环境合适的情况下即为getOwnPropertySymbols

1.2 copy

经过上面的处理,可以知道,对于 { a: 1, b: '2', c: 3 } [ 'a', 'c' ] 这个例子来说,我们的入参为copyObject({ a: 1, b: '2', c: 3 }, [ 'a', 'b', 'c' ], {}(result的引用))

那么继续对copyObject的方法进行简化:

function copyObject(source, props, object, customizer) {
 var isNew = !object; // var isNew = false
 object || (object = {}); // 无操作

 var index = -1,
   length = props.length; // length = 3
 // 遍历props,也即是[ 'a', 'b', 'c' ]
 while (++index < length) {
   var key = props[index];
   var newValue = customizer
     ? customizer(object[key], source[key], key, object, source)
     : undefined; // 相当于 var newValue = undefined

   if (newValue === undefined) {
     // 走这里,newValue等于原对象key的value
     newValue = source[key];
   }
   if (isNew) {
     baseAssignValue(object, key, newValue);
   } else {
     // 走这里, 对于普通的key assignValue的核心代码为
     // object[key] = value
     // 在传入的原对象上赋值
     assignValue(object, key, newValue);
   }
 }
 return object;
}

走过一遍代码可以看出,本质就是 新对象[key] = 原本对象[key],并没有判断是否是引用之类的,所以等同于omitjs里面的Object.assign和解构,或者说是 Object.assign和解构 等同于它,毕竟这是7年前的代码,那时候还没有es2015。

baseUnset(result, paths[length]);

核心代码相当于delete object[key]

delete object[toKey(last(path))]

拆到这里,可以看到虽然lodash虽然有许多边界条件判断容错和代码模拟,但是本质上来说跟omitjs是一样的,只是写法不同。而我的心情也有点复杂,毕竟两者代码的阅读难度完全不是一个等级的,但是学习嘛,不学到怎么知道这里学过了呢...

参考文档: javascript - Object spread vs. Object.assign - Stack Overflow