mirror of
https://github.com/toss/es-toolkit.git
synced 2025-01-07 16:59:26 +03:00
perf(get, unset, matchesProperty, toPath): Improve performance
This commit is contained in:
parent
1a5e9c8e64
commit
8b07ec7bd7
@ -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');
|
||||
|
@ -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']);
|
||||
});
|
||||
});
|
||||
|
@ -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(']');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
1
src/compat/_internal/objectProto.ts
Normal file
1
src/compat/_internal/objectProto.ts
Normal file
@ -0,0 +1 @@
|
||||
export const objectProto: any = Object.prototype;
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user