Skip to content

Commit e833a07

Browse files
Copilotdayongkr
andcommitted
Fix cloneDeepWith to return Object.prototype for null prototype objects
Co-authored-by: dayongkr <[email protected]>
1 parent 4cb8c45 commit e833a07

File tree

2 files changed

+74
-17
lines changed

2 files changed

+74
-17
lines changed

src/compat/object/cloneDeepWith.spec.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,47 @@ describe('cloneDeepWith', function () {
131131
it('should match the type of lodash', () => {
132132
expectTypeOf(cloneDeepWith).toEqualTypeOf<typeof cloneDeepWithLodash>();
133133
});
134+
135+
it('should clone objects with null prototype to have Object prototype (lodash compat)', () => {
136+
const objWithNullProto = Object.create(null);
137+
objWithNullProto.a = 1;
138+
objWithNullProto.b = 2;
139+
140+
const cloned = cloneDeepWith(objWithNullProto);
141+
142+
// The cloned object should have Object.prototype, not null prototype
143+
expect(Object.getPrototypeOf(cloned)).toBe(Object.prototype);
144+
expect(cloned.a).toBe(1);
145+
expect(cloned.b).toBe(2);
146+
expect(typeof cloned.toString).toBe('function');
147+
});
148+
149+
it('should clone nested objects with null prototype to have Object prototype', () => {
150+
const obj = Object.create(null);
151+
obj.nested = Object.create(null);
152+
obj.nested.value = 42;
153+
154+
const cloned = cloneDeepWith(obj);
155+
156+
expect(Object.getPrototypeOf(cloned)).toBe(Object.prototype);
157+
expect(Object.getPrototypeOf(cloned.nested)).toBe(Object.prototype);
158+
expect(cloned.nested.value).toBe(42);
159+
});
160+
161+
it('should clone objects with null prototype using customizer', () => {
162+
const objWithNullProto = Object.create(null);
163+
objWithNullProto.a = 1;
164+
objWithNullProto.b = 2;
165+
166+
const cloned = cloneDeepWith(objWithNullProto, value => {
167+
if (typeof value === 'number') {
168+
return value * 2;
169+
}
170+
return undefined;
171+
});
172+
173+
expect(Object.getPrototypeOf(cloned)).toBe(Object.prototype);
174+
expect(cloned.a).toBe(2);
175+
expect(cloned.b).toBe(4);
176+
});
134177
});

src/compat/object/cloneDeepWith.ts

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { cloneDeepWith as cloneDeepWithToolkit } from '../../object/cloneDeepWith.ts';
22
import { copyProperties } from '../../object/cloneDeepWith.ts';
3-
import { argumentsTag, booleanTag, numberTag, stringTag } from '../_internal/tags.ts';
3+
import { argumentsTag, booleanTag, numberTag, objectTag, stringTag } from '../_internal/tags.ts';
44

55
type CloneDeepWithCustomizer<TObject> = (
66
value: any,
@@ -78,46 +78,60 @@ export function cloneDeepWith<T>(value: T): T;
7878
* console.log(clonedArr === arr); // false
7979
*/
8080
export function cloneDeepWith<T>(obj: T, customizer?: CloneDeepWithCustomizer<T>): any | T {
81-
return cloneDeepWithToolkit(obj, (value, key, object, stack) => {
81+
const internalCustomizer = (value: any, key: PropertyKey | undefined, object: T, stack: Map<any, any>): any => {
8282
const cloned = customizer?.(value, key as any, object, stack);
8383

8484
if (cloned !== undefined) {
8585
return cloned;
8686
}
8787

88-
if (typeof obj !== 'object') {
88+
if (typeof value !== 'object' || value === null) {
8989
return undefined;
9090
}
9191

92-
switch (Object.prototype.toString.call(obj)) {
92+
const tag = Object.prototype.toString.call(value);
93+
94+
switch (tag) {
9395
case numberTag:
9496
case stringTag:
9597
case booleanTag: {
96-
// eslint-disable-next-line
97-
// @ts-ignore
98-
const result = new obj.constructor(obj?.valueOf()) as T;
99-
copyProperties(result, obj);
98+
const result = new value.constructor(value?.valueOf());
99+
copyProperties(result, value);
100100
return result;
101101
}
102102

103103
case argumentsTag: {
104104
const result = {} as any;
105105

106-
copyProperties(result, obj);
106+
copyProperties(result, value);
107107

108-
// eslint-disable-next-line
109-
// @ts-ignore
110-
result.length = obj.length;
111-
// eslint-disable-next-line
112-
// @ts-ignore
113-
result[Symbol.iterator] = obj[Symbol.iterator];
108+
result.length = value.length;
109+
result[Symbol.iterator] = value[Symbol.iterator];
114110

115-
return result as T;
111+
return result;
112+
}
113+
114+
case objectTag: {
115+
// For plain objects with null prototype (Object.create(null)),
116+
// lodash returns an object with Object.prototype
117+
if (Object.getPrototypeOf(value) === null) {
118+
if (stack.has(value)) {
119+
return stack.get(value);
120+
}
121+
122+
const result = {};
123+
stack.set(value, result);
124+
copyProperties(result, value, obj, stack, internalCustomizer);
125+
return result;
126+
}
127+
return undefined;
116128
}
117129

118130
default: {
119131
return undefined;
120132
}
121133
}
122-
});
134+
};
135+
136+
return cloneDeepWithToolkit(obj, internalCustomizer);
123137
}

0 commit comments

Comments
 (0)