├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── bench ├── index.js ├── looparr.js ├── loopobj.js ├── package-lock.json └── package.json ├── bower.json ├── bump.sh ├── fclone.d.ts ├── gulpfile.js ├── package-lock.json ├── package.json ├── src └── fclone.js └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | dist 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | !dist 2 | bower_components 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | branches: 3 | only: 4 | - master 5 | 6 | node_js: 7 | - "0.10" 8 | - "4" 9 | - "6" 10 | 11 | install: 12 | - npm install 13 | - npm run build 14 | 15 | matrix: 16 | fast_finish: true 17 | allow_failures: 18 | - node_js: 0.10 19 | 20 | # container-base 21 | sudo: false 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Clone javascript object without circular values 4 | Copyright © 2016 Antoine Bluchet 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the "Software"), 8 | to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 10 | and/or sell copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included 14 | in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 20 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 22 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FClone 2 | 3 | Clone objects by dropping circular references 4 | 5 | [![Build Status](https://travis-ci.org/soyuka/fclone.svg?branch=master)](https://travis-ci.org/soyuka/fclone) 6 | 7 | This module clones a Javascript object in safe mode (eg: drops circular values) recursively. Circular values are replaced with a string: `'[Circular]'`. 8 | 9 | Ideas from [tracker1/safe-clone-deep](https://github.com/tracker1/safe-clone-deep). I improved the workflow a bit by: 10 | - refactoring the code (complete rewrite) 11 | - fixing node 6+ 12 | - micro optimizations 13 | - use of `Array.isArray` and `Buffer.isBuffer` 14 | 15 | Node 0.10 compatible, distributed files are translated to es2015. 16 | 17 | ## Installation 18 | 19 | ```bash 20 | npm install fclone 21 | # or 22 | bower install fclone 23 | ``` 24 | 25 | ## Usage 26 | 27 | ```javascript 28 | const fclone = require('fclone'); 29 | 30 | let a = {c: 'hello'}; 31 | a.b = a; 32 | 33 | let o = fclone(a); 34 | 35 | console.log(o); 36 | // outputs: { c: 'hello', b: '[Circular]' } 37 | 38 | //JSON.stringify is now safe 39 | console.log(JSON.stringify(o)); 40 | ``` 41 | 42 | ## Benchmarks 43 | 44 | Some benchs: 45 | 46 | ``` 47 | # Clone 48 | fclone (not a string) x 14,121 ops/sec ±0.75% (89 runs sampled) 49 | clone (not a string) x 9,293 ops/sec ±0.93% (90 runs sampled) 50 | deepcopy (not a string) x 5,375 ops/sec ±0.73% (92 runs sampled) 51 | rfdc x 12,786 ops/sec ±1.31% (91 runs sampled) 52 | 53 | # Stringify 54 | fclone + json.stringify x 8,289 ops/sec ±0.74% (90 runs sampled) 55 | fast-safe-stringify x 8,241 ops/sec ±0.48% (92 runs sampled) 56 | util.inspect (outputs a string) x 2,115 ops/sec ±0.84% (89 runs sampled) 57 | jsan x 5,090 ops/sec ±0.65% (92 runs sampled) 58 | circularjson x 4,471 ops/sec ±0.67% (92 runs sampled) 59 | json-stringify-safe x 7,150 ops/sec ±0.97% (91 runs sampled) 60 | Fastest is fclone (not a string) 61 | ``` 62 | -------------------------------------------------------------------------------- /bench/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const Benchmark = require('benchmark') 3 | const suite = new Benchmark.Suite 4 | 5 | function Complex() { 6 | this.bar = 'foo' 7 | this.foo = new Array(100).fill(0).map(e => Math.random()) 8 | } 9 | 10 | Complex.prototype.method = function() {} 11 | 12 | const a = {b: 'hello', c: {foo: 'bar', bar: 'foo', error: new Error('Test')}, complex: new Complex()} 13 | 14 | a.a = a 15 | a.c.complex = a.complex 16 | a.env = process.env 17 | 18 | const fclone = require('../dist/fclone.js') 19 | const clone = require('clone') 20 | const deepcopy = require('deepcopy') 21 | const jsonstringifysafe = require('json-stringify-safe') 22 | const jsan = require('jsan') 23 | const circularjson = require('circular-json-es6') 24 | const util = require('util') 25 | const fastsafestringify = require('fast-safe-stringify') 26 | const rfdc = require('rfdc')({circles: true}) 27 | 28 | suite 29 | .add('fclone (not a string)', function() { 30 | let b = fclone(a) 31 | }) 32 | .add('fclone + json.stringify', function() { 33 | let b = JSON.stringify(fclone(a)) 34 | }) 35 | .add('rfdc', function() { 36 | let b = rfdc(a) 37 | }) 38 | .add('fast-safe-stringify', function() { 39 | let b = fastsafestringify(a) 40 | }) 41 | .add('util.inspect (outputs a string)', function() { 42 | let b = util.inspect(a) 43 | }) 44 | .add('jsan', function() { 45 | let b = jsan.stringify(a) 46 | }) 47 | .add('circularjson', function() { 48 | let b = circularjson.stringify(a) 49 | }) 50 | .add('deepcopy (not a string)', function() { 51 | let b = deepcopy(a) 52 | }) 53 | .add('json-stringify-safe', function() { 54 | let b = jsonstringifysafe(a) 55 | }) 56 | .add('clone (not a string)', function() { 57 | let b = clone(a) 58 | }) 59 | .on('cycle', function(event) { 60 | console.log(String(event.target)) 61 | }) 62 | .on('complete', function() { 63 | console.log('Fastest is ' + this.filter('fastest').map('name')) 64 | }) 65 | .run({ 'async': true }) 66 | -------------------------------------------------------------------------------- /bench/looparr.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const Benchmark = require('benchmark') 3 | const suite = new Benchmark.Suite 4 | 5 | let arr = new Array(100).fill(Math.random() * 100) 6 | 7 | suite 8 | .add('for', function() { 9 | let l = arr.length 10 | for (let i = 0; i < l; i++) { 11 | let o = arr[i] 12 | } 13 | }) 14 | .add('while --', function() { 15 | let l = arr.length 16 | while(l--) { 17 | let o = arr[l] 18 | } 19 | }) 20 | .add('while ++', function() { 21 | let l = arr.length 22 | let i = -1 23 | while(l > ++i) { 24 | let o = arr[i] 25 | } 26 | }) 27 | .on('cycle', function(event) { 28 | console.log(String(event.target)) 29 | }) 30 | .on('complete', function() { 31 | console.log('Fastest is ' + this.filter('fastest').map('name')) 32 | }) 33 | .run({ 'async': true }) 34 | -------------------------------------------------------------------------------- /bench/loopobj.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const Benchmark = require('benchmark') 3 | const suite = new Benchmark.Suite 4 | 5 | let obj = process.env 6 | 7 | suite 8 | .add('for in', function() { 9 | for(let i in obj) { 10 | let o = obj[i] 11 | } 12 | }) 13 | .add('while --', function() { 14 | let keys = Object.keys(obj) 15 | let l = keys.length 16 | while(l--) { 17 | let o = obj[keys[l]] 18 | } 19 | }) 20 | .add('while shift', function() { 21 | let keys = Object.keys(obj) 22 | let k 23 | 24 | while(k = keys.shift()) { 25 | let o = obj[k] 26 | } 27 | }) 28 | .on('cycle', function(event) { 29 | console.log(String(event.target)) 30 | }) 31 | .on('complete', function() { 32 | console.log('Fastest is ' + this.filter('fastest').map('name')) 33 | }) 34 | .run({ 'async': true }) 35 | -------------------------------------------------------------------------------- /bench/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bench", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "version": "1.0.0", 9 | "license": "ISC", 10 | "dependencies": { 11 | "benchmark": "^2.1.0", 12 | "circular-json-es6": "^2.0.0", 13 | "clone": "^1.0.2", 14 | "deepcopy": "^0.6.3", 15 | "fast-safe-stringify": "^1.1.13", 16 | "jsan": "^3.1.2", 17 | "json-stringify-safe": "^5.0.1" 18 | }, 19 | "devDependencies": { 20 | "rfdc": "^1.3.0" 21 | } 22 | }, 23 | "node_modules/benchmark": { 24 | "version": "2.1.4", 25 | "resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz", 26 | "integrity": "sha1-CfPeMckWQl1JjMLuVloOvzwqVik=", 27 | "dependencies": { 28 | "lodash": "^4.17.4", 29 | "platform": "^1.3.3" 30 | } 31 | }, 32 | "node_modules/circular-json-es6": { 33 | "version": "2.0.2", 34 | "resolved": "https://registry.npmjs.org/circular-json-es6/-/circular-json-es6-2.0.2.tgz", 35 | "integrity": "sha512-ODYONMMNb3p658Zv+Pp+/XPa5s6q7afhz3Tzyvo+VRh9WIrJ64J76ZC4GQxnlye/NesTn09jvOiuE8+xxfpwhQ==" 36 | }, 37 | "node_modules/clone": { 38 | "version": "1.0.4", 39 | "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", 40 | "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", 41 | "engines": { 42 | "node": ">=0.8" 43 | } 44 | }, 45 | "node_modules/deepcopy": { 46 | "version": "0.6.3", 47 | "resolved": "https://registry.npmjs.org/deepcopy/-/deepcopy-0.6.3.tgz", 48 | "integrity": "sha1-Y0eA8vhlardxr4+oQx7RzO5Vx7A=" 49 | }, 50 | "node_modules/fast-safe-stringify": { 51 | "version": "1.2.3", 52 | "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-1.2.3.tgz", 53 | "integrity": "sha512-QJYT/i0QYoiZBQ71ivxdyTqkwKkQ0oxACXHYxH2zYHJEgzi2LsbjgvtzTbLi1SZcF190Db2YP7I7eTsU2egOlw==" 54 | }, 55 | "node_modules/jsan": { 56 | "version": "3.1.13", 57 | "resolved": "https://registry.npmjs.org/jsan/-/jsan-3.1.13.tgz", 58 | "integrity": "sha512-9kGpCsGHifmw6oJet+y8HaCl14y7qgAsxVdV3pCHDySNR3BfDC30zgkssd7x5LRVAT22dnpbe9JdzzmXZnq9/g==" 59 | }, 60 | "node_modules/json-stringify-safe": { 61 | "version": "5.0.1", 62 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 63 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 64 | }, 65 | "node_modules/lodash": { 66 | "version": "4.17.21", 67 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 68 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 69 | }, 70 | "node_modules/platform": { 71 | "version": "1.3.6", 72 | "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", 73 | "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==" 74 | }, 75 | "node_modules/rfdc": { 76 | "version": "1.3.0", 77 | "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", 78 | "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", 79 | "dev": true 80 | } 81 | }, 82 | "dependencies": { 83 | "benchmark": { 84 | "version": "2.1.4", 85 | "resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz", 86 | "integrity": "sha1-CfPeMckWQl1JjMLuVloOvzwqVik=", 87 | "requires": { 88 | "lodash": "^4.17.4", 89 | "platform": "^1.3.3" 90 | } 91 | }, 92 | "circular-json-es6": { 93 | "version": "2.0.2", 94 | "resolved": "https://registry.npmjs.org/circular-json-es6/-/circular-json-es6-2.0.2.tgz", 95 | "integrity": "sha512-ODYONMMNb3p658Zv+Pp+/XPa5s6q7afhz3Tzyvo+VRh9WIrJ64J76ZC4GQxnlye/NesTn09jvOiuE8+xxfpwhQ==" 96 | }, 97 | "clone": { 98 | "version": "1.0.4", 99 | "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", 100 | "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" 101 | }, 102 | "deepcopy": { 103 | "version": "0.6.3", 104 | "resolved": "https://registry.npmjs.org/deepcopy/-/deepcopy-0.6.3.tgz", 105 | "integrity": "sha1-Y0eA8vhlardxr4+oQx7RzO5Vx7A=" 106 | }, 107 | "fast-safe-stringify": { 108 | "version": "1.2.3", 109 | "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-1.2.3.tgz", 110 | "integrity": "sha512-QJYT/i0QYoiZBQ71ivxdyTqkwKkQ0oxACXHYxH2zYHJEgzi2LsbjgvtzTbLi1SZcF190Db2YP7I7eTsU2egOlw==" 111 | }, 112 | "jsan": { 113 | "version": "3.1.13", 114 | "resolved": "https://registry.npmjs.org/jsan/-/jsan-3.1.13.tgz", 115 | "integrity": "sha512-9kGpCsGHifmw6oJet+y8HaCl14y7qgAsxVdV3pCHDySNR3BfDC30zgkssd7x5LRVAT22dnpbe9JdzzmXZnq9/g==" 116 | }, 117 | "json-stringify-safe": { 118 | "version": "5.0.1", 119 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 120 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 121 | }, 122 | "lodash": { 123 | "version": "4.17.21", 124 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 125 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 126 | }, 127 | "platform": { 128 | "version": "1.3.6", 129 | "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", 130 | "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==" 131 | }, 132 | "rfdc": { 133 | "version": "1.3.0", 134 | "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", 135 | "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", 136 | "dev": true 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /bench/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bench", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "benchmark": "^2.1.0", 14 | "circular-json-es6": "^2.0.0", 15 | "clone": "^1.0.2", 16 | "deepcopy": "^0.6.3", 17 | "fast-safe-stringify": "^1.1.13", 18 | "jsan": "^3.1.2", 19 | "json-stringify-safe": "^5.0.1" 20 | }, 21 | "devDependencies": { 22 | "rfdc": "^1.3.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fclone", 3 | "description": "Fastest JSON cloning module that handles circular references", 4 | "main": "dist/fclone.js", 5 | "authors": [ 6 | "soyuka" 7 | ], 8 | "license": "MIT", 9 | "keywords": [ 10 | "clone", 11 | "deep", 12 | "circular", 13 | "json", 14 | "stringify", 15 | "fast" 16 | ], 17 | "homepage": "https://github.com/soyuka/fclone", 18 | "ignore": [ 19 | "**/.*", 20 | "node_modules", 21 | "bower_components", 22 | "test.js", 23 | "!dist/dpicker.js", 24 | "!dist/dpicker.min.js", 25 | "screen.png", 26 | "gulpfile.js", 27 | "demo", 28 | "bump.sh" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /bump.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | [[ '' == $1 ]] && echo "Please provide patch, minor, major argument" && exit 1 4 | 5 | gulp 6 | cp fclone.d.ts dist/ 7 | newver=$(npm --no-git-tag-version version $1) 8 | git add -f dist package.json 9 | git commit -m $newver 10 | git tag $newver 11 | npm publish 12 | git reset --hard HEAD~1 13 | newver=$(npm --no-git-tag-version version $1) 14 | git add package.json 15 | git commit -m $newver 16 | git push --tags 17 | git push 18 | -------------------------------------------------------------------------------- /fclone.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'fclone' { 2 | export default function fclone(obj: T): T; 3 | } 4 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | var gulp = require('gulp') 3 | var wrap = require('gulp-wrap') 4 | var rename = require('gulp-rename') 5 | var babel = require('gulp-babel') 6 | var uglify = require('gulp-uglify') 7 | 8 | var umd = [ 9 | "(function (root, factory) {", 10 | " if (typeof define === 'function' && define.amd) {", 11 | " // AMD", 12 | " define('fclone', [], factory);", 13 | " } else if (typeof module === 'object' && module.exports) {", 14 | " //node", 15 | " module.exports = factory();", 16 | " } else {", 17 | " // Browser globals (root is window)", 18 | " root.fclone = factory();", 19 | " }", 20 | "}(this, function () {", 21 | " <%= contents %>", 22 | " return fclone", 23 | "}));"].join(require('os').EOL); 24 | 25 | gulp.task('default', function() { 26 | return gulp.src(['src/fclone.js']) 27 | .pipe(babel({presets: ['es2015']})) 28 | .pipe(wrap(umd)) 29 | .pipe(gulp.dest('dist')) 30 | .pipe(uglify()) 31 | .pipe(rename('fclone.min.js')) 32 | .pipe(gulp.dest('dist')) 33 | }) 34 | 35 | gulp.task('watch', gulp.series('default', function() { 36 | gulp.watch('src/*.js', ['default']) 37 | })) 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fclone", 3 | "version": "1.0.11", 4 | "description": "Clone objects by dropping circular references", 5 | "main": "dist/fclone", 6 | "scripts": { 7 | "test": "./node_modules/.bin/_mocha", 8 | "build": "./node_modules/.bin/gulp" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/soyuka/fclone.git" 13 | }, 14 | "keywords": [ 15 | "clone", 16 | "deep", 17 | "circular", 18 | "json", 19 | "stringify", 20 | "fast" 21 | ], 22 | "author": "soyuka ", 23 | "license": "MIT", 24 | "devDependencies": { 25 | "babel-preset-es2015": "^6.9.0", 26 | "chai": "^3.5.0", 27 | "gulp": "^4.0.0", 28 | "gulp-babel": "^6.1.2", 29 | "gulp-rename": "^1.2.2", 30 | "gulp-uglify": "^1.5.4", 31 | "gulp-wrap": "^0.13.0", 32 | "mocha": "^2.5.3" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/fclone.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // see if it looks and smells like an iterable object, and do accept length === 0 4 | function isArrayLike(item) { 5 | if (Array.isArray(item)) return true; 6 | 7 | const len = item && item.length; 8 | return typeof len === 'number' && (len === 0 || (len - 1) in item) && typeof item.indexOf === 'function'; 9 | } 10 | 11 | function fclone(obj, refs) { 12 | if (!obj || "object" !== typeof obj) return obj; 13 | 14 | if (obj instanceof Date) { 15 | return new Date(obj); 16 | } 17 | 18 | if (typeof Buffer !== 'undefined' && Buffer.isBuffer(obj)) { 19 | return Buffer.from(obj); 20 | } 21 | 22 | // typed array Int32Array etc. 23 | if (ArrayBuffer.isView(obj)) { 24 | return obj.subarray(0); 25 | } 26 | 27 | if (!refs) { refs = []; } 28 | 29 | if (isArrayLike(obj)) { 30 | refs[refs.length] = obj; 31 | let l = obj.length; 32 | let i = -1; 33 | let copy = []; 34 | 35 | while (l > ++i) { 36 | copy[i] = ~refs.indexOf(obj[i]) ? '[Circular]' : fclone(obj[i], refs); 37 | } 38 | 39 | refs.length && refs.length--; 40 | return copy; 41 | } 42 | 43 | refs[refs.length] = obj; 44 | let copy = {}; 45 | 46 | if (obj instanceof Error) { 47 | copy.name = obj.name; 48 | copy.message = obj.message; 49 | copy.stack = obj.stack; 50 | } 51 | 52 | let keys = Object.keys(obj); 53 | let l = keys.length; 54 | 55 | while(l--) { 56 | let k = keys[l]; 57 | copy[k] = ~refs.indexOf(obj[k]) ? '[Circular]' : fclone(obj[k], refs); 58 | } 59 | 60 | refs.length && refs.length--; 61 | return copy; 62 | } 63 | 64 | fclone.default = fclone 65 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var expect = require('chai').expect 4 | var clone = require('../dist/fclone.js') 5 | 6 | describe('fclone', function(){ 7 | var input, output 8 | 9 | beforeEach(function(){ 10 | var a = {} 11 | a.a = a 12 | a.b = {} 13 | a.b.a = a 14 | a.b.b = a.b 15 | a.c = {} 16 | a.c.b = a.b 17 | a.c.c = a.c 18 | a.x = 1 19 | a.b.x = 2 20 | a.c.x = 3 21 | a.d = [0,a,1,a.b,2,a.c,3] 22 | input = a 23 | }) 24 | 25 | describe('will clone', function(){ 26 | 27 | it('a string', function() { 28 | var i = '' 29 | var o = clone(i) 30 | expect(o).to.equal(i) 31 | }) 32 | 33 | it('an object', function() { 34 | var t = {foo: 'bar', bar: 'foo'} 35 | var o = clone(t) 36 | 37 | delete t.foo 38 | 39 | expect(t.foo).to.be.undefined 40 | expect(o.foo).to.equal('bar') 41 | }) 42 | 43 | it('a Date', function(){ 44 | var a = new Date() 45 | var b = clone(a) 46 | expect(a).to.deep.equal(b) 47 | expect(a).to.not.equal(b) 48 | }) 49 | 50 | it('a Buffer', function(){ 51 | var a = Buffer.from('this is a test') 52 | var b = clone(a) 53 | expect(a.toString()).to.equal(b.toString()) 54 | expect(a).to.not.equal(b) 55 | }) 56 | 57 | it('an Error\'s properties', function(){ 58 | var a = new Error("this is a test") 59 | var b = clone(a) 60 | 61 | expect(a).to.not.equal(b) 62 | expect(b).to.have.property('name', a.name) 63 | expect(b).to.have.property('message', a.message) 64 | expect(b).to.have.property('stack', a.stack) 65 | }) 66 | 67 | it('an inherited property', function(){ 68 | function Base(){ 69 | this.base = true 70 | } 71 | function Child(){ 72 | this.child = true 73 | } 74 | Child.prototype = new Base() 75 | 76 | var z = clone(new Child()) 77 | expect(z).to.have.property('child',true) 78 | expect(z).not.to.have.property('base') 79 | }) 80 | 81 | it('an Uint8Array', function() { 82 | var t = new Uint8Array(3) 83 | ;[0,1,2].map(function(e) { 84 | t[e] = 0 85 | }) 86 | 87 | var o = clone(t) 88 | 89 | expect(o).to.be.an.instanceof(Uint8Array) 90 | expect(o).to.have.length.of(3) 91 | }) 92 | 93 | it('an array-like object', function() { 94 | var t = {length: 3, 0: 'test', 1: 'test', 2: 'test'} 95 | 96 | var o = clone(t) 97 | 98 | expect(o).to.deep.equal(t) 99 | }) 100 | 101 | it('a uint8array like', function() { 102 | var t = {subarray: function() { return 'fail'}} 103 | var o = clone(t) 104 | 105 | expect(o).not.to.equal('fail') 106 | expect(o.subarray()).to.equal('fail') 107 | }) 108 | }) 109 | 110 | describe('will not clone circular data', function(){ 111 | beforeEach(function(){ 112 | output = clone(input) 113 | }) 114 | 115 | it('base object', function(){ 116 | expect(output).to.have.property('a','[Circular]') 117 | expect(output).to.have.property('b') 118 | expect(output).to.have.property('x',1) 119 | expect(output).to.have.property('c') 120 | }) 121 | 122 | it('nested property', function(){ 123 | expect(output.b).to.exist 124 | expect(output.b).to.have.property('a','[Circular]') 125 | expect(output.b).to.have.property('b','[Circular]') 126 | expect(output.b).to.have.property('x',2) 127 | }) 128 | 129 | it('secondary nested property', function(){ 130 | expect(output.c).to.exist 131 | expect(output.c).to.not.have.property('a') 132 | expect(output.c).to.have.property('b') 133 | expect(output.c).to.have.property('c','[Circular]') 134 | expect(output.c.b).to.deep.equal({a: '[Circular]', b: '[Circular]', x: 2}) 135 | expect(output.c).to.have.property('x',3) 136 | }) 137 | }) 138 | }) 139 | --------------------------------------------------------------------------------