├── .gitignore ├── .github ├── dependabot.yml.bak └── workflows │ └── main.yml ├── package.json ├── LICENSE ├── tests ├── extendext.spec.js ├── deepmerge.spec.js └── extend.spec.js ├── README.md └── jquery-extendext.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.idea 3 | /*.iml 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml.bak: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: build 13 | run: | 14 | npm install 15 | npm run test 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-extendext", 3 | "version": "1.0.0", 4 | "description": "jQuery.extend with configurable behaviour for arrays", 5 | "authors": [ 6 | { 7 | "name": "Damien \"Mistic\" Sorel", 8 | "email": "contact@git.strangeplanet.fr", 9 | "homepage": "http://www.strangeplanet.fr" 10 | } 11 | ], 12 | "main": "jquery-extendext.js", 13 | "files": [ 14 | "jquery-extendext.js" 15 | ], 16 | "dependencies": { 17 | "jquery": ">=1.9.1" 18 | }, 19 | "devDependencies": { 20 | "jest": "^27.2.0", 21 | "jest-expect-message": "^1.0.2" 22 | }, 23 | "keywords": [ 24 | "jQuery", 25 | "extend", 26 | "arrays" 27 | ], 28 | "license": "MIT", 29 | "homepage": "https://github.com/mistic100/jQuery.extendext", 30 | "repository": { 31 | "type": "git", 32 | "url": "git://github.com/mistic100/jQuery.extendext.git" 33 | }, 34 | "bugs": { 35 | "url": "https://github.com/mistic100/jQuery.extendext/issues" 36 | }, 37 | "scripts": { 38 | "test": "jest tests/*.spec.js" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2019 Damien Sorel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/extendext.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | const $ = require('jquery'); 6 | require('../jquery-extendext'); 7 | 8 | describe('extendext new modes', () => { 9 | it('should extend arrays', () => { 10 | const a = { 11 | foo : { bar: 3 }, 12 | array: [{ does: 'work', too: [1, 2, 3] }] 13 | }; 14 | const b = { 15 | foo : { baz: 4 }, 16 | quux : 5, 17 | array: [{ does: 'work', too: [4, 5, 6] }, { really: 'yes' }] 18 | }; 19 | const o = { 20 | foo : { bar: 3, baz: 4 }, 21 | array: [{ does: 'work', too: [1, 2, 3, 4, 5, 6] }, { really: 'yes' }], 22 | quux : 5 23 | }; 24 | 25 | expect($.extendext(true, 'extend', a, b)).toEqual(o); 26 | }); 27 | 28 | it('should concat arrays', () => { 29 | const a = { 30 | foo : { bar: 3 }, 31 | array: [1, 2, 4] 32 | }; 33 | const b = { 34 | foo : { baz: 4 }, 35 | array: [1, 4, 5] 36 | }; 37 | const o = { 38 | foo : { bar: 3, baz: 4 }, 39 | array: [1, 2, 4, 1, 4, 5] 40 | }; 41 | 42 | expect($.extendext(true, 'concat', a, b)).toEqual(o); 43 | }); 44 | 45 | it('should replace arrays', () => { 46 | const a = { 47 | foo : { bar: 3 }, 48 | array: [1, 2, 4] 49 | }; 50 | const b = { 51 | foo : { baz: 4 }, 52 | array: [4, 5] 53 | }; 54 | const o = { 55 | foo : { bar: 3, baz: 4 }, 56 | array: [4, 5] 57 | }; 58 | 59 | expect($.extendext(true, 'replace', a, b)).toEqual(o); 60 | }); 61 | }); 62 | 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jQuery.extendext 2 | 3 | [![npm version](https://img.shields.io/npm/v/jquery-extendext.svg?style=flat-square)](https://www.npmjs.com/package/jquery-extendext) 4 | [![jsDelivr CDN](https://data.jsdelivr.com/v1/package/npm/jquery-extendext/badge)](https://www.jsdelivr.com/package/npm/jquery-extendext) 5 | [![Build Status](https://github.com/mistic100/jQuery.extendext/workflows/CI/badge.svg)](https://github.com/mistic100/jQuery.extendext/actions) 6 | 7 | jQuery.extend with configurable behaviour for arrays. 8 | 9 | ## Isn't $.extend good enough ? 10 | 11 | Well, it's actually pretty good, and is generally sufficient, but it merges arrays in a strange way depending of what you want. Example: 12 | 13 | ```js 14 | var DEFAULTS = { 15 | operators: ['AND', 'OR', 'XOR'] 16 | }; 17 | 18 | var config = { 19 | operators: ['OR', 'XOR'] 20 | }; 21 | 22 | config = $.extend(true, {}, DEFAULTS, config); 23 | ``` 24 | 25 | When executing this code, one will expects to get `config.operators = ['OR', 'XOR']`, but instead you get `['OR', 'XOR', 'XOR]`, because `$.extend` merges arrays like objects as per spec. 26 | 27 | Other deep merging utilities I found either have the same behaviour or perform both merge and append on array values ([nrf110/deepmerge](https://github.com/nrf110/deepmerge) for example). 28 | 29 | ## Usage 30 | 31 | **jQuery.extendext.js** contains a new `$.extendext` function with the exact same behaviour as `$.extend` if not additional config is provided. 32 | 33 | The difference is that it accepts a optional second string argument to specify how arrays should be merged. 34 | 35 | ```js 36 | jQuery.extendext([deep ,][arrayMode ,] target, object1 [, objectN ] ) 37 | ``` 38 | 39 | * **deep** _boolean_ — If true, the merge becomes recursive (aka. deep copy). 40 | * **arrayMode** _string_ — Specify the arrays merge operation, either `replace`, `concat`, `extend` or `default` 41 | * **target** _object_ — The object to extend. It will receive the new properties. 42 | * **object1** _object_ — An object containing additional properties to merge in. 43 | * **objectN** _object_ — Additional objects containing properties to merge in. 44 | 45 | ### "replace" mode 46 | 47 | In this mode, every Array values in `target` is replaced by a copy of the same value found in `objectN`. The copy is recursive if `deep` is true. 48 | 49 | ```js 50 | var DEFAULTS = { 51 | operators: ['AND', 'OR', 'XOR'] 52 | }; 53 | 54 | var config = { 55 | operators: ['OR', 'XOR'] 56 | }; 57 | 58 | config = $.extendext(true, 'replace', {}, DEFAULTS, config); 59 | 60 | assert.deepEqual(config, { 61 | operators: ['OR', 'XOR'] 62 | }) // true; 63 | ``` 64 | 65 | ### "concat" mode 66 | 67 | In this mode, Arrays found in both `target` and `objectN` are always concatenated. If `deep` is true, a recursive copy of each value if concatenated instead of the value itself. 68 | 69 | ```js 70 | var DEFAULTS = { 71 | operators: ['AND', 'OR', 'XOR'] 72 | }; 73 | 74 | var config = { 75 | operators: ['OR', 'XOR'] 76 | }; 77 | 78 | config = $.extendext(true, 'concat', {}, DEFAULTS, config); 79 | 80 | assert.deepEqual(config, { 81 | operators: ['AND', 'OR', 'XOR', 'OR', 'XOR'] 82 | }) // true; 83 | ``` 84 | 85 | ### "extend" mode 86 | 87 | This is how [nrf110/deepmerge](https://github.com/nrf110/deepmerge) works. In this mode, Arrays values are treated a bit differently: 88 | 89 | * If plain objects are found at the same position in both `target` and `objectN` they are merged recursively or not (depending on `deep` option). 90 | * Otherwise, if the value in `objectN` is not found in `target`, it is pushed at the end of the array. 91 | 92 | ```js 93 | var DEFAULTS = { 94 | operators: ['AND', 'OR', 'XOR'] 95 | }; 96 | 97 | var config = { 98 | operators: ['XOR', 'NAND'] 99 | }; 100 | 101 | config = $.extendext(true, 'extend', {}, DEFAULTS, config); 102 | 103 | assert.deepEqual(config, { 104 | operators: ['AND', 'OR', 'XOR', 'NAND'] 105 | }) // true; 106 | ``` 107 | 108 | ### "default" mode 109 | 110 | Same as `$.extend`. 111 | 112 | ```js 113 | var DEFAULTS = { 114 | operators: ['AND', 'OR', 'XOR'] 115 | }; 116 | 117 | var config = { 118 | operators: ['OR', 'XOR'] 119 | }; 120 | 121 | config = $.extendext(true, 'default', {}, DEFAULTS, config); 122 | 123 | assert.deepEqual(config, { 124 | operators: ['OR', 'XOR', 'XOR'] 125 | }) // true; 126 | ``` 127 | 128 | ## Tests 129 | 130 | A jest test suite is provided in `tests` directory. 131 | 132 | `$.extendext` is tested against core jQuery tests for `$.extend` and `nrf110/deepmerge` tests (with the difference that extendext, like extend, modifies the first argument where deepmerge does not touch it). 133 | -------------------------------------------------------------------------------- /jquery-extendext.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery.extendext 1.0.0 3 | * 4 | * Copyright 2014-2019 Damien "Mistic" Sorel (http://www.strangeplanet.fr) 5 | * Licensed under MIT (http://opensource.org/licenses/MIT) 6 | * 7 | * Based on jQuery.extend by jQuery Foundation, Inc. and other contributors 8 | */ 9 | 10 | (function (root, factory) { 11 | if (typeof define === 'function' && define.amd) { 12 | define(['jquery'], factory); 13 | } 14 | else if (typeof module === 'object' && module.exports) { 15 | module.exports = factory(require('jquery')); 16 | } 17 | else { 18 | factory(root.jQuery); 19 | } 20 | }(this, function ($) { 21 | "use strict"; 22 | 23 | $.extendext = function () { 24 | var options, name, src, copy, copyIsArray, clone, 25 | target = arguments[0] || {}, 26 | i = 1, 27 | length = arguments.length, 28 | deep = false, 29 | arrayMode = 'default'; 30 | 31 | // Handle a deep copy situation 32 | if (typeof target === "boolean") { 33 | deep = target; 34 | 35 | // Skip the boolean and the target 36 | target = arguments[i++] || {}; 37 | } 38 | 39 | // Handle array mode parameter 40 | if (typeof target === "string") { 41 | arrayMode = target.toLowerCase(); 42 | if (arrayMode !== 'concat' && arrayMode !== 'replace' && arrayMode !== 'extend') { 43 | arrayMode = 'default'; 44 | } 45 | 46 | // Skip the string param 47 | target = arguments[i++] || {}; 48 | } 49 | 50 | // Handle case when target is a string or something (possible in deep copy) 51 | if (typeof target !== "object" && !$.isFunction(target)) { 52 | target = {}; 53 | } 54 | 55 | // Extend jQuery itself if only one argument is passed 56 | if (i === length) { 57 | target = this; 58 | i--; 59 | } 60 | 61 | for (; i < length; i++) { 62 | // Only deal with non-null/undefined values 63 | if ((options = arguments[i]) !== null) { 64 | // Special operations for arrays 65 | if ($.isArray(options) && arrayMode !== 'default') { 66 | clone = target && $.isArray(target) ? target : []; 67 | 68 | switch (arrayMode) { 69 | case 'concat': 70 | target = clone.concat($.extend(deep, [], options)); 71 | break; 72 | 73 | case 'replace': 74 | target = $.extend(deep, [], options); 75 | break; 76 | 77 | case 'extend': 78 | options.forEach(function (e, i) { 79 | if (typeof e === 'object') { 80 | var type = $.isArray(e) ? [] : {}; 81 | clone[i] = $.extendext(deep, arrayMode, clone[i] || type, e); 82 | 83 | } else if (clone.indexOf(e) === -1) { 84 | clone.push(e); 85 | } 86 | }); 87 | 88 | target = clone; 89 | break; 90 | } 91 | 92 | } else { 93 | // Extend the base object 94 | for (name in options) { 95 | copy = options[name]; 96 | 97 | // Prevent never-ending loop 98 | if (name === '__proto__' || target === copy) { 99 | continue; 100 | } 101 | 102 | // Recurse if we're merging plain objects or arrays 103 | if (deep && copy && ( $.isPlainObject(copy) || 104 | (copyIsArray = $.isArray(copy)) )) { 105 | src = target[name]; 106 | 107 | // Ensure proper type for the source value 108 | if ( copyIsArray && !Array.isArray( src ) ) { 109 | clone = []; 110 | } else if ( !copyIsArray && !$.isPlainObject( src ) ) { 111 | clone = {}; 112 | } else { 113 | clone = src; 114 | } 115 | copyIsArray = false; 116 | 117 | // Never move original objects, clone them 118 | target[name] = $.extendext(deep, arrayMode, clone, copy); 119 | 120 | // Don't bring in undefined values 121 | } else if (copy !== undefined) { 122 | target[name] = copy; 123 | } 124 | } 125 | } 126 | } 127 | } 128 | 129 | // Return the modified object 130 | return target; 131 | }; 132 | })); 133 | -------------------------------------------------------------------------------- /tests/deepmerge.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | const $ = require('jquery'); 6 | require('../jquery-extendext'); 7 | 8 | describe('tests from deepmerge', () => { 9 | 10 | it('should add keys in target that do not exist at the root', () => { 11 | const src = { key1: 'value1', key2: 'value2' }; 12 | const target = {}; 13 | const expected = { key1: 'value1', key2: 'value2' }; 14 | 15 | expect($.extendext(true, 'extend', target, src)).toEqual(expected); 16 | }); 17 | 18 | it('should merge existing simple keys in target at the roots', () => { 19 | const src = { key1: 'changed', key2: 'value2' }; 20 | const target = { key1: 'value1', key3: 'value3' }; 21 | const expected = { 22 | key1: 'changed', 23 | key2: 'value2', 24 | key3: 'value3' 25 | }; 26 | 27 | expect($.extendext(true, 'extend', target, src)).toEqual(expected); 28 | }); 29 | 30 | it('should merge nested objects into target', () => { 31 | const src = { 32 | key1: { 33 | subkey1: 'changed', 34 | subkey3: 'added' 35 | } 36 | }; 37 | const target = { 38 | key1: { 39 | subkey1: 'value1', 40 | subkey2: 'value2' 41 | } 42 | }; 43 | const expected = { 44 | key1: { 45 | subkey1: 'changed', 46 | subkey2: 'value2', 47 | subkey3: 'added' 48 | } 49 | }; 50 | 51 | expect($.extendext(true, 'extend', target, src)).toEqual(expected); 52 | }); 53 | 54 | it('should replace simple key with nested object in target', () => { 55 | const src = { 56 | key1: { 57 | subkey1: 'subvalue1', 58 | subkey2: 'subvalue2' 59 | } 60 | }; 61 | const target = { 62 | key1: 'value1', 63 | key2: 'value2' 64 | }; 65 | const expected = { 66 | key1: { 67 | subkey1: 'subvalue1', 68 | subkey2: 'subvalue2' 69 | }, 70 | key2: 'value2' 71 | }; 72 | 73 | expect($.extendext(true, 'extend', target, src)).toEqual(expected); 74 | }); 75 | 76 | it('should should add nested object in target', () => { 77 | const src = { 78 | 'b': { 79 | 'c': {} 80 | } 81 | }; 82 | const target = { 83 | 'a': {} 84 | }; 85 | const expected = { 86 | 'a': {}, 87 | 'b': { 88 | 'c': {} 89 | } 90 | }; 91 | 92 | expect($.extendext(true, 'extend', target, src)).toEqual(expected); 93 | }); 94 | 95 | it('should should replace object with simple key in target', () => { 96 | const src = { key1: 'value1' }; 97 | const target = { 98 | key1: { 99 | subkey1: 'subvalue1', 100 | subkey2: 'subvalue2' 101 | }, 102 | key2: 'value2' 103 | }; 104 | const expected = { key1: 'value1', key2: 'value2' }; 105 | 106 | expect($.extendext(true, 'extend', target, src)).toEqual(expected); 107 | }); 108 | 109 | it('should should work on simple array', () => { 110 | const src = ['one', 'three']; 111 | const target = ['one', 'two']; 112 | const expected = ['one', 'two', 'three']; 113 | 114 | expect($.extendext(true, 'extend', target, src)).toEqual(expected); 115 | }); 116 | 117 | it('should should work on another simple array', () => { 118 | const target = ['a1', 'a2', 'c1', 'f1', 'p1']; 119 | const src = ['t1', 's1', 'c2', 'r1', 'p2', 'p3']; 120 | const expected = ['a1', 'a2', 'c1', 'f1', 'p1', 't1', 's1', 'c2', 'r1', 'p2', 'p3']; 121 | 122 | expect($.extendext(true, 'extend', target, src)).toEqual(expected); 123 | }); 124 | 125 | it('should should work on array properties', () => { 126 | const src = { 127 | key1: ['one', 'three'], 128 | key2: ['four'] 129 | }; 130 | const target = { 131 | key1: ['one', 'two'] 132 | }; 133 | const expected = { 134 | key1: ['one', 'two', 'three'], 135 | key2: ['four'] 136 | }; 137 | 138 | expect($.extendext(true, 'extend', target, src)).toEqual(expected); 139 | }); 140 | 141 | it('should should work on array of objects', () => { 142 | const src = [ 143 | { key1: ['one', 'three'], key2: ['one'] }, 144 | { key3: ['five'] } 145 | ]; 146 | const target = [ 147 | { key1: ['one', 'two'] }, 148 | { key3: ['four'] } 149 | ]; 150 | const expected = [ 151 | { key1: ['one', 'two', 'three'], key2: ['one'] }, 152 | { key3: ['four', 'five'] } 153 | ]; 154 | 155 | expect($.extendext(true, 'extend', target, src)).toEqual(expected); 156 | }); 157 | 158 | it('should should work on arrays of nested objects', () => { 159 | const target = [ 160 | { key1: { subkey: 'one' } } 161 | ]; 162 | const src = [ 163 | { key1: { subkey: 'two' } }, 164 | { key2: { subkey: 'three' } } 165 | ]; 166 | const expected = [ 167 | { key1: { subkey: 'two' } }, 168 | { key2: { subkey: 'three' } } 169 | ]; 170 | 171 | expect($.extendext(true, 'extend', target, src)).toEqual(expected); 172 | }) 173 | 174 | }); 175 | -------------------------------------------------------------------------------- /tests/extend.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | require('jest-expect-message'); 6 | 7 | const $ = require('jquery'); 8 | require('../jquery-extendext'); 9 | 10 | describe('tests from jQuery', () => { 11 | 12 | it('jQuery.extend(Object, Object)', function () { 13 | expect.assertions(28); 14 | 15 | var empty, optionsWithLength, optionsWithDate, myKlass, 16 | customObject, optionsWithCustomObject, MyNumber, ret, 17 | nullUndef, target, recursive, obj, 18 | defaults, defaultsCopy, options1, options1Copy, options2, options2Copy, merged2, 19 | settings = { 'xnumber1': 5, 'xnumber2': 7, 'xstring1': 'peter', 'xstring2': 'pan' }, 20 | options = { 'xnumber2': 1, 'xstring2': 'x', 'xxx': 'newstring' }, 21 | optionsCopy = { 'xnumber2': 1, 'xstring2': 'x', 'xxx': 'newstring' }, 22 | merged = { 'xnumber1': 5, 'xnumber2': 1, 'xstring1': 'peter', 'xstring2': 'x', 'xxx': 'newstring' }, 23 | deep1 = { 'foo': { 'bar': true } }, 24 | deep2 = { 'foo': { 'baz': true }, 'foo2': document }, 25 | deep2copy = { 'foo': { 'baz': true }, 'foo2': document }, 26 | deepmerged = { 'foo': { 'bar': true, 'baz': true }, 'foo2': document }, 27 | arr = [1, 2, 3], 28 | nestedarray = { 'arr': arr }; 29 | 30 | $.extendext(settings, options); 31 | expect(settings, 'Check if extended: settings must be extended').toEqual(merged); 32 | expect(options, 'Check if not modified: options must not be modified').toEqual(optionsCopy); 33 | 34 | $.extendext(settings, null, options); 35 | expect(settings, 'Check if extended: settings must be extended').toEqual(merged); 36 | expect(options, 'Check if not modified: options must not be modified').toEqual(optionsCopy); 37 | 38 | $.extendext(true, deep1, deep2); 39 | expect(deep1['foo'], 'Check if foo: settings must be extended').toEqual(deepmerged['foo']); 40 | expect(deep2['foo'], 'Check if not deep2: options must not be modified').toEqual(deep2copy['foo']); 41 | expect(deep1['foo2'], 'Make sure that a deep clone was not attempted on the document').toEqual(document); 42 | 43 | expect($.extendext(true, {}, nestedarray)['arr'] !== arr, 'Deep extend of object must clone child array').toEqual(true); 44 | 45 | // #5991 46 | expect(Array.isArray($.extendext(true, { 'arr': {} }, nestedarray)['arr']), 'Cloned array have to be an Array').toEqual(true); 47 | expect($.isPlainObject($.extendext(true, { 'arr': arr }, { 'arr': {} })['arr']), 'Cloned object have to be an plain object').toEqual(true); 48 | 49 | empty = {}; 50 | optionsWithLength = { 'foo': { 'length': -1 } }; 51 | $.extendext(true, empty, optionsWithLength); 52 | expect(empty['foo'], 'The length property must copy correctly').toEqual(optionsWithLength['foo']); 53 | 54 | empty = {}; 55 | optionsWithDate = { 'foo': { 'date': new Date() } }; 56 | $.extendext(true, empty, optionsWithDate); 57 | expect(empty['foo'], 'Dates copy correctly').toEqual(optionsWithDate['foo']); 58 | 59 | /** @constructor */ 60 | myKlass = function () { 61 | }; 62 | customObject = new myKlass(); 63 | optionsWithCustomObject = { 'foo': { 'date': customObject } }; 64 | empty = {}; 65 | $.extendext(true, empty, optionsWithCustomObject); 66 | expect(empty['foo'] && empty['foo']['date'] === customObject, 'Custom objects copy correctly (no methods)').toEqual(true); 67 | 68 | // Makes the class a little more realistic 69 | myKlass.prototype = { 70 | 'someMethod': function () { 71 | } 72 | }; 73 | empty = {}; 74 | $.extendext(true, empty, optionsWithCustomObject); 75 | expect(empty['foo'] && empty['foo']['date'] === customObject, 'Custom objects copy correctly').toEqual(true); 76 | 77 | MyNumber = Number; 78 | 79 | ret = $.extendext(true, { 'foo': 4 }, { 'foo': new MyNumber(5) }); 80 | expect(parseInt(ret.foo, 10) === 5, 'Wrapped numbers copy correctly').toEqual(true); 81 | 82 | nullUndef = $.extendext({}, options, { 'xnumber2': null }); 83 | expect(nullUndef['xnumber2'] === null, 'Check to make sure null values are copied').toEqual(true); 84 | 85 | nullUndef = $.extendext({}, options, { 'xnumber2': undefined }); 86 | expect(nullUndef['xnumber2'] === options['xnumber2'], 'Check to make sure undefined values are not copied').toEqual(true); 87 | 88 | nullUndef = $.extendext({}, options, { 'xnumber0': null }); 89 | expect(nullUndef['xnumber0'] === null, 'Check to make sure null values are inserted').toEqual(true); 90 | 91 | target = {}; 92 | recursive = { foo: target, bar: 5 }; 93 | $.extendext(true, target, recursive); 94 | expect(target, 'Check to make sure a recursive obj doesn\'t go never-ending loop by not copying it over').toEqual({ bar: 5 }); 95 | 96 | ret = $.extendext(true, { foo: [] }, { foo: [0] }); // 1907 97 | expect(ret.foo.length, 'Check to make sure a value with coercion \'false\' copies over when necessary to fix #1907').toEqual(1); 98 | 99 | ret = $.extendext(true, { foo: '1,2,3' }, { foo: [1, 2, 3] }); 100 | expect(typeof ret.foo !== 'string', 'Check to make sure values equal with coercion (but not actually equal) overwrite correctly').toEqual(true); 101 | 102 | ret = $.extendext(true, { foo: 'bar' }, { foo: null }); 103 | expect(typeof ret.foo !== 'undefined', 'Make sure a null value doesn\'t crash with deep extend, for #1908').toEqual(true); 104 | 105 | obj = { foo: null }; 106 | $.extendext(true, obj, { foo: 'notnull' }); 107 | expect(obj.foo, 'Make sure a null value can be overwritten').toEqual('notnull'); 108 | 109 | function func() { 110 | } 111 | 112 | $.extendext(func, { key: 'value' }); 113 | expect(func.key, 'Verify a function can be extended').toEqual('value'); 114 | 115 | defaults = { xnumber1: 5, xnumber2: 7, xstring1: 'peter', xstring2: 'pan' }; 116 | defaultsCopy = { xnumber1: 5, xnumber2: 7, xstring1: 'peter', xstring2: 'pan' }; 117 | options1 = { xnumber2: 1, xstring2: 'x' }; 118 | options1Copy = { xnumber2: 1, xstring2: 'x' }; 119 | options2 = { xstring2: 'xx', xxx: 'newstringx' }; 120 | options2Copy = { xstring2: 'xx', xxx: 'newstringx' }; 121 | merged2 = { xnumber1: 5, xnumber2: 1, xstring1: 'peter', xstring2: 'xx', xxx: 'newstringx' }; 122 | 123 | settings = $.extendext({}, defaults, options1, options2); 124 | expect(settings, 'Check if extended: settings must be extended').toEqual(merged2); 125 | expect(defaults, 'Check if not modified: options1 must not be modified').toEqual(defaultsCopy); 126 | expect(options1, 'Check if not modified: options1 must not be modified').toEqual(options1Copy); 127 | expect(options2, 'Check if not modified: options2 must not be modified').toEqual(options2Copy); 128 | }); 129 | 130 | it('jQuery.extend(Object, Object {created with "defineProperties"})', function () { 131 | expect.assertions(2); 132 | 133 | var definedObj = Object.defineProperties({}, { 134 | 'enumerableProp' : { 135 | get : function () { 136 | return true; 137 | }, 138 | enumerable: true 139 | }, 140 | 'nonenumerableProp': { 141 | get: function () { 142 | return true; 143 | } 144 | } 145 | }), 146 | accessorObj = {}; 147 | 148 | $.extendext(accessorObj, definedObj); 149 | expect(accessorObj.enumerableProp, 'Verify that getters are transferred').toEqual(true); 150 | expect(accessorObj.nonenumerableProp, 'Verify that non-enumerable getters are ignored').toEqual(undefined); 151 | }); 152 | 153 | it('jQuery.extend(true,{},{a:[], o:{}}); deep copy with array, followed by object', function () { 154 | expect.assertions(2); 155 | 156 | var result, initial = { 157 | 158 | // This will make "copyIsArray" true 159 | array: [1, 2, 3, 4], 160 | 161 | // If "copyIsArray" doesn't get reset to false, the check 162 | // will evaluate true and enter the array copy block 163 | // instead of the object copy block. Since the ternary in the 164 | // "copyIsArray" block will evaluate to false 165 | // (check if operating on an array with ), this will be 166 | // replaced by an empty array. 167 | object: {} 168 | }; 169 | 170 | result = $.extendext(true, {}, initial); 171 | 172 | expect(result, 'The [result] and [initial] have equal shape and values').toEqual(initial); 173 | expect(!Array.isArray(result.object), 'result.object wasn\'t paved with an empty array').toEqual(true); 174 | }); 175 | 176 | it('jQuery.extend( true, ... ) Object.prototype pollution', function () { 177 | expect.assertions(1); 178 | 179 | $.extendext(true, {}, JSON.parse('{"__proto__": {"devMode": true}}')); 180 | expect(!('devMode' in {}), 'Object.prototype not polluted').toEqual(true); 181 | }); 182 | 183 | }); 184 | --------------------------------------------------------------------------------