├── .editorconfig ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.js ├── lib └── deep-extend.js ├── package.json └── test ├── index.spec.js └── mocha.opts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | trim_trailing_whitespace = true 5 | indent_style = tab 6 | end_of_line = lf 7 | insert_final_newline = true 8 | max_line_length = 100 9 | 10 | [package.json] 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [*.md] 15 | max_line_length = 80 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: yarn 3 | node_js: 4 | - "node" 5 | - "10" 6 | - "9" 7 | - "8" 8 | - "7" 9 | - "6" 10 | - "5" 11 | - "4" 12 | - "4.0.0" # minimal supported version 13 | before_install: 14 | - npm config set strict-ssl false 15 | - npm config set registry="http://registry.npmjs.org/" 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | v0.6.0 5 | ------ 6 | 7 | - Updated "devDependencies" versions to fix vulnerability alerts 8 | - Dropped support of io.js and node.js v0.12.x and lower since new versions of 9 | "devDependencies" couldn't work with those old node.js versions 10 | (minimal supported version of node.js now is v4.0.0) 11 | 12 | v0.5.1 13 | ------ 14 | 15 | - Fix prototype pollution vulnerability (thanks to @mwakerman for the PR) 16 | - Avoid using deprecated Buffer API (thanks to @ChALkeR for the PR) 17 | 18 | v0.5.0 19 | ------ 20 | 21 | - Auto-testing provided by Travis CI; 22 | - Support older Node.JS versions (`v0.11.x` and `v0.10.x`); 23 | - Removed tests files from npm package. 24 | 25 | v0.4.2 26 | ------ 27 | 28 | - Fix for `null` as an argument. 29 | 30 | v0.4.1 31 | ------ 32 | 33 | - Removed test code from npm package 34 | ([see pull request #21](https://github.com/unclechu/node-deep-extend/pull/21)); 35 | - Increased minimal version of Node from `0.4.0` to `0.12.0` 36 | (because can't run tests on lesser version anyway). 37 | 38 | v0.4.0 39 | ------ 40 | 41 | - **WARNING!** Broken backward compatibility with `v0.3.x`; 42 | - Fixed bug with extending arrays instead of cloning; 43 | - Deep cloning for arrays; 44 | - Check for own property; 45 | - Fixed some documentation issues; 46 | - Strict JS mode. 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2018, Viacheslav Lotsmanov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Deep Extend 2 | =========== 3 | 4 | Recursive object extending. 5 | 6 | [![Build Status](https://api.travis-ci.org/unclechu/node-deep-extend.svg?branch=master)](https://travis-ci.org/unclechu/node-deep-extend) 7 | 8 | [![NPM](https://nodei.co/npm/deep-extend.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/deep-extend/) 9 | 10 | Install 11 | ------- 12 | 13 | ```bash 14 | $ npm install deep-extend 15 | ``` 16 | 17 | Usage 18 | ----- 19 | 20 | ```javascript 21 | var deepExtend = require('deep-extend'); 22 | var obj1 = { 23 | a: 1, 24 | b: 2, 25 | d: { 26 | a: 1, 27 | b: [], 28 | c: { test1: 123, test2: 321 } 29 | }, 30 | f: 5, 31 | g: 123, 32 | i: 321, 33 | j: [1, 2] 34 | }; 35 | var obj2 = { 36 | b: 3, 37 | c: 5, 38 | d: { 39 | b: { first: 'one', second: 'two' }, 40 | c: { test2: 222 } 41 | }, 42 | e: { one: 1, two: 2 }, 43 | f: [], 44 | g: (void 0), 45 | h: /abc/g, 46 | i: null, 47 | j: [3, 4] 48 | }; 49 | 50 | deepExtend(obj1, obj2); 51 | 52 | console.log(obj1); 53 | /* 54 | { a: 1, 55 | b: 3, 56 | d: 57 | { a: 1, 58 | b: { first: 'one', second: 'two' }, 59 | c: { test1: 123, test2: 222 } }, 60 | f: [], 61 | g: undefined, 62 | c: 5, 63 | e: { one: 1, two: 2 }, 64 | h: /abc/g, 65 | i: null, 66 | j: [3, 4] } 67 | */ 68 | ``` 69 | 70 | Unit testing 71 | ------------ 72 | 73 | ```bash 74 | $ npm test 75 | ``` 76 | 77 | Changelog 78 | --------- 79 | 80 | [CHANGELOG.md](./CHANGELOG.md) 81 | 82 | Any issues? 83 | ----------- 84 | 85 | Please, report about issues 86 | [here](https://github.com/unclechu/node-deep-extend/issues). 87 | 88 | License 89 | ------- 90 | 91 | [MIT](./LICENSE) 92 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/deep-extend'); 2 | -------------------------------------------------------------------------------- /lib/deep-extend.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * @description Recursive object extending 3 | * @author Viacheslav Lotsmanov 4 | * @license MIT 5 | * 6 | * The MIT License (MIT) 7 | * 8 | * Copyright (c) 2013-2018 Viacheslav Lotsmanov 9 | * 10 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 11 | * this software and associated documentation files (the "Software"), to deal in 12 | * the Software without restriction, including without limitation the rights to 13 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 14 | * the Software, and to permit persons to whom the Software is furnished to do so, 15 | * subject to the following conditions: 16 | * 17 | * The above copyright notice and this permission notice shall be included in all 18 | * copies or substantial portions of the Software. 19 | * 20 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 22 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 23 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 24 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 26 | */ 27 | 28 | 'use strict'; 29 | 30 | function isSpecificValue(val) { 31 | return ( 32 | val instanceof Buffer 33 | || val instanceof Date 34 | || val instanceof RegExp 35 | ) ? true : false; 36 | } 37 | 38 | function cloneSpecificValue(val) { 39 | if (val instanceof Buffer) { 40 | var x = Buffer.alloc 41 | ? Buffer.alloc(val.length) 42 | : new Buffer(val.length); 43 | val.copy(x); 44 | return x; 45 | } else if (val instanceof Date) { 46 | return new Date(val.getTime()); 47 | } else if (val instanceof RegExp) { 48 | return new RegExp(val); 49 | } else { 50 | throw new Error('Unexpected situation'); 51 | } 52 | } 53 | 54 | /** 55 | * Recursive cloning array. 56 | */ 57 | function deepCloneArray(arr) { 58 | var clone = []; 59 | arr.forEach(function (item, index) { 60 | if (typeof item === 'object' && item !== null) { 61 | if (Array.isArray(item)) { 62 | clone[index] = deepCloneArray(item); 63 | } else if (isSpecificValue(item)) { 64 | clone[index] = cloneSpecificValue(item); 65 | } else { 66 | clone[index] = deepExtend({}, item); 67 | } 68 | } else { 69 | clone[index] = item; 70 | } 71 | }); 72 | return clone; 73 | } 74 | 75 | function safeGetProperty(object, property) { 76 | return property === '__proto__' ? undefined : object[property]; 77 | } 78 | 79 | /** 80 | * Extening object that entered in first argument. 81 | * 82 | * Returns extended object or false if have no target object or incorrect type. 83 | * 84 | * If you wish to clone source object (without modify it), just use empty new 85 | * object as first argument, like this: 86 | * deepExtend({}, yourObj_1, [yourObj_N]); 87 | */ 88 | var deepExtend = module.exports = function (/*obj_1, [obj_2], [obj_N]*/) { 89 | if (arguments.length < 1 || typeof arguments[0] !== 'object') { 90 | return false; 91 | } 92 | 93 | if (arguments.length < 2) { 94 | return arguments[0]; 95 | } 96 | 97 | var target = arguments[0]; 98 | 99 | // convert arguments to array and cut off target object 100 | var args = Array.prototype.slice.call(arguments, 1); 101 | 102 | var val, src, clone; 103 | 104 | args.forEach(function (obj) { 105 | // skip argument if isn't an object, is null, or is an array 106 | if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) { 107 | return; 108 | } 109 | 110 | Object.keys(obj).forEach(function (key) { 111 | src = safeGetProperty(target, key); // source value 112 | val = safeGetProperty(obj, key); // new value 113 | 114 | // recursion prevention 115 | if (val === target) { 116 | return; 117 | 118 | /** 119 | * if new value isn't object then just overwrite by new value 120 | * instead of extending. 121 | */ 122 | } else if (typeof val !== 'object' || val === null) { 123 | target[key] = val; 124 | return; 125 | 126 | // just clone arrays (and recursive clone objects inside) 127 | } else if (Array.isArray(val)) { 128 | target[key] = deepCloneArray(val); 129 | return; 130 | 131 | // custom cloning and overwrite for specific objects 132 | } else if (isSpecificValue(val)) { 133 | target[key] = cloneSpecificValue(val); 134 | return; 135 | 136 | // overwrite by new value if source isn't object or array 137 | } else if (typeof src !== 'object' || src === null || Array.isArray(src)) { 138 | target[key] = deepExtend({}, val); 139 | return; 140 | 141 | // source value and new value is objects both, extending... 142 | } else { 143 | target[key] = deepExtend(src, val); 144 | return; 145 | } 146 | }); 147 | }); 148 | 149 | return target; 150 | }; 151 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deep-extend", 3 | "description": "Recursive object extending", 4 | "license": "MIT", 5 | "version": "0.6.0", 6 | "homepage": "https://github.com/unclechu/node-deep-extend", 7 | "keywords": [ 8 | "deep-extend", 9 | "extend", 10 | "deep", 11 | "recursive", 12 | "xtend", 13 | "clone", 14 | "merge", 15 | "json" 16 | ], 17 | "licenses": [ 18 | { 19 | "type": "MIT", 20 | "url": "https://raw.githubusercontent.com/unclechu/node-deep-extend/master/LICENSE" 21 | } 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "git://github.com/unclechu/node-deep-extend.git" 26 | }, 27 | "author": "Viacheslav Lotsmanov ", 28 | "bugs": "https://github.com/unclechu/node-deep-extend/issues", 29 | "contributors": [ 30 | { 31 | "name": "Romain Prieto", 32 | "url": "https://github.com/rprieto" 33 | }, 34 | { 35 | "name": "Max Maximov", 36 | "url": "https://github.com/maxmaximov" 37 | }, 38 | { 39 | "name": "Marshall Bowers", 40 | "url": "https://github.com/maxdeviant" 41 | }, 42 | { 43 | "name": "Misha Wakerman", 44 | "url": "https://github.com/mwakerman" 45 | } 46 | ], 47 | "main": "lib/deep-extend.js", 48 | "engines": { 49 | "node": ">=4.0.0" 50 | }, 51 | "scripts": { 52 | "test": "./node_modules/.bin/mocha" 53 | }, 54 | "devDependencies": { 55 | "mocha": "5.2.0", 56 | "should": "13.2.1" 57 | }, 58 | "files": [ 59 | "index.js", 60 | "lib/" 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /test/index.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('should'); 4 | var extend = require('../index'); // it must be ./lib/deep-extend.js 5 | 6 | describe('deep-extend', function () { 7 | 8 | it('should ignore undefined', function () { 9 | var a = { hello: 1 }; 10 | var b = undefined; 11 | extend(a, b); 12 | a.should.eql({ 13 | hello: 1 14 | }); 15 | }); 16 | 17 | it('should ignore null', function () { 18 | var a = { hello: 1 }; 19 | var b = null; 20 | extend(a, b); 21 | a.should.eql({ 22 | hello: 1 23 | }); 24 | }); 25 | 26 | it('can extend on 1 level', function () { 27 | var a = { hello: 1 }; 28 | var b = { world: 2 }; 29 | extend(a, b); 30 | a.should.eql({ 31 | hello: 1, 32 | world: 2 33 | }); 34 | }); 35 | 36 | it('can extend on 2 levels', function () { 37 | var a = { person: { name: 'John' } }; 38 | var b = { person: { age: 30 } }; 39 | extend(a, b); 40 | a.should.eql({ 41 | person: { name: 'John', age: 30 } 42 | }); 43 | }); 44 | 45 | it('can extend with Buffer values', function () { 46 | var a = { hello: 1 }; 47 | var b = { value: new Buffer('world') }; 48 | extend(a, b); 49 | a.should.eql({ 50 | hello: 1, 51 | value: new Buffer('world') 52 | }); 53 | }); 54 | 55 | it('Buffer is cloned', function () { 56 | var a = {}; 57 | var b = { value: new Buffer('foo') }; 58 | extend(a, b); 59 | a.value.write('bar'); 60 | a.value.toString().should.eql('bar'); 61 | b.value.toString().should.eql('foo'); 62 | }); 63 | 64 | it('Date objects', function () { 65 | var a = { d: new Date() }; 66 | var b = extend({}, a); 67 | b.d.should.instanceOf(Date); 68 | }); 69 | 70 | it('Date object is cloned', function () { 71 | var a = { d: new Date() }; 72 | var b = extend({}, a); 73 | b.d.setTime((new Date()).getTime() + 100000); 74 | b.d.getTime().should.not.eql(a.d.getTime()); 75 | }); 76 | 77 | it('RegExp objects', function () { 78 | var a = { d: new RegExp() }; 79 | var b = extend({}, a); 80 | b.d.should.instanceOf(RegExp); 81 | }); 82 | 83 | it('RegExp object is cloned', function () { 84 | var a = { d: new RegExp('b', 'g') }; 85 | var b = extend({}, a); 86 | b.d.test('abc'); 87 | b.d.lastIndex.should.not.eql(a.d.lastIndex); 88 | }); 89 | 90 | it('doesn\'t change sources', function () { 91 | var a = { a: [1] }; 92 | var b = { a: [2] }; 93 | var c = { c: 3 }; 94 | var d = extend({}, a, b, c); 95 | 96 | a.should.eql({ a: [1] }); 97 | b.should.eql({ a: [2] }); 98 | c.should.eql({ c: 3 }); 99 | }); 100 | 101 | it('example from README.md', function () { 102 | var obj1 = { 103 | a: 1, 104 | b: 2, 105 | d: { 106 | a: 1, 107 | b: [], 108 | c: { test1: 123, test2: 321 } 109 | }, 110 | f: 5, 111 | g: 123, 112 | i: 321, 113 | j: [1, 2] 114 | }; 115 | var obj2 = { 116 | b: 3, 117 | c: 5, 118 | d: { 119 | b: { first: 'one', second: 'two' }, 120 | c: { test2: 222 } 121 | }, 122 | e: { one: 1, two: 2 }, 123 | f: [], 124 | g: (void 0), 125 | h: /abc/g, 126 | i: null, 127 | j: [3, 4] 128 | }; 129 | 130 | extend(obj1, obj2); 131 | 132 | obj1.should.eql({ 133 | a: 1, 134 | b: 3, 135 | d: { 136 | a: 1, 137 | b: { first: 'one', second: 'two' }, 138 | c: { test1: 123, test2: 222 } 139 | }, 140 | f: [], 141 | g: undefined, 142 | c: 5, 143 | e: { one: 1, two: 2 }, 144 | h: /abc/g, 145 | i: null, 146 | j: [3, 4] 147 | }); 148 | 149 | ('g' in obj1).should.eql(true); 150 | ('x' in obj1).should.eql(false); 151 | }); 152 | 153 | it('clone arrays instead of extend', function () { 154 | extend({ a: [1, 2, 3] }, { a: [2, 3] }).should.eql({ a: [2, 3] }); 155 | }); 156 | 157 | it('recursive clone objects and special objects in cloned arrays', function () { 158 | var obj1 = { 159 | x: 1, 160 | y: new Buffer('foo') 161 | }; 162 | var b = new Buffer('bar'); 163 | var obj2 = { 164 | x: 1, 165 | y: [2, 4, obj1, b], 166 | z: new Buffer('test') 167 | }; 168 | var foo = { 169 | a: [obj2, obj2] 170 | }; 171 | var bar = extend({}, foo); 172 | bar.a[0].x = 2; 173 | bar.a[0].z.write('text', 'utf-8'); 174 | bar.a[1].x = 3; 175 | bar.a[1].z.write('lel', 'utf-8'); 176 | bar.a[0].y[0] = 3; 177 | bar.a[0].y[2].x = 5; 178 | bar.a[0].y[2].y.write('heh', 'utf-8'); 179 | bar.a[0].y[3].write('ho', 'utf-8'); 180 | bar.a[1].y[1] = 3; 181 | bar.a[1].y[2].y.write('nah', 'utf-8'); 182 | bar.a[1].y[3].write('he', 'utf-8'); 183 | 184 | obj2.x.should.eql(1); 185 | obj2.z.toString().should.eql('test'); 186 | bar.a[0].x.should.eql(2); 187 | bar.a[0].z.toString().should.eql('text'); 188 | bar.a[1].x.should.eql(3); 189 | bar.a[1].z.toString().should.eql('lelt'); 190 | obj1.x.should.eql(1); 191 | obj1.y.toString().should.eql('foo'); 192 | b.toString().should.eql('bar'); 193 | 194 | bar.a[0].y[0].should.eql(3); 195 | bar.a[0].y[1].should.eql(4); 196 | bar.a[0].y[2].x.should.eql(5); 197 | bar.a[0].y[2].y.toString().should.eql('heh'); 198 | bar.a[0].y[3].toString().should.eql('hor'); 199 | 200 | bar.a[1].y[0].should.eql(2); 201 | bar.a[1].y[1].should.eql(3); 202 | bar.a[1].y[2].x.should.eql(1); 203 | bar.a[1].y[2].y.toString().should.eql('nah'); 204 | bar.a[1].y[3].toString().should.eql('her'); 205 | 206 | foo.a.length.should.eql(2); 207 | bar.a.length.should.eql(2); 208 | Object.keys(obj2).should.eql(['x', 'y', 'z']); 209 | Object.keys(bar.a[0]).should.eql(['x', 'y', 'z']); 210 | Object.keys(bar.a[1]).should.eql(['x', 'y', 'z']); 211 | obj2.y.length.should.eql(4); 212 | bar.a[0].y.length.should.eql(4); 213 | bar.a[1].y.length.should.eql(4); 214 | Object.keys(obj2.y[2]).should.eql(['x', 'y']); 215 | Object.keys(bar.a[0].y[2]).should.eql(['x', 'y']); 216 | Object.keys(bar.a[1].y[2]).should.eql(['x', 'y']); 217 | }); 218 | 219 | it('checking keys for hasOwnPrototype', function () { 220 | var A = function () { 221 | this.x = 1; 222 | this.y = 2; 223 | }; 224 | A.prototype.z = 3; 225 | var foo = new A(); 226 | extend({ x: 123 }, foo).should.eql({ 227 | x: 1, 228 | y: 2 229 | }); 230 | foo.z = 5; 231 | extend({ x: 123 }, foo, { y: 22 }).should.eql({ 232 | x: 1, 233 | y: 22, 234 | z: 5 235 | }); 236 | }); 237 | 238 | describe('issue #33', function () { 239 | 240 | it('correct usage (cloning)', function () { 241 | var sharedObject = {foo: 'zero'}; 242 | var objDef = {bar: sharedObject, baz: sharedObject}; 243 | var obj = {bar: {foo: 'one'}, baz: {foo: 'two'}}; 244 | obj = extend({}, {bar: objDef.bar, baz: objDef.baz}, obj); 245 | obj.should.eql({bar: {foo: 'one'}, baz: {foo: 'two'}}); 246 | }); 247 | 248 | it('incorrect usage (just extending)', function () { 249 | var sharedObject = {foo: 'serif'}; 250 | var objDef = {bar: sharedObject, baz: sharedObject}; 251 | var obj = {bar: {foo: 'one'}, baz: {foo: 'two'}}; 252 | obj = extend({bar: objDef.bar, baz: objDef.baz}, obj); 253 | obj.should.eql({bar: {foo: 'two'}, baz: {foo: 'two'}}); 254 | }); 255 | }); 256 | 257 | // Vulnerability reported via hacker1: https://hackerone.com/reports/311333 258 | // See https://github.com/unclechu/node-deep-extend/issues/39 259 | // See https://github.com/unclechu/node-deep-extend/pull/40 260 | it('should not modify Object prototype (hacker1 #311333)', function () { 261 | var a = {}; 262 | extend({}, JSON.parse('{"__proto__":{"oops":"It works!"}}')) 263 | should.not.exist(a.oops); 264 | should.not.exist(Object.prototype.oops); 265 | }); 266 | 267 | }); 268 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter spec 2 | --------------------------------------------------------------------------------