lukeed / klona Goto Github PK
View Code? Open in Web Editor NEWA tiny (240B to 501B) and fast utility to "deep clone" Objects, Arrays, Dates, RegExps, and more!
License: MIT License
A tiny (240B to 501B) and fast utility to "deep clone" Objects, Arrays, Dates, RegExps, and more!
License: MIT License
thx @lukeed , i like this repo.
maybe we should support blob, but it's uneasy for test. if you accept it, i will make a pr.
if (str === '[object Blob]') return x.slice();
While testing a tool that uses klona/lite
, I realized the library did not clone objects created within iframes.
There's a bigger discussion here about how prototypes are different for similar objects when created in different iframes (or within an iframe versus the top page).
When I create an object within an iframe:
this snippet https://github.com/lukeed/klona/blob/master/src/lite.js#L7-L13 is executed, given typeof x.constructor === 'function'
and not object
when the object is created within an iframe.
Line 10 though checks if the property already exists on the temporary object, which is empty, ignoring the property I'm trying to clone.
I got around the problem by switching from klona/lite
to klona/full
as the latter doesn't include that constructor check, but I'd love to switch back to shave those extra few bites off of my final bundle
When cloning instances of classes containing instance fields, their initializers are not run:
import { klona as klonaFull } from 'klona/full';
class Foo {
foo = ( console.log( 'initializing' ), 0 );
}
const foo1 = new Foo();
const foo2 = klonaFull( foo1 );
The preceding example logs initializing
only once, even though I'd expect the log to be printed twice.
This bug is not present in klona/lite
and klona
, most likely due to the fact that they have a branch detecting constructor functions:
import { klona as klonaLite } from 'klona/lite';
import { klona } from 'klona';
class Foo {
foo = ( console.log( 'initializing' ), 0 );
}
const foo1 = new Foo();
const foo2 = klonaLite( foo1 );
const foo3 = klona( foo1 );
Here, initializing
is correctly logged three times.
This behavior has some implications. For example, TypeScript 5.0 supports the newest decorators proposal, and it transpiles such decorators to similar code. The following example:
function log( target: unknown, ctx: ClassFieldDecoratorContext ): void {
ctx.addInitializer( () => {
console.log( 'initializing' );
} );
}
class Foo {
@log
foo = 0;
}
is transpiled by the TypeScript compiler as follows:
function log(target, ctx) {
ctx.addInitializer(() => {
console.log("initializing");
});
}
let Foo = (() => {
let _instanceExtraInitializers = [];
let _foo_decorators;
let _foo_initializers = [];
return class Foo {
static {
_foo_decorators = [log];
__esDecorate(
null,
null,
_foo_decorators,
{
kind: "field",
name: "foo",
static: false,
private: false,
access: {
has: (obj) => "foo" in obj,
get: (obj) => obj.foo,
set: (obj, value) => {
obj.foo = value;
},
},
},
_foo_initializers,
_instanceExtraInitializers
);
}
foo =
(__runInitializers(this, _instanceExtraInitializers),
__runInitializers(this, _foo_initializers, 0));
};
})();
Note that some helpers (such as __esDecorate
) were omitted for brevity.
When cloning objects that use decorators using klona/full
, initializers added via ctx.addInitializer()
are not run.
for example:
const a = {b: 1, c: <span>dom</span>
}
const clone = klona(a);
...
error info: var k, tmp, str=Object.prototype.toString.call(x); // Maximum call stack size exceeded
I use strict typescript.
When I use klona/full I get the following error:
Could not find a declaration file for module 'klona/full'. 'node_modules/klona/full/index.js' implicitly has an 'any' type. Try `npm install @types/klona` if it exists or add a new declaration (.d.ts) file containing `declare module 'klona/full';`
I fixed it by creating a custom definition file, but it would be nice if it was already defined.
declare module 'klona/full' {
export function klona<T>(input: T): T;
}
https://developer.mozilla.org/en-US/docs/Web/API/structuredClone
Would be nice to see a speed & capability comparison.
Note: structuredClone() has spotty workers compatibility ATM
thx @lukeed , i like this repo.
maybe we should support blob, but it's uneasy for test. if you accept it, i will make a pr.if (str === '[object Blob]') return x.slice();Originally posted by @tooss367 in #8 (comment)
Originally posted by @tooss367 in #27 (comment)
https://github.com/GrosSacASac/utilsac/blob/master/deep.js
I hope you find it useful
Hey Luke,
Myself and a coworker have been running into an issue with klona. When we attempt to use klona with CommonJS, such as the following:
const klona = require('klona');
....
const dup = klona(data);
We get the error: klona is not a function
.
All other requires seem to be working and I've tried deconstructing the require as well. Any idea what the issue might be?
If part of the data being copied is a reference to an object that has been frozen with Object.freeze
, it's not really necessary to copy it, as it can't be mutated on either side anyway.
My suggestion is to check Object.isFrozen
, and only copy the reference if it returns true,
The point of this, is that it provides a way for the user to opt out of copying parts of the data. Not being able to do this is currently a problem for me, because there are types are not possible to copy using this function, and I don't have a way to disable it, since it's used inside another package (vee-validate)
HI, im new in the node.js field and dont know how to use this one.
can you upload step by step guide for newbies.
no problem if you feel hesitate by my answer. i know that not an issue request.
Symbols can be used as property keys, but they are ignored.
const klona = require('klona');
const mySymbol = Symbol('mySymbol');
const x = { value: 42, [mySymbol]: 'hello' };
console.log(x);
// { value: 42, [Symbol(mySymbol)]: 'hello' }
const y = klona(x);
console.log(y);
// { value: 42 }
// expected: { value: 42, [Symbol(mySymbol)]: 'hello' }
It would be very interesting to see how it performs vs the naive JSON.parse(JSON.stringify(obj))
approach.
Love the simplicity of the code @lukeed! This ticket is just 2 suggestions, not issues at all:
export const klona = target => { // ...
export default klona
So the devs can do both of these:
import klona from "klona";
import { klona } from "klona";
d.ts
file, you can use typescript
as a devDependency
to generate the d.ts
automatically from the docs. So if you do something like this:/**
* Tiny & fast deep-clone.
*
* @template TargetType
* @param {TargetType} target Target to be cloned.
* @returns {TargetType}
*/
const klona = target => { // ...
And then you run tsc
(having a tsconfig.json
which has allowJs
set to true
, and declaration
set to true
), you'll get a d.ts
file like this:
export function klona<TargetType>(target: TargetType): TargetType;
export default klona;
So if you add tsc to your build as a last step, you can work in the JS only and forget about the d.ts
.
If you want me to lend you a hand with any of this suggestions, I can create an PR for it π
The current type definition results in an error when using library with TypeScript:
TypeError: klona.default is not a function
To fix this error, type definitions should be changed to the following:
export = klona;
declare function klona<T>(val: T): T
Just like dayjs does it.
More info about the error.
I am trying to tidy our codebase and replace all of our _.deepClone stuff with Klona or with simpler solutions where it fits. Unfortunately, in one case where a deep clone is required, we have a circular ref that we cannot remove at this point. Would it be possible for Klona to ignore a circular ref or add some option how to handle these, so we could ignore it?
I expect something like
const circRef = {...}
const cloned = klona(circRef, {
onCircularReference(ref) {
return undefined
}
})
I am also happy with any package that removes circular references, but I would love to have both in one place.
if (str === '[object Blob]') return x.slice
maybe we should support blob, but it's uneasy for test. if you accept it, i will make a pr.
if (str === '[object Blob]') return x.slice();#8
Originally posted by @tooss367 in #8 (comment)Originally posted by @tooss367 in #27 (comment)
Originally posted by @tooss367 in #28 (comment)
It seems that there is no check for circular dependencies. Once a deep clone is performed on an object with circular dependencies, it will cause stack overflow.
So, like rfdc, can you provide parameters for deep clone of objects with circular dependencies?
This is the reproduction code.
Reproducible repo:
// Works with `clone-deep` too
const clone = require('clone');
const { klona } = require('klona/full');
const obj = Object.create({
method() {
return 'foo';
},
});
console.log(clone(obj).method());
console.log(klona(obj).method());
I noticed the usage of Object.prototype.toString and it seemed really interesting, but custom classes can be mistaken as other objects inadvertently through this method.
> Object.prototype.toString.call(new (class Object {}))
'[object Object]'
I am not sure if this is wanted behavior, but if there is an array that has undefined
as an element it stays as undefined.
JSON parse/stringify turns it into a null.
The same happens if a property is undefined
, but with JSON it is completely stripped away.
In the README
it says that undefined
is only from supported from the lite
, and JSON does not support undefined
anyway.
I am assuming the way klona/json
is intended to be used is we know in advance that our objects are JSON compatible in the first place, so this might be working as intended (but Map/Set are handled the same way as the JSON methods).
When running across an accidental circular reference (aka uncovering a bug) buried deep in a stack, calling klona
throws a Maximum call stack size exceeded
but with the stacktrace only containing the last n
lines from within klona
, meaning that there's no way to see what the call stack is that called klona
. This is a frustrating developer experience.
Note: this is different than #37 which wants a way to do something. I only want a useful stacktrace so I can track down and handle the error.
Imagine that you have a bug deep in your API stack that creates a circular reference:
// circular.js
const dog = { type: 'dog' }
const person = { type: 'person' }
dog.owner = person
person.pet = dog
Making a naive copy of person
e.g. JSON.parse(JSON.stringify(person))
will of course throw a circular reference exception, notably with the stacktrace intact so you can see where the clone was created and the error was thrown:
console.log(JSON.parse(JSON.stringify(person)))
^
TypeError: Converting circular structure to JSON
[...snip...]
at file:///circular.js:5:38
[...snip...]
However, when cloning person
with any of the klona importables, e.g.:
import { klona } from 'klona/json'
// ...
console.log(klona(person))
The stacktrace throws a RangeError
with the stacktrace entirely filled with these:
RangeError: Maximum call stack size exceeded
at Object.toString (<anonymous>)
at klona (/my-app/node_modules/klona/json/index.mjs:10:32)
at klona (/my-app/node_modules/klona/json/index.mjs:21:56)
at klona (/my-app/node_modules/klona/json/index.mjs:21:56)
at klona (/my-app/node_modules/klona/json/index.mjs:21:56)
at klona (/my-app/node_modules/klona/json/index.mjs:21:56)
at klona (/my-app/node_modules/klona/json/index.mjs:6:66)
at klona (/my-app/node_modules/klona/json/index.mjs:21:56)
at klona (/my-app/node_modules/klona/json/index.mjs:21:56)
at klona (/my-app/node_modules/klona/json/index.mjs:21:56)
My current workaround in any of my apps is to basically wrap klona
with a try/catch and re-throw a new error:
// clone.js
import { klona } from 'klona/json'
export const clone = obj => {
try {
return klona(obj)
} catch(error) {
throw new Error('Got an error in klona: ' + error.message)
}
}
// circular.js
import { clone } from './clone.js'
const dog = { type: 'dog' }
const person = { type: 'person' }
dog.owner = person
person.pet = dog
console.log(clone(person))
which then throws this, which has the useful bits of the stacktrace in the error:
file:///clone.js:6
throw new Error('Got an error in klona: ' + error.message)
^
Error: Got an error in klona: Maximum call stack size exceeded
at clone (file:///clone.js:6:9)
at file:///circular.js:8:13
Of course, this is a really annoying developer experience, since I have to choose between either try/catch-ing for every klona
call (with the wrapper) or completely losing the ability to find where the klona
call is happening that has a circular reference.
For the most part I've been able to track down bugs without the wrapper, knowing that I'm working in a particular flow so the problem should be around ~here, but as the application has grown in complexity it's more frustrating to run into this issue, which is why I made the wrapper and came here.
Probably the best solution would be to split the exported function and the recursed function, so the try catch happens only on the one exported function instead of every recursed call:
function _klona(val) {
// the existing code, except recursion calls _klona
}
export function klona(val) {
try {
return _klona(val)
} catch (error) {
if (error.name === 'RangeError') throw new Error('Circular reference detected')
else throw error
}
}
I'm okay using that little wrapper in my apps, but it would be a nicer developer experience to have it built in.
If you like that solution, I can make the switch and add tests etc. if you are open to that.
If not just say so here and feel free to close this issue.
(In part I'm filing an issue so that when this comes up again I'll see this and not keep bugging you about it. π )
let original = new Map([["a", {}]])
let c = klona(original)
c.get("a").x = 7
original.get("a").x // 7 oups
I know this is a weird edge case, but since Object.prototype.constructor
is mutable, shouldn't a check be done for the proper constructor before calling it?
Lines 32 to 34 in c969b50
Reproduction code (tested with both Luxon v1.25 and v2.0.2):
import { klona } from 'klona/lite';
import { DateTime } from 'luxon';
const data = {
date: DateTime.fromISO('2021-10-01'),
};
const copy = klona(data);
Expected result:
copy
is a new object, caontaining the same date
as the original. Since DateTime
is immutable, it doesn't need to copy it.
Actual result:
Exception:
luxon.js?1315:6204 Uncaught TypeError: Cannot read properties of undefined (reading 'zone')
at new DateTime (luxon.js?1315:6204)
at klona (index.mjs?ba1d:8)
at klona (index.mjs?ba1d:25)
at eval (main.ts?cd49:30)
Problem scope:
This is a problem for us, when using vee-validate, with is using klona internally, and we are using dates in our custom form components. There's no way to opt out of this behavior, which is just crashing our page after upgrading vee-validate.
Suggestion:
Obviously, it's not in the scope of this package to be compatible with everything that's out there. Although, it is questionable to call constructors of unknown classes, and then try to copy field by field. This will probably fail for a large number of classes. There should be some way to opt-out of the copying, and make klona copy by reference, which would be OK in this case. Maybe it would be possible to mark objects with a certain property, to make then "non-copyable" by klona. Even better, mark the constructor of the class in question, so that all objects of that class are copied by reference.
const clone = require('clone');
const {klona} = require('klona');
const {klona: klonaFull} = require('klona/full');
function MyPlugin() {
}
MyPlugin.prototype = {
install: function() {
console.log('install');
}
}
const options = {
plugin: new MyPlugin(),
};
console.log('oringinal', options.plugin.install);
console.log('clone', clone(options).plugin.install);
console.log('klona', klona(options).plugin.install);
console.log('klona/full', klonaFull(options).plugin.install);
Output:
oringinal [Function: install]
clone [Function: install]
klona [Function: install]
klona/full undefined
Wondering its a bug or is by design, this issue makes less-loader lost less plugin methods.
Great job again, @lukeed!
By the way, do you plan to extend with additional common types, like Date
, Buffer
, Map
, Set
?
as the above.
As with others of my modules, I think a "choose your path" option for this module would be great.
By default β aka, the regular import β a middle of the road approach should be offered that covers the majority of cases. As of today, this would be the current klona
module, supporting Objects, Arrays, Dates, RegExps, Maps, Sets, all TypedArray variants (includes Buffer
), and primitives, of course.
Using this mode will continue to look like this:
import klona from 'klona';
There will definitely be a "full" mode (name TBD) that does everything "default" does, but with these added features. I originally was going to publish klona
with these features to start, but removed them since they don't fit the 90% use case IMO.
Of course, this mode will be a bit larger (~400 bytes) and significantly slower than the current klona
β however it'll still be faster than most contenders of the current benchmark & none of them offer these extra features.
Again, this is opt-in behavior & I'm a fan of being explicit about what you need and where you need it. Using this mode will look like this:
import klona from 'klona/full'; // name TBD
There will also be "lite" or "json" mode (name TBD) for handling simpler cases. I actually think the 90% use case doesn't bother with cloning RegExps, TypedArrays, or even Maps & Sets, but "default" included them to be safe.
The lite/json mode, as the name suggests, will handle far fewer cases than the "default" and "full" counterparts. Because of this, this mode will be ~200 bytes and the fastest of the three.
I'm still debating if this mode should handle RegExp and Date. If so, then the name would have to be "lite" β otherwise only valid JSON datatypes will remain, thus making "json" a clear & obvious choice for dealing with JSON data objects.
As with "full", using this mode is an opt-in behavior and should be obvious when you've made that choice. Using this mode will look like:
import klona from 'klona/lite';
// ~ OR ~
import klona from 'klona/json';
Please leave any comments or concerns or naming suggestions that you may have.
This is planned for a minor/feature release since nothing changes to the default mode.
Thanks!
thx @lukeed , i like this repo.
maybe we should support blob, but it's uneasy for test. if you accept it, i will make a pr.
if (str === '[object Blob]') return x.slice();
Originally posted by @tooss367 in #8 (comment)
function Test () {}
Test.prototype.val = 42;
console.log(klona(new Test())); // { val: 42 }
console.log(klona(new Test()).__proto__); // {}
thx @lukeed , i like this repo.
maybe we should support blob, but it's uneasy for test. if you accept it, i will make a pr.
if (str === '[object Blob]') return x.slice();
Originally posted by @tooss367 in #8 (comment)
I added deepmerge
and run benchmark and i got this results
Validation:
β JSON.stringify (FAILED @ "initial copy")
β fast-clone (FAILED @ "initial copy")
β lodash
β clone-deep
β deep-copy (FAILED @ "initial copy")
β depcopy
β klona
β deepmerge (FAILED @ "initial copy")
Benchmark:
JSON.stringify x 20,873 ops/sec Β±3.54% (80 runs sampled)
fast-clone x 8,542 ops/sec Β±15.12% (61 runs sampled)
lodash x 21,840 ops/sec Β±7.48% (80 runs sampled)
clone-deep x 43,664 ops/sec Β±6.99% (75 runs sampled)
deep-copy x 65,898 ops/sec Β±5.09% (81 runs sampled)
depcopy x 15,026 ops/sec Β±2.96% (83 runs sampled)
klona x 149,326 ops/sec Β±3.57% (84 runs sampled)
deepmerge x 18,183,850 ops/sec Β±3.00% (80 runs sampled)
A declarative, efficient, and flexible JavaScript library for building user interfaces.
π Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. πππ
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google β€οΈ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.