我正在参与掘金会员专属活动-源码共读第一期,点击参与
课程: 源码共读第一期|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构建文件。
目录结构
.
├── .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接收两个参数obj与fields,其中fields是数组类型,表示要删除的字段的key列表。
函数开始通过Object.assign创建了一个新对象shallowCopy,这样是为了之后的delete在shallowCopy上进行操作,而不修改原对象。
代码使用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
const、let、var创建的属性也不可以被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);
});
});