源码共读 | omit.js

905 阅读4分钟

我正在参与掘金会员专属活动-源码共读第一期,点击参与

课程: 源码共读第一期|omit.js

项目依赖


我们先来看看项目package.json文件中的依赖配置:

  • "@umijs/fabric": "^2.2.2",
  • "assert": "^1.4.1",
  • "eslint": "^7.4.0",
  • "father": "^2.29.5",
  • "np": "^6.3.1",
  • "rc-tools": "^6.3.3"

其中eslint是我们常见的代码质量检测的工具,既不必说。其他的依赖并不是很熟悉,我们来了解一下它们:

  • @umijs/fabric @umiji/fabric是一个包含了 prettier,eslint,stylelint 的配置文件合集,主要是方便基础工具快速的使用公共的预设配置。
  • assert 看这个名字就可以知道,这是一个断言库,主要是为了实现单元测试的相关功能,语法基本与jest一致。

  • father father是一款基于webpack的打包工具,负责生成输出cjs,esm构建文件。

  • np 一个npm发布工具,通过可交互命令行来快速发布包

  • rc-tools 似乎是一个react相关的工具,但是代码中并没有使用,这个仓库也很久没有更新了。

目录结构


.
├── .eslintignore
├── .eslintrc.js
├── .fatherrc.js
├── .git
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── index.d.ts
├── index.js
├── package.json
├── src
└── tests

项目目录结构如上,主要目录与文件是:

  • src - 代码目录
  • tests - 测试相关
  • index.d.ts - 提供ts类型支持
  • index.js - 打包入口目录

源码阅读

function omit(obj, fields) {

	// eslint-disable-next-line prefer-object-spread
	const shallowCopy = Object.assign({}, obj);
	
	for (let i = 0; i < fields.length; i += 1) {
	
		const key = fields[i];
		
		delete shallowCopy[key];
	
	}
	
	return shallowCopy;

}

export default omit;

函数omit接收两个参数objfields,其中fields是数组类型,表示要删除的字段的key列表。

函数开始通过Object.assign创建了一个新对象shallowCopy,这样是为了之后的deleteshallowCopy上进行操作,而不修改原对象。

代码使用delete运算符进行删除属性字段,删除属性字段主要有两种方式,一种就是使用delete运算符, 而另一种是通过构建新的返回对象的方式如:

let {[key]:a,...rest} = obj
return rest

但是如果要求在原对象上进行删除,似乎就只能使用delete操作符了。

逻辑很简单,下来看一下函数的类型断言:

declare function Omit<T, K extends keyof T>(
  obj: T,
  keys: Array<K>
): Omit<T, K>;

export default Omit;

通过K extends keyof T约束K需要实现keyof T, 返回类型使用了Omit类型。

delete操作符

delete操作符的返回值是boolean, 如果删除成功即返回true, 如:

const d = {a:1}
delete d.a // true

但是如果删除不存在的属性,delete的返回值依然是true

const d = {a:1}
delete d.y // true

当一个属性被设置为不可设置,delete 操作将不会有任何效果,并且会返回false, 而在严格模式下会抛出语法错误

var user = {};
Object.defineProperty(user, 'name', {configurable: false});

console.log(delete user.name);  // false

constletvar创建的属性也不可以被delete操作符删除,会返回false

const a = 1
var b = 2
let c = 3

delete a // false
delete b // false
delete c // false

通过class构建的实例上的属性可以使用delete删除

class User{

	name = "lee";
	
	clear(){
		delete this.name
	}
}

删除内层属性

之上的代码里仅支持删除外层属性,我们可以扩展一下也支持删除内层属性。

具体的做法就是支持参数为key1.key2.key3的形式

如果为多级属性的话,则需要进行递归在最内层删除后,使用返回的对象进行覆盖。

a.b.c,实际执行删除的只有内层的c但是需要用删除c后返回的对象来覆盖b, 同理在对a进行覆盖。

具体实现如下:

function omit<T extends Record<string, any>, K extends string>(
  obj: T,
  keys: K[]
): OmitResult<T, K> {
  const target = Object.assign({}, obj);

  for (let key of keys) {
    if (Object.hasOwn(target, key)) {
      delete target[key];
    }

    if (typeof key === "string" && key.includes(".")) {
      let [current, ...rest] = key.split(".");

      if (current in obj) {
        Object.assign(target, {
          [current]: omit(obj[current], [rest.join(".")]),
        });
      }
    }
  }

  return target as OmitResult<T, K>;
}

因为没有了K extends keyof T的元素,所以可能传入非属性Key的字段名,所以我们需要通过Object.hasOwn判断是否为该对象的Key

之后在通过split('.')每次去除需要过滤的Key值进行处理。

为了保证递归后返回值断言的类型正确,我们引入了OmitResult<T,K>对返回值进行类型推断。

type OmitResult<T, K extends string> = K extends `${infer F}.${infer R}`
  ? F extends keyof T
    ? Omit<T, F> & { [P in F]: OmitResult<T[F], R> }
    : T
  : K extends keyof T
  ? Omit<T, K>
  : T;

通过判断是否满足${infer F}.${infer R},如果满足则进行递归,负责返回Omit<T,K>类型。{ [P in F]: OmitResult<T[F], R> }是内层删除字段后重构的类型。

单元测试

当一切修改完成后,记得要进行单元测试,以我们修改后的代码为例:

describe("omit test", () => {
  const obj = { a: 1, b: 2, c: 3, d: { e: { x: "x", y: "y" } } };

  it("basic type test", async () => {
    expect(omit(obj, ["a"])).not.toHaveProperty("a");
    expect(omit(obj, ["b"])).not.toHaveProperty("b");
    expect(omit(obj, ["b"])).not.toHaveProperty("b");
    expect(omit(obj, ["d"])).not.toHaveProperty("d");
    expect(omit(obj, ["d"])).not.toHaveProperty("d");
  });

  it("nest type test", async () => {
    const data = omit(obj, ["d.e.x"]);
    expect(data).toHaveProperty("a");
    expect(data).toHaveProperty("d");
    expect(data.d).toHaveProperty("e");
    expect(data.d.e).toHaveProperty("y");
    expect(data.d.e).not.toHaveProperty("x");
  });

  it("error type test", async () => {
    const data = omit(obj, ["d.e.s"]);
    expect(data).toHaveProperty("a");
    expect(data).toHaveProperty("d");
    expect(data.d).toHaveProperty("e");
    expect(data.d.e).toHaveProperty("y");
    expect(data.d.e).toHaveProperty("x");

    expect(data).toEqual(obj);
  });

  it("exception type test", async () => {
    const data = omit(obj, ["_..😊.._"]);
    expect(data).toHaveProperty("a");
    expect(data).toHaveProperty("d");
    expect(data.d).toHaveProperty("e");
    expect(data.d.e).toHaveProperty("y");
    expect(data.d.e).toHaveProperty("x");

    expect(data).toEqual(obj);
  });
});

github