├── .gitignore ├── .travis.yml ├── History.md ├── Readme.md ├── index.js ├── package-lock.json ├── package.json ├── support └── bench.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .nyc_output 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 8 5 | - 10 6 | - 12 7 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 0.1.1 / 2014-05-23 3 | ================== 4 | 5 | * fix inheritance shadowing 6 | 7 | 0.1.0 / 2014-05-17 8 | ================== 9 | 10 | * add function binding 11 | * debug 12 | 13 | 0.0.1 / 2014-05-17 14 | ================== 15 | 16 | * fix object check 17 | * add history 18 | 19 | 0.0.0 / 2014-05-17 20 | ================== 21 | 22 | * refactor 23 | * docs 24 | * add nested object logic to getOwnPropertyDescriptor 25 | * add passing test for deleting a cloned value 26 | * add passing test for overriding a nested value 27 | * clone nested objects when getting 28 | * add failing test for setting nested values 29 | * add set after delete test + refactor 30 | * extend delete test 31 | * add ci 32 | * add delete support 33 | * use assert module from npm for better debugging 34 | * add passing override test 35 | * initial commit 36 | 37 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | # proxy-clone [![build status](https://secure.travis-ci.org/juliangruber/proxy-clone.svg)](http://travis-ci.org/juliangruber/proxy-clone) 3 | 4 | [ES6 Proxies](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) based deep clone, that's way more efficient than the traditional clone implementations when dealing with big objects. 5 | 6 | Requires node 6 or later. 7 | 8 | Note: This is not a traditional clone(). Changes to the source object will be reflected in the clone, changes to the clone however won't modify the source. 9 | 10 | ## Performance 11 | 12 | Depending on the object size, `proxy-clone` can be a lot faster than naive JSON clone or the [clone](https://npmjs.org/clone) module from npm. The most important thing to note is that clone speed is constant, however using the cloned object is slightly slower. 13 | 14 | ``` 15 | JSON small x 60,036 ops/sec ±1.09% (92 runs sampled) 16 | JSON medium x 5,919 ops/sec ±0.86% (91 runs sampled) 17 | JSON big x 526 ops/sec ±1.20% (89 runs sampled) 18 | JSON gigantic x 39.50 ops/sec ±1.83% (54 runs sampled) 19 | 20 | clone small x 50,288 ops/sec ±1.20% (91 runs sampled) 21 | clone medium x 4,381 ops/sec ±1.03% (90 runs sampled) 22 | clone big x 230 ops/sec ±0.85% (85 runs sampled) 23 | clone gigantic x 4.14 ops/sec ±1.54% (15 runs sampled) 24 | 25 | proxy-clone small x 842,147 ops/sec ±1.18% (93 runs sampled) 26 | proxy-clone medium x 891,579 ops/sec ±1.49% (87 runs sampled) 27 | proxy-clone big x 814,796 ops/sec ±0.83% (92 runs sampled) 28 | proxy-clone gigantic x 792,461 ops/sec ±0.79% (89 runs sampled) 29 | ``` 30 | 31 | ## Installation 32 | 33 | ```bash 34 | $ npm install proxy-clone 35 | ``` 36 | 37 | ## Stability 38 | 39 | This module makes certain assumptions about what you do with the cloned object, and I only tested it with the operations one project required. If something behaves odly, open an issue and I'll look into it. 40 | 41 | ## Example 42 | 43 | The api is what you'd expect: 44 | 45 | ```js 46 | var clone = require('proxy-clone'); 47 | var assert = require('assert'); 48 | 49 | var obj = { 50 | foo: { 51 | bar: 'baz' 52 | } 53 | }; 54 | 55 | var cloned = clone(obj); 56 | assert.deepEqual(cloned, obj); 57 | ``` 58 | 59 | ## API 60 | 61 | ### clone(obj) 62 | 63 | Return a deep clone of `obj`. 64 | 65 | ## How it works 66 | 67 | Traditional clone implementations recursively iterate over the object and 68 | copy all the properties to a new object, which can be slow. This module 69 | instead creates an ES6 70 | [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy). When you 71 | change a property on the proxy, it adds it to an internal change log. When 72 | you read from the proxy, it first checks for overrides, otherwise returns 73 | the original value from the object. 74 | 75 | 76 | ## Kudos 77 | 78 | Thanks to @segmentio for letting me publish this private module that I developed while working for them. 79 | 80 | 81 | ## License 82 | 83 | MIT 84 | 85 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | const debug = require('debug')('proxy-clone') 8 | 9 | /** 10 | * Unique utility. 11 | */ 12 | 13 | const unique = (el, i, arr) => arr.indexOf(el) === i 14 | 15 | /** 16 | * Object check. 17 | */ 18 | 19 | const isObject = obj => typeof obj === 'object' && obj !== null 20 | 21 | /** 22 | * Clone `obj`. 23 | * 24 | * @param {Object} obj 25 | * @return {Object} 26 | */ 27 | 28 | const proxyClone = obj => { 29 | const override = Object.create(null) 30 | const deleted = Object.create(null) 31 | 32 | const get = name => { 33 | let value 34 | if (!deleted[name]) value = override[name] || obj[name] 35 | if (isObject(value)) { 36 | value = proxyClone(value) 37 | override[name] = value 38 | } 39 | if (typeof value === 'function') { 40 | value = value.bind(obj) 41 | } 42 | return value 43 | } 44 | 45 | return new Proxy(Object.prototype, { 46 | getPrototypeOf: () => Object.getPrototypeOf(obj), 47 | setPrototypeOf: () => { 48 | throw new Error('Not yet implemented: setPrototypeOf') 49 | }, 50 | isExtensible: () => { 51 | throw new Error('Not yet implemented: isExtensible') 52 | }, 53 | preventExtensions: () => { 54 | throw new Error('Not yet implemented: preventExtensions') 55 | }, 56 | getOwnPropertyDescriptor: (target, name) => { 57 | let desc 58 | if (!deleted[name]) { 59 | desc = 60 | Object.getOwnPropertyDescriptor(override, name) || 61 | Object.getOwnPropertyDescriptor(obj, name) 62 | } 63 | if (desc) desc.configurable = true 64 | debug('getOwnPropertyDescriptor %s = %j', name, desc) 65 | return desc 66 | }, 67 | defineProperty: () => { 68 | throw new Error('Not yet implemented: defineProperty') 69 | }, 70 | has: (_, name) => { 71 | const has = !deleted[name] && (name in override || name in obj) 72 | debug('has %s = %s', name, has) 73 | return has 74 | }, 75 | get: (receiver, name) => { 76 | const value = get(name) 77 | debug('get %s = %j', name, value) 78 | return value 79 | }, 80 | set: (_, name, val) => { 81 | delete deleted[name] 82 | override[name] = val 83 | debug('set %s = %j', name, val) 84 | return true 85 | }, 86 | deleteProperty: (_, name) => { 87 | debug('deleteProperty %s', name) 88 | deleted[name] = true 89 | delete override[name] 90 | }, 91 | ownKeys: () => { 92 | const keys = Object.keys(obj) 93 | .concat(Object.keys(override)) 94 | .filter(unique) 95 | .filter(key => !deleted[key]) 96 | debug('ownKeys %j', keys) 97 | return keys 98 | }, 99 | apply: () => { 100 | throw new Error('Not yet implemented: apply') 101 | }, 102 | construct: () => { 103 | throw new Error('Not yet implemented: construct') 104 | }, 105 | enumerate: () => { 106 | throw new Error('Not yet implemented: enumerate') 107 | } 108 | }) 109 | } 110 | 111 | /** 112 | * Expose `proxyClone`. 113 | */ 114 | 115 | module.exports = proxyClone 116 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "proxy-clone", 3 | "version": "1.0.3", 4 | "repository": "juliangruber/proxy-clone", 5 | "scripts": { 6 | "bench": "node support/bench", 7 | "test": "prettier-standard '**/*.js' && standard && tap test.js", 8 | "release": "np" 9 | }, 10 | "dependencies": { 11 | "debug": "^4.1.1" 12 | }, 13 | "devDependencies": { 14 | "benchmark": "^2.1.4", 15 | "clone": "^2.1.2", 16 | "np": "^5.2.1", 17 | "prettier-standard": "^9.1.1", 18 | "standard": "^13.0.0", 19 | "tap": "^14.4.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /support/bench.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Benchmark = require('benchmark') 4 | const proxyClone = require('..') 5 | const deepClone = require('clone') 6 | 7 | const suite = new Benchmark.Suite() 8 | 9 | const gen = size => { 10 | const o = {} 11 | for (let i = 0; i < size; i++) { 12 | o[i + 'foo'] = { bar: 'baz' } 13 | o[i + 'beep'] = ['boop', 1] 14 | } 15 | return o 16 | } 17 | 18 | const input = { 19 | small: gen(10), 20 | medium: gen(100), 21 | big: gen(1000), 22 | gigantic: gen(10000) 23 | } 24 | 25 | const use = o => { 26 | var foo = o.foo 27 | o.bar = foo 28 | } 29 | 30 | const test = (name, fn) => 31 | Object.keys(input).forEach(n => 32 | suite.add(name + ' ' + n, () => { 33 | const obj = fn(input[n]) 34 | use(obj) 35 | }) 36 | ) 37 | 38 | const jsonClone = obj => JSON.parse(JSON.stringify(obj)) 39 | 40 | test('JSON', jsonClone) 41 | test('clone', deepClone) 42 | test('proxy-clone', proxyClone) 43 | 44 | suite.on('cycle', e => console.log(String(e.target))) 45 | 46 | suite.run({ async: true }) 47 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const clone = require('./') 2 | const test = require('tap').test 3 | 4 | test('clone(obj)', t => { 5 | t.test('should clone', t => { 6 | const obj = { foo: 'bar' } 7 | const cloned = clone(obj) 8 | t.assert(cloned) 9 | t.deepEqual(cloned, obj) 10 | t.end() 11 | }) 12 | 13 | t.test('set', t => { 14 | t.test('should add a value', t => { 15 | const obj = { foo: 'bar' } 16 | const cloned = clone(obj) 17 | cloned.bar = 'baz' 18 | t.deepEqual(cloned, { foo: 'bar', bar: 'baz' }) 19 | t.deepEqual(obj, { foo: 'bar' }) 20 | t.end() 21 | }) 22 | 23 | t.test('should override a value', t => { 24 | const obj = { foo: 'bar' } 25 | const cloned = clone(obj) 26 | cloned.foo = 'baz' 27 | t.deepEqual(cloned, { foo: 'baz' }) 28 | t.deepEqual(obj, { foo: 'bar' }) 29 | t.end() 30 | }) 31 | 32 | t.test('should add a previously deleted value', t => { 33 | const obj = { foo: 'bar' } 34 | const cloned = clone(obj) 35 | delete cloned.foo 36 | cloned.foo = 'baz' 37 | t.deepEqual(cloned, { foo: 'baz' }) 38 | t.deepEqual(obj, { foo: 'bar' }) 39 | t.end() 40 | }) 41 | 42 | t.test('should add a nested value', t => { 43 | const obj = { foo: { bar: 'baz' } } 44 | const cloned = clone(obj) 45 | cloned.foo.beep = 'boop' 46 | t.deepEqual(cloned, { foo: { bar: 'baz', beep: 'boop' } }) 47 | t.deepEqual(obj, { foo: { bar: 'baz' } }) 48 | t.end() 49 | }) 50 | 51 | t.test('should override a nested value', t => { 52 | const obj = { foo: { bar: 'baz' } } 53 | const cloned = clone(obj) 54 | cloned.foo.bar = 'beep' 55 | t.deepEqual(cloned, { foo: { bar: 'beep' } }) 56 | t.deepEqual(obj, { foo: { bar: 'baz' } }) 57 | t.end() 58 | }) 59 | 60 | t.test('should not clone nulls', t => { 61 | const obj = { foo: null } 62 | const cloned = clone(obj) 63 | t.deepEqual(cloned, { foo: null }) 64 | t.end() 65 | }) 66 | 67 | t.end() 68 | }) 69 | 70 | t.test('delete', t => { 71 | t.test('should delete a value', t => { 72 | const obj = { foo: 'bar' } 73 | const cloned = clone(obj) 74 | delete cloned.foo 75 | delete cloned.unknown 76 | t.deepEqual(cloned, {}) 77 | t.deepEqual(obj, { foo: 'bar' }) 78 | t.end() 79 | }) 80 | 81 | t.test('should delete a nested value', t => { 82 | const obj = { foo: { bar: 'baz' } } 83 | const cloned = clone(obj) 84 | delete cloned.foo.bar 85 | delete cloned.foo.unknown 86 | t.deepEqual(cloned, { foo: {} }) 87 | t.deepEqual(obj, { foo: { bar: 'baz' } }) 88 | t.end() 89 | }) 90 | t.end() 91 | }) 92 | 93 | t.test('get', t => { 94 | t.test('should bind functions', t => { 95 | function Obj () {} 96 | Obj.prototype.fn = function () { 97 | t.assert(this instanceof Obj) 98 | } 99 | const obj = new Obj() 100 | const cloned = clone(obj) 101 | cloned.fn() 102 | t.end() 103 | }) 104 | 105 | t.test('should inherit', t => { 106 | const d = new Date() 107 | const c = clone(d) 108 | t.deepEqual(d.toString(), c.toString()) 109 | t.end() 110 | }) 111 | t.end() 112 | }) 113 | 114 | t.end() 115 | }) 116 | --------------------------------------------------------------------------------