JavaScript equivalent of jQuery's extend method
Asked Answered
P

8

111

Background

I have a function that takes a config object as an argument. Within the function, I also have default object. Each of those objects contains properties that essentially work as settings for the rest of the code within the function. In order to prevent having to specify all of the settings within the config object, I use jQuery's extend method to fill in a new object, settings with any default values from the default object if they weren't specified in the config object:

var config = {key1: value1};
var default = {key1: default1, key2: default2, key 3: default 3};

var settings = $.extend(default, config);

//resulting properties of settings:
settings = {key1: value1, key2: default2, key 3: default 3};

Problem

This works great, but I'd like to reproduce this functionality without the need for jQuery. Is there an equally elegant (or close to) means to do this with plain ol' javascript?


Edit: Non-Duplicate Justification

This question is not a duplicate of the "How can I merge properties of two JavaScript objects dynamically?" question. Whereas that question simply wants to create an object that contains all of the keys and values from two separate objects - I specifically want to address how to do this in the event that both objects share some but not all keys and which object will get precedence (the default) for the resulting object in the event that there are duplicate keys. And even more specifically, I wanted to address the use of jQuery's method to achieve this and find an alternative way to do so without jQuery. While many of the answers to both questions overlap, that does not mean that the questions themselves are the same.

Propitiatory answered 25/6, 2012 at 20:56 Comment(3)
Just steal the way jQuery does it james.padolsey.com/jquery/#v=1.6.2&fn=jQuery.extend :-PModerato
Good idea @RocketHazmat, note if you copypasta that function it has a few other jQuery dependencies like jQuery.isPlainObject and jQuery.isFunctionEy
@RocketHazmat someone did it already: gomakethings.com/vanilla-javascript-version-of-jquery-extend/…Unmannerly
P
139

To get the result in your code, you would do:

function extend(a, b){
    for(var key in b)
        if(b.hasOwnProperty(key))
            a[key] = b[key];
    return a;
}

Keep in mind that the way you used extend there will modify the default object. If you don't want that, use

$.extend({}, default, config)

A more robust solution that mimics jQuery's functionality would be as follows:

function extend(){
    for(var i=1; i<arguments.length; i++)
        for(var key in arguments[i])
            if(arguments[i].hasOwnProperty(key))
                arguments[0][key] = arguments[i][key];
    return arguments[0];
}
Petaloid answered 25/6, 2012 at 21:3 Comment(2)
I would like to see a code example in action, maybe fiddle, because plain js is soo much cooler but so abstract, thanks a million.Bilander
this doesn't recurse, like $.extend doesXyloid
B
59

You can use the ECMA 2018 spread operator in object literals...

var config = {key1: value1};
var default = {key1: default1, key2: default2, key 3: default 3};

var settings = {...default, ...config}

//resulting properties of settings:
settings = {key1: value1, key2: default2, key 3: default 3};

BabelJS support for older browsers

Bulldozer answered 6/2, 2018 at 0:24 Comment(4)
Much the neatest and cleanest way of doing it!Mosemoseley
Does perform deep copy?Maiden
If it doesn't perform deep copy, it isn't a generalized replacement for extendAmorous
this does not do a deep copy as it's using the spread operator.Receptionist
G
33

You can use Object.assign.

var defaults = {key1: "default1", key2: "default2", key3: "defaults3"};
var config = {key1: "value1"};

var settings = Object.assign({}, defaults, config); // values in config override values in defaults
console.log(settings); // Object {key1: "value1", key2: "default2", key3: "defaults3"}

It copies the values of all enumerable own properties from one or more source objects to a target object and returns the target object.

Object.assign(target, ...sources)

It works in all desktop browsers except IE (but including Edge). It has mitigated mobile support.

See for yourself here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign

About deep copy

However, Object.assign does not have the deep option that jQuery's extend method have.

Note: you can generally use JSON for a similar effect though

var config = {key1: "value1"};
    var defaults = {key1: "default1", key2: "default2", keyDeep: {
        kd1: "default3",
        kd2: "default4"
    }};
    var settings = JSON.parse(JSON.stringify(Object.assign({}, defaults, config))); 
    console.log(settings.keyDeep); // Object {kd1: "default3", kd2: "default4"}
Grace answered 28/8, 2016 at 4:47 Comment(4)
Yeah, but here it jumps over the key1 from defaults and not adding it to the new object.Vandervelde
@Anonymous Actually it does add it, but it's overridden by the key1 value from the config array.Grace
I supposed that is happening. So, it's not adding it afterall.Vandervelde
The section "About deep copy" is wrong. First issue: parse( stringify( X ) ) === X. Second issue: "deep" means that keys whose value is an object will be merged too, recursively. So to prove it, you should use something like const a = { x: { a: 1 }, y: 2 }; const b = { x: { b: 2 }, y: 3 }; and verify that deep(a, b) === { x: { a:1, b:2 }, y: 3 }, which is exactly what jQuery deep does.Praxis
K
10

This is my slightly different approach with deep copy I came up with while trying to eliminate a jQuery dependency. It is mostly designed for being small so it might have not all feature one expects. Should be fully ES5-compatible (starting from IE9 due to usage of Object.keys):

function extend(obj1, obj2) {
    var keys = Object.keys(obj2);
    for (var i = 0; i < keys.length; i += 1) {
      var val = obj2[keys[i]];
      obj1[keys[i]] = ['string', 'number', 'array', 'boolean'].indexOf(typeof val) === -1 ? extend(obj1[keys[i]] || {}, val) : val;
    }
    return obj1;
  }

You may wonder what the fifth line does exactly do ... If obj2.key is an object literal (i.e. if it's no ordinary type) we recursively call extend on it. If a property with that name doesn't exist in obj1 yet, we initialize it to an empty object first. Otherwise we simply set obj1.key to obj2.key.

Here are some of my mocha/chai tests that should prove the common cases to work here:

it('should extend a given flat object with another flat object', () => {
  const obj1 = {
    prop1: 'val1',
    prop2: 42,
    prop3: true,
    prop4: 20.16,
  };
  const obj2 = {
    prop4: 77.123,
    propNew1: 'newVal1',
    propNew2: 71,
  };
  assert.deepEqual(utils.extend(obj1, obj2), {
    prop1: 'val1',
    prop2: 42,
    prop3: true,
    prop4: 77.123,
    propNew1: 'newVal1',
    propNew2: 71,
  });
});

it('should deep-extend a given flat object with a nested object', () => {
  const obj1 = {
    prop1: 'val1',
    prop2: 'val2',
  };
  const obj2 = {
    propNew1: 'newVal1',
    propNew2: {
      propNewDeep1: 'newDeepVal1',
      propNewDeep2: 42,
      propNewDeep3: true,
      propNewDeep4: 20.16,
    },
  };
  assert.deepEqual(utils.extend(obj1, obj2), {
    prop1: 'val1',
    prop2: 'val2',
    propNew1: 'newVal1',
    propNew2: {
      propNewDeep1: 'newDeepVal1',
      propNewDeep2: 42,
      propNewDeep3: true,
      propNewDeep4: 20.16,
    },
  });
});

it('should deep-extend a given nested object with another nested object and deep-overwrite members', () => {
  const obj1 = {
    prop1: 'val1',
    prop2: {
      propDeep1: 'deepVal1',
      propDeep2: 42,
      propDeep3: true,
      propDeep4: {
        propDeeper1: 'deeperVal1',
        propDeeper2: 777,
        propDeeper3: 'I will survive',
      },
    },
    prop3: 'lone survivor',
  };
  const obj2 = {
    prop1: 'newVal1',
    prop2: {
      propDeep1: 'newDeepVal1',
      propDeep2: 84,
      propDeep3: false,
      propDeep4: {
        propDeeper1: 'newDeeperVal1',
        propDeeper2: 888,
      },
    },
  };
  assert.deepEqual(utils.extend(obj1, obj2), {
    prop1: 'newVal1',
    prop2: {
      propDeep1: 'newDeepVal1',
      propDeep2: 84,
      propDeep3: false,
      propDeep4: {
        propDeeper1: 'newDeeperVal1',
        propDeeper2: 888,
        propDeeper3: 'I will survive',
      },
    },
    prop3: 'lone survivor',
  });
});

I'd be happy about feedback or comments on this implementation. Thanks in advance!

Kerr answered 20/11, 2016 at 14:22 Comment(1)
This implementation is better than the most upvoted answer. However, you should add other types to the indexOf test that you consider is a "primitive" value (such as function). obj1[keys[i]] = ['string', 'number', 'array', 'boolean', 'function'].indexOf(typeof val) === -1 ? extend(obj1[keys[i]] || {}, val) : val; or even: obj1[keys[i]] = typeof val === 'object' && !(val instanceof Array) ? extend(obj1[keys[i]] || {}, val) : val;Madiemadigan
G
3

I prefer this code that uses my generic forEachIn, and does not mangle the first object:

function forEachIn(obj, fn) {
    var index = 0;
    for (var key in obj) {
        if (obj.hasOwnProperty(key)) {
            fn(obj[key], key, index++);
        }
    }
}

function extend() {
    var result = {};
    for (var i = 0; i < arguments.length; i++) {
        forEachIn(arguments[i],
            function(obj, key) {
                result[key] = obj;
            });
    }
    return result;
}

If you really do want to merge stuff into the first object, you can do:

obj1 = extend(obj1, obj2);
Geoponics answered 21/11, 2016 at 16:42 Comment(1)
This is by far the best answer here. It does deep copy, does not mangle the first object, supports infinite objects, and can alter types while extending (i.e. many answers here will not extend a string to an object, this answer will). This gets every check mark in my book. full extend replacement, and extremely easy to understand.Hieronymus
A
2

You can loop through Object's properties using for statement.

var settings = extend(default, config);

function extend(a, b){
    var c = {};
    for(var p in a)
        c[p] = (b[p] == null) ? a[p] : b[p];
    return c;
}
Aerodyne answered 25/6, 2012 at 21:7 Comment(2)
That will not take into account properties that don't exist in a. Though it is not in the example, $.extend actually works that way, merging all properties of all objects.Imposition
As I read jquery base code, it actually does a recursive function to copy or clone the value. So it is deeply clone/copying, not just to copy at the first level as the above code.Cembalo
D
0

It helps me a lot when I develop with pure javascript.

function extends(defaults, selfConfig){
     selfConfig = JSON.parse(JSON.stringify(defaults));
     for (var item in config) {
         if (config.hasOwnProperty(item)) {
             selfConfig[item] = config[item];
         }
     }
     return selfConfig;
}
Driedup answered 24/3, 2016 at 14:48 Comment(0)
C
0

The approach of Ivan Kuckir's can also be adapted to create a new object prototype:

Object.prototype.extend = function(b){
for(var key in b)
    if(b.hasOwnProperty(key))
        this[key] = b[key];
    return this;
}

var settings = default.extend(config);
Corporeity answered 28/4, 2016 at 3:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.