perf(get, unset, matchesProperty, toPath): Improve performance

This commit is contained in:
raon0211 2024-09-14 22:58:18 +09:00
parent 1a5e9c8e64
commit 8b07ec7bd7
9 changed files with 159 additions and 54 deletions

View File

@ -2,6 +2,16 @@ import { bench, describe } from 'vitest';
import { get as getToolkit } from 'es-toolkit/compat';
import { get as getLodash } from 'lodash';
describe('get with simple string', () => {
bench('es-toolkit/get', () => {
getToolkit({ a: 1, b: 2 }, 'a');
});
bench('lodash/get', () => {
getLodash({ a: 1, b: 2 }, 'a');
});
});
describe('get with string', () => {
bench('es-toolkit/get', () => {
getToolkit({ a: { b: 3 } }, 'a.b');

View File

@ -1,13 +1,28 @@
import { bench, describe } from 'vitest';
import { omit as omitToolkit } from 'es-toolkit';
import { omit as omitToolkitCompat } from 'es-toolkit/compat';
import { omit as omitLodash } from 'lodash';
describe('omit', () => {
describe('omit: simple', () => {
bench('es-toolkit/omit', () => {
omitToolkit({ foo: 1, bar: 2, baz: 3 }, ['foo', 'bar']);
});
bench('es-toolkit/compat/omit', () => {
omitToolkitCompat({ foo: 1, bar: 2, baz: 3 }, ['foo', 'bar']);
});
bench('lodash/omit', () => {
omitLodash({ foo: 1, bar: 2, baz: 3 }, ['foo', 'bar']);
});
});
describe('omit: complex', () => {
bench('es-toolkit/compat/omit', () => {
omitToolkitCompat({ foo: { bar: { baz: 1 } }, quux: 2, a: { b: 3 } }, ['foo.bar.baz', 'quux']);
});
bench('lodash/omit', () => {
omitLodash({ foo: { bar: { baz: 1 } }, quux: 2, a: { b: 3 } }, ['foo.bar.baz', 'quux']);
});
});

View File

@ -1,6 +1,3 @@
const IS_PLAIN = /^\w*$/;
const IS_DEEP = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/;
/**
* Checks if a given key is a deep key.
*
@ -25,7 +22,7 @@ export function isDeepKey(key: PropertyKey): boolean {
return false;
}
case 'string': {
return !IS_PLAIN.test(key) && IS_DEEP.test(key);
return key.includes('.') || key.includes('[') || key.includes(']');
}
}
}

View File

@ -0,0 +1 @@
export const objectProto: any = Object.prototype;

View File

@ -2,21 +2,11 @@ import { describe, expect, it } from 'vitest';
import { toKey } from './toKey';
describe('toKey', () => {
it('converts strings to strings', () => {
expect(toKey('asd')).toBe('asd');
});
it('converts symbols to symbols', () => {
const symbol = Symbol('a');
expect(toKey(symbol)).toBe(symbol);
});
it("converts 0 to '0'", () => {
expect(toKey(0)).toBe('0');
});
it("converts -0 to '-0'", () => {
expect(toKey(-0)).toBe('-0');
expect(toKey(Object(-0))).toBe('-0');
});
});

View File

@ -1,5 +1,3 @@
import { isSymbol } from '../predicate/isSymbol';
/**
* Converts `value` to a string key if it's not a string or symbol.
*
@ -7,14 +5,9 @@ import { isSymbol } from '../predicate/isSymbol';
* @param {*} value The value to inspect.
* @returns {string|symbol} Returns the key.
*/
export function toKey(value: unknown) {
if (typeof value === 'string' || isSymbol(value)) {
return value;
}
if (Object.is(value?.valueOf(), -0)) {
export function toKey(value: number) {
if (Object.is(value, -0)) {
return '-0';
}
return `${value}`;
return value.toString();
}

View File

@ -309,32 +309,78 @@ export function get(object: unknown, path: PropertyKey | readonly PropertyKey[],
* @returns {any} - Returns the resolved value.
*/
export function get(object: any, path: PropertyKey | readonly PropertyKey[], defaultValue?: any): any {
let resolvedPath;
if (Array.isArray(path)) {
resolvedPath = path;
} else if (typeof path === 'string' && isDeepKey(path) && object?.[path] == null) {
resolvedPath = toPath(path);
} else {
resolvedPath = [path];
if (object == null) {
return defaultValue;
}
if (resolvedPath.length === 0) {
switch (typeof path) {
case 'string': {
const result = object[path];
if (result === undefined) {
if (isDeepKey(path)) {
return get(object, toPath(path), defaultValue);
} else {
return defaultValue;
}
}
return result;
}
case 'number':
case 'symbol': {
if (typeof path === 'number') {
path = toKey(path);
}
const result = object[path];
if (result === undefined) {
return defaultValue;
}
return result;
}
default: {
if (Array.isArray(path)) {
return getWithPath(object, path, defaultValue);
}
if (Object.is(path?.valueOf(), -0)) {
path = '-0';
} else {
path = String(path);
}
const result = object[path];
if (result === undefined) {
return defaultValue;
}
return result;
}
}
}
function getWithPath(object: any, path: readonly PropertyKey[], defaultValue?: any): any {
if (path.length === 0) {
return defaultValue;
}
let current = object;
let index;
for (index = 0; index < resolvedPath.length && current != null; index++) {
const key = toKey(resolvedPath[index]);
for (let index = 0; index < path.length; index++) {
if (current == null) {
return defaultValue;
}
current = current[key];
current = current[path[index]];
}
if (current === null && index === resolvedPath.length) {
return current;
if (current === undefined) {
return defaultValue;
}
return current ?? defaultValue;
return current;
}

View File

@ -1,3 +1,4 @@
import { isDeepKey } from '../_internal/isDeepKey.ts';
import { toKey } from '../_internal/toKey.ts';
import { toPath } from '../util/toPath.ts';
import { get } from './get.ts';
@ -19,27 +20,67 @@ import { get } from './get.ts';
* unset(obj, ['a', 'b', 'c']); // true
* console.log(obj); // { a: { b: {} } }
*/
export function unset(obj: unknown, path: PropertyKey | readonly PropertyKey[]): boolean {
export function unset(obj: any, path: PropertyKey | readonly PropertyKey[]): boolean {
if (obj == null) {
return true;
}
const resolvedPath = Array.isArray(path) ? path : typeof path === 'string' ? toPath(path) : [path];
switch (typeof path) {
case 'symbol':
case 'number':
case 'object': {
if (Array.isArray(path)) {
return unsetWithPath(obj, path);
}
const parent = get(obj, resolvedPath.slice(0, -1), obj);
const lastKey = toKey(resolvedPath[resolvedPath.length - 1]);
if (typeof path === 'number') {
path = toKey(path);
} else if (typeof path === 'object') {
if (Object.is(path?.valueOf(), -0)) {
path = '-0';
} else {
path = String(path);
}
}
if (typeof parent !== 'object' || parent == null || !Object.prototype.hasOwnProperty.call(parent, lastKey)) {
if (obj?.[path] === undefined) {
return true;
}
try {
delete obj[path];
return true;
} catch {
return false;
}
}
case 'string': {
if (obj?.[path] === undefined && isDeepKey(path)) {
return unsetWithPath(obj, toPath(path));
}
try {
delete obj[path];
return true;
} catch {
return false;
}
}
}
}
function unsetWithPath(obj: unknown, path: readonly PropertyKey[]): boolean {
const parent = get(obj, path.slice(0, -1), obj);
const lastKey = path[path.length - 1];
if (parent?.[lastKey] === undefined) {
return true;
}
const isDeletable = Object.getOwnPropertyDescriptor(parent, lastKey)?.configurable;
if (!isDeletable) {
try {
delete parent[lastKey];
return true;
} catch {
return false;
}
delete parent[lastKey];
return true;
}

View File

@ -33,7 +33,19 @@ export function matchesProperty(
property: PropertyKey | readonly PropertyKey[],
source: unknown
): (target?: unknown) => boolean {
property = Array.isArray(property) ? property : toKey(property);
switch (typeof property) {
case 'object': {
if (Object.is(property?.valueOf(), -0)) {
property = '-0';
}
break;
}
case 'number': {
property = toKey(property);
break;
}
}
source = cloneDeep(source);
return function (target?: unknown) {