Skip to content

Commit 85b7ed6

Browse files
committed
improve performance of strict copies
1 parent e9a08c7 commit 85b7ed6

File tree

4 files changed

+74
-88
lines changed

4 files changed

+74
-88
lines changed

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ _Small number of properties, all values are primitives_
311311
| fast-clone | 1,635,636 |
312312
| ramda | 1,143,794 |
313313
| deepclone | 1,253,298 |
314-
| fast-copy (strict) | 1,007,193 |
314+
| fast-copy (strict) | 1,161,882 |
315315

316316
#### Complex objects
317317

@@ -324,8 +324,8 @@ _Large number of properties, values are a combination of primitives and complex
324324
| fast-clone | 99,246 |
325325
| clone | 82,487 |
326326
| ramda | 78,805 |
327+
| fast-copy (strict) | 70,160 |
327328
| lodash.cloneDeep | 66,068 |
328-
| fast-copy (strict) | 65,617 |
329329

330330
#### Big data
331331

@@ -337,8 +337,8 @@ _Very large number of properties with high amount of nesting, mainly objects and
337337
| fast-clone | 265 |
338338
| lodash.cloneDeep | 165 |
339339
| deepclone | 149 |
340+
| fast-copy (strict) | 133 |
340341
| clone | 122 |
341-
| fast-copy (strict) | 120 |
342342
| ramda | 39 |
343343

344344
#### Circular objects
@@ -351,7 +351,7 @@ _Objects that deeply reference themselves_
351351
| deepclone | 1,285,548 |
352352
| lodash.cloneDeep | 1,104,529 |
353353
| clone | 1,103,213 |
354-
| fast-copy (strict) | 757,758 |
354+
| fast-copy (strict) | 1,028,220 |
355355
| ramda | 388,033 |
356356
| fast-clone | 0 (not supported) |
357357

@@ -367,7 +367,7 @@ _Custom constructors, React components, etc_
367367
| fast-clone | 58,831 |
368368
| deepclone | 25,780 |
369369
| ramda | 25,309 |
370-
| fast-copy (strict) | 17,766 |
370+
| fast-copy (strict) | 20,480 |
371371

372372
## Development
373373

eslint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export default defineConfig([
2525
'error',
2626
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
2727
],
28+
'@typescript-eslint/prefer-for-of': 'off',
2829
'@typescript-eslint/unbound-method': 'off',
2930
},
3031
settings: {

src/copier.ts

Lines changed: 64 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -11,51 +11,42 @@ export interface State {
1111
prototype: any;
1212
}
1313

14-
const {
15-
defineProperty,
16-
getOwnPropertyDescriptor,
17-
getOwnPropertyNames,
18-
getOwnPropertySymbols,
19-
} = Object;
2014
const { hasOwnProperty, propertyIsEnumerable } = Object.prototype;
2115

22-
const SUPPORTS_SYMBOL = typeof getOwnPropertySymbols === 'function';
23-
24-
function getStrictPropertiesModern(object: any): Array<string | symbol> {
25-
return (getOwnPropertyNames(object) as Array<string | symbol>).concat(
26-
getOwnPropertySymbols(object),
27-
);
28-
}
29-
3016
/**
3117
* Get the properites used when copying objects strictly. This includes both keys and symbols.
3218
*/
33-
const getStrictProperties = SUPPORTS_SYMBOL
34-
? getStrictPropertiesModern
35-
: getOwnPropertyNames;
19+
const getStrictProperties = ((getNames, getSymbols) => {
20+
if (typeof getSymbols === 'function') {
21+
return (object: object): Array<string | symbol> => {
22+
const names = getNames(object) as Array<string | symbol>;
23+
const symbols = getSymbols(object);
24+
25+
return symbols.length ? names.concat(symbols) : names;
26+
};
27+
}
28+
29+
return getNames;
30+
})(Object.getOwnPropertyNames, Object.getOwnPropertySymbols);
3631

3732
/**
3833
* Striclty copy all properties contained on the object.
3934
*/
40-
function copyOwnPropertiesStrict<Value>(
35+
function copyOwnPropertiesStrict<Value extends object>(
4136
value: Value,
4237
clone: Value,
4338
state: State,
4439
): Value {
4540
const properties = getStrictProperties(value);
4641

47-
for (
48-
let index = 0, length = properties.length, property, descriptor;
49-
index < length;
50-
++index
51-
) {
52-
property = properties[index];
42+
for (let index = 0; index < properties.length; ++index) {
43+
const property = properties[index];
5344

5445
if (property === 'callee' || property === 'caller') {
5546
continue;
5647
}
5748

58-
descriptor = getOwnPropertyDescriptor(value, property);
49+
const descriptor = Object.getOwnPropertyDescriptor(value, property);
5950

6051
if (!descriptor) {
6152
// In extra edge cases where the property descriptor cannot be retrived, fall back to
@@ -70,7 +61,7 @@ function copyOwnPropertiesStrict<Value>(
7061
}
7162

7263
try {
73-
defineProperty(clone, property, descriptor);
64+
Object.defineProperty(clone, property, descriptor);
7465
} catch {
7566
// Tee above can fail on node in edge cases, so fall back to the loose assignment.
7667
(clone as any)[property] = descriptor.value;
@@ -89,7 +80,7 @@ export function copyArrayLoose(array: any[], state: State) {
8980
// set in the cache immediately to be able to reuse the object recursively
9081
state.cache.set(array, clone);
9182

92-
for (let index = 0, length = array.length; index < length; ++index) {
83+
for (let index = 0; index < array.length; ++index) {
9384
clone[index] = state.copier(array[index], state);
9485
}
9586

@@ -177,62 +168,58 @@ export function copyMapStrict<Value extends Map<any, any>>(
177168
return copyOwnPropertiesStrict(map, copyMapLoose(map, state), state);
178169
}
179170

180-
function copyObjectLooseLegacy<Value extends Record<string, any>>(
181-
object: Value,
182-
state: State,
183-
): Value {
184-
const clone: any = getCleanClone(state.prototype);
185-
186-
// set in the cache immediately to be able to reuse the object recursively
187-
state.cache.set(object, clone);
188-
189-
for (const key in object) {
190-
if (hasOwnProperty.call(object, key)) {
191-
clone[key] = state.copier(object[key], state);
192-
}
193-
}
194-
195-
return clone;
196-
}
197-
198-
function copyObjectLooseModern<Value extends Record<string, any>>(
199-
object: Value,
200-
state: State,
201-
): Value {
202-
const clone = getCleanClone(state.prototype);
203-
204-
// set in the cache immediately to be able to reuse the object recursively
205-
state.cache.set(object, clone);
206-
207-
for (const key in object) {
208-
if (hasOwnProperty.call(object, key)) {
209-
clone[key] = state.copier(object[key], state);
210-
}
171+
/**
172+
* Deeply copy the properties (keys and symbols) and values of the original.
173+
*/
174+
export const copyObjectLoose = ((getSymbols) => {
175+
if (typeof getSymbols === 'function') {
176+
return <Value extends Record<string, any>>(
177+
object: Value,
178+
state: State,
179+
): Value => {
180+
const clone = getCleanClone(state.prototype);
181+
182+
// set in the cache immediately to be able to reuse the object recursively
183+
state.cache.set(object, clone);
184+
185+
for (const key in object) {
186+
if (hasOwnProperty.call(object, key)) {
187+
clone[key] = state.copier(object[key], state);
188+
}
189+
}
190+
191+
const symbols = getSymbols(object);
192+
193+
for (let index = 0; index < symbols.length; ++index) {
194+
const symbol = symbols[index];
195+
196+
if (propertyIsEnumerable.call(object, symbol)) {
197+
clone[symbol] = state.copier((object as any)[symbol], state);
198+
}
199+
}
200+
201+
return clone;
202+
};
211203
}
212204

213-
const symbols = getOwnPropertySymbols(object);
205+
return <Value extends Record<string, any>>(
206+
object: Value,
207+
state: State,
208+
): Value => {
209+
const clone: any = getCleanClone(state.prototype);
214210

215-
for (
216-
let index = 0, length = symbols.length, symbol;
217-
index < length;
218-
++index
219-
) {
220-
symbol = symbols[index];
211+
// set in the cache immediately to be able to reuse the object recursively
212+
state.cache.set(object, clone);
221213

222-
if (propertyIsEnumerable.call(object, symbol)) {
223-
clone[symbol] = state.copier((object as any)[symbol], state);
214+
for (const key in object) {
215+
if (hasOwnProperty.call(object, key)) {
216+
clone[key] = state.copier(object[key], state);
217+
}
224218
}
225-
}
226219

227-
return clone;
228-
}
229-
230-
/**
231-
* Deeply copy the properties (keys and symbols) and values of the original.
232-
*/
233-
export const copyObjectLoose = SUPPORTS_SYMBOL
234-
? copyObjectLooseModern
235-
: copyObjectLooseLegacy;
220+
return clone;
221+
};
222+
})(Object.getOwnPropertySymbols);
236223

237224
/**
238225
* Deeply copy the properties (keys and symbols) and values of the original, as well

src/index.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ import type { InternalCopier, State } from './copier';
2121

2222
export type { State } from './copier';
2323

24-
const { isArray } = Array;
25-
const { assign } = Object;
2624
// Handling extremely old environments without Object.getPrototypeOf.
2725
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
2826
const getPrototypeOf = Object.getPrototypeOf || ((obj) => obj.__proto__);
@@ -52,7 +50,7 @@ const DEFAULT_LOOSE_OPTIONS: Required<CreateCopierOptions> = {
5250
regExp: copyRegExp,
5351
set: copySetLoose,
5452
};
55-
const DEFAULT_STRICT_OPTIONS: Required<CreateCopierOptions> = assign(
53+
const DEFAULT_STRICT_OPTIONS: Required<CreateCopierOptions> = Object.assign(
5654
{},
5755
DEFAULT_LOOSE_OPTIONS,
5856
{
@@ -104,7 +102,7 @@ function getTagSpecificCopiers(
104102
* Create a custom copier based on the object-specific copy methods passed.
105103
*/
106104
export function createCopier(options: CreateCopierOptions) {
107-
const normalizedOptions = assign({}, DEFAULT_LOOSE_OPTIONS, options);
105+
const normalizedOptions = Object.assign({}, DEFAULT_LOOSE_OPTIONS, options);
108106
const tagSpecificCopiers = getTagSpecificCopiers(normalizedOptions);
109107

110108
if (!tagSpecificCopiers.Object || !tagSpecificCopiers.Array) {
@@ -136,7 +134,7 @@ export function createCopier(options: CreateCopierOptions) {
136134
}
137135

138136
// arrays
139-
if (isArray(value)) {
137+
if (Array.isArray(value)) {
140138
return copyArray(value, state);
141139
}
142140

@@ -164,7 +162,7 @@ export function createCopier(options: CreateCopierOptions) {
164162
* same internals as `copyStrict`.
165163
*/
166164
export function createStrictCopier(options: CreateCopierOptions) {
167-
return createCopier(assign({}, DEFAULT_STRICT_OPTIONS, options));
165+
return createCopier(Object.assign({}, DEFAULT_STRICT_OPTIONS, options));
168166
}
169167

170168
/**

0 commit comments

Comments
 (0)