JavaScript equivalente ao método extend do jQuery

92

fundo

Tenho uma função que usa um configobjeto como argumento. Dentro da função, também tenho defaultobjeto. Cada um desses objetos contém propriedades que funcionam essencialmente como configurações para o resto do código dentro da função. A fim de evitar ter que especificar todas as configurações dentro do configobjeto, eu uso o extendmétodo jQuery para preencher um novo objeto, settingscom quaisquer valores padrão do defaultobjeto, se eles não foram especificados no configobjeto:

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};

Problema

Isso funciona muito bem, mas eu gostaria de reproduzir essa funcionalidade sem a necessidade do jQuery. Existe um meio igualmente elegante (ou próximo de) para fazer isso com o velho javascript?


Editar: Justificativa Não Duplicada

Esta pergunta não é uma duplicata da pergunta " Como posso mesclar propriedades de dois objetos JavaScript dinamicamente? " Considerando que essa questão deseja simplesmente criar um objeto que contém todas as chaves e valores de dois objetos separados - eu quero especificamente abordar como fazer isso no caso de ambos os objetos compartilharem algumas, mas não todas as chaves e qual objeto terá precedência ( o padrão) para o objeto resultante no caso de haver chaves duplicadas. E ainda mais especificamente, eu queria abordar o uso do método jQuery para conseguir isso e encontrar uma maneira alternativa de fazer isso sem jQuery. Embora muitas das respostas a ambas as perguntas se sobreponham, isso não significa que as próprias perguntas sejam as mesmas.

mbeasley
fonte
8
Simplesmente roube da maneira que o jQuery faz james.padolsey.com/jquery/#v=1.6.2&fn=jQuery.extend :-P
Rocket Hazmat
Boa ideia @RocketHazmat, observe que se você copiar essa função, ela terá algumas outras dependências jQuery como jQuery.isPlainObjectejQuery.isFunction
Andy Raddatz

Respostas:

131

Para obter o resultado em seu código, você faria:

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

Lembre-se de que a maneira como você usou extender irá modificar o objeto padrão. Se você não quer isso, use

$.extend({}, default, config)

Uma solução mais robusta que imita a funcionalidade do jQuery seria a seguinte:

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];
}
Ryan Lynch
fonte
1
Eu gostaria de ver um exemplo de código em ação, talvez violino, porque o plain js é muito mais legal, mas tão abstrato, um milhão de agradecimentos.
dia
3
isso não recursiva, como $ .extend faz
ekkis
30

Você pode usar 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"}

Ele copia os valores de todas as propriedades próprias enumeráveis ​​de um ou mais objetos de origem para um objeto de destino e retorna o objeto de destino.

Object.assign(target, ...sources)

Ele funciona em todos os navegadores de desktop, exceto o IE (mas incluindo o Edge). Tem suporte móvel mitigado.

Veja você mesmo aqui: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign

Sobre cópia profunda

No entanto, Object.assign não tem a opção deep que o método extend do jQuery tem.

Nota: você geralmente pode usar JSON para um efeito semelhante, embora

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"}
ling
fonte
Sim, mas aqui ele salta sobre o key1 dos padrões e não o adiciona ao novo objeto.
Ionut Necula
@Anonymous Na verdade, ele adiciona, mas é sobrescrito pelo valor key1 do array de configuração.
ling
Eu acho que isso está acontecendo. Então, não está adicionando afinal.
Ionut Necula
4

Esta é a minha abordagem ligeiramente diferente com cópia profunda que criei ao tentar eliminar uma dependência do jQuery. Ele foi projetado principalmente para ser pequeno, então pode não ter todos os recursos que você espera. Deve ser totalmente compatível com ES5 (a partir do IE9 devido ao uso de 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;
  }

Você pode se perguntar o que a quinta linha faz exatamente ... Se obj2.key for um objeto literal (ou seja, se não for um tipo comum), chamamos recursivamente de extender nele. Se uma propriedade com esse nome ainda não existe em obj1, nós a inicializamos como um objeto vazio primeiro. Caso contrário, simplesmente definimos obj1.key como obj2.key.

Aqui estão alguns dos meus testes de mocha / chai que devem provar que os casos comuns funcionam aqui:

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',
  });
});

Eu ficaria feliz com feedback ou comentários sobre esta implementação. Desde já, obrigado!

Rico Pfaus
fonte
2

Você pode percorrer as propriedades do objeto usando a forinstrução.

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;
}
Ivan Kuckir
fonte
2
Isso não levará em consideração propriedades que não existem em a. Embora não esteja no exemplo, $.extendna verdade funciona dessa forma, mesclando todas as propriedades de todos os objetos.
Felix Kling
1
Conforme leio o código base do jquery, ele realmente executa uma função recursiva para copiar ou clonar o valor. Portanto, é clonar / copiar profundamente, não apenas copiar no primeiro nível como o código acima.
aladine de
2

Eu prefiro este código que usa meu forEachIn genérico e não destrói o primeiro objeto:

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;
}

Se você realmente deseja mesclar coisas no primeiro objeto, pode fazer:

obj1 = extend(obj1, obj2);
GeeWhizBang
fonte
Esta é de longe a melhor resposta aqui. Ele copia profundamente, não destrói o primeiro objeto, suporta objetos infinitos e pode alterar os tipos enquanto estende (ou seja, muitas respostas aqui não estenderão uma string para um objeto, esta resposta irá). Isso recebe todas as marcas de seleção no meu livro. substituição de extensão total e extremamente fácil de entender.
Nick Steele
0

Isso me ajuda muito quando eu desenvolvo com javascript puro.

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;
}
Alan Ktquez
fonte
0

A abordagem de Ivan Kuckir também pode ser adaptada para criar um novo protótipo de objeto:

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);
Doug Manuel
fonte