JavaScript odpowiednik metody rozszerzającej jQuery

94

tło

Mam funkcję, która przyjmuje configobiekt jako argument. W ramach funkcji mam również defaultobiekt. Każdy z tych obiektów zawiera właściwości, które zasadniczo działają jako ustawienia dla pozostałej części kodu w funkcji. Aby uniknąć konieczności określania wszystkich ustawień w configobiekcie, używam extendmetody jQuery do wypełnienia nowego obiektu, settingsz dowolnymi wartościami domyślnymi z defaultobiektu, jeśli nie zostały określone w configobiekcie:

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

To działa świetnie, ale chciałbym odtworzyć tę funkcjonalność bez potrzeby korzystania z jQuery. Czy istnieje równie elegancki (lub zbliżony) sposób zrobienia tego za pomocą zwykłego javascript?


Edycja: Wyrównanie bez powtórzeń

To pytanie nie jest powtórzeniem pytania „ Jak mogę dynamicznie scalać właściwości dwóch obiektów JavaScript? ”. Podczas gdy to pytanie chce po prostu utworzyć obiekt, który zawiera wszystkie klucze i wartości z dwóch oddzielnych obiektów - szczególnie chcę się zająć, jak to zrobić w przypadku, gdy oba obiekty mają niektóre, ale nie wszystkie klucze, i który obiekt uzyska pierwszeństwo wartość domyślna) dla wynikowego obiektu w przypadku zduplikowanych kluczy. A dokładniej, chciałem zająć się wykorzystaniem metody jQuery, aby to osiągnąć i znaleźć alternatywny sposób na zrobienie tego bez jQuery. Chociaż wiele odpowiedzi na oba pytania pokrywa się, nie oznacza to, że same pytania są takie same.

mbeasley
źródło
8
Po prostu ukradnij sposób, w jaki robi to jQuery james.padolsey.com/jquery/#v=1.6.2&fn=jQuery.extend :-P
Rocket Hazmat
Dobry pomysł @RocketHazmat, zauważ, że jeśli kopiujesz tę funkcję, ma ona kilka innych zależności jQuery, takich jak jQuery.isPlainObjectijQuery.isFunction
Andy Raddatz

Odpowiedzi:

133

Aby uzyskać wynik w swoim kodzie, wykonaj następujące czynności:

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

Pamiętaj, że sposób, w jaki użyłeś rozszerzenia, zmodyfikuje domyślny obiekt. Jeśli tego nie chcesz, użyj

$.extend({}, default, config)

Bardziej niezawodne rozwiązanie, które naśladuje funkcjonalność jQuery, wyglądałoby następująco:

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
źródło
1
Chciałbym zobaczyć przykład kodu w akcji, może skrzypce, ponieważ zwykły js jest o wiele fajniejszy, ale tak abstrakcyjny, dzięki milionowi.
thednp
3
to się nie powtarza, tak jak $ .extend robi
ekkis
30

Możesz użyć 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"}

Kopiuje wartości wszystkich wyliczalnych właściwości własnych z co najmniej jednego obiektu źródłowego do obiektu docelowego i zwraca obiekt docelowy.

Object.assign(target, ...sources)

Działa we wszystkich przeglądarkach komputerowych z wyjątkiem IE (ale w tym Edge). Ma złagodzone wsparcie mobilne.

Przekonaj się tutaj: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/ assign

O głębokiej kopii

Jednak Object. assign nie ma opcji deep, którą ma metoda rozszerzenia jQuery.

Uwaga: generalnie możesz użyć JSON dla podobnego efektu

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"}
molwa
źródło
Tak, ale tutaj przeskakuje on key1 z wartości domyślnych i nie dodaje go do nowego obiektu.
Ionut Necula,
@Anonymous Właściwie to dodaje go, ale jest zastępowany przez wartość key1 z tablicy config.
trwa
Przypuszczałem, że to się dzieje. Więc ostatecznie nie jest to dodawanie.
Ionut Necula
5

To jest moje nieco inne podejście do głębokiego kopiowania, które wymyśliłem, próbując wyeliminować zależność od jQuery. Jest głównie zaprojektowany jako mały, więc może nie mieć wszystkich funkcji, których można się spodziewać. Powinien być w pełni kompatybilny z ES5 (począwszy od IE9 ze względu na użycie 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;
  }

Możesz się zastanawiać, co dokładnie robi piąta linia ... Jeśli obj2.key jest literałem obiektowym (tj. Jeśli nie jest to zwykły typ), rekurencyjnie wywołujemy na nim rozszerzenie. Jeśli właściwość o tej nazwie jeszcze nie istnieje w obj1, najpierw inicjalizujemy ją w pustym obiekcie. W przeciwnym razie po prostu ustawiamy obj1.key na obj2.key.

Oto niektóre z moich testów mokki / herbaty, które powinny udowodnić, że sprawdzają się tutaj typowe przypadki:

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

Byłbym zadowolony z opinii lub komentarzy na temat tej implementacji. Z góry dziękuję!

Rico Pfaus
źródło
2

Możesz przeglądać właściwości Object za pomocą forinstrukcji.

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
źródło
2
To nie będzie uwzględniać właściwości, których nie ma w a. Chociaż nie ma tego w przykładzie, w $.extendrzeczywistości działa w ten sposób, łącząc wszystkie właściwości wszystkich obiektów.
Felix Kling
1
Czytając kod bazowy jquery, faktycznie wykonuje on rekurencyjną funkcję kopiowania lub klonowania wartości. Więc jest to głębokie klonowanie / kopiowanie, a nie tylko kopiowanie na pierwszym poziomie, jak powyższy kod.
aladine
2

Wolę ten kod, który używa mojego ogólnego forEachIn i nie zmienia pierwszego obiektu:

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

Jeśli naprawdę chcesz scalić rzeczy w pierwszy obiekt, możesz zrobić:

obj1 = extend(obj1, obj2);
GeeWhizBang
źródło
To zdecydowanie najlepsza odpowiedź. Robi głębokie kopiowanie, nie zmienia pierwszego obiektu, obsługuje nieskończone obiekty i może zmieniać typy podczas rozszerzania (tj. Wiele odpowiedzi tutaj nie rozszerzy łańcucha na obiekt, ta odpowiedź będzie). To dostaje każdy znacznik wyboru w mojej książce. pełna wymiana i niezwykle łatwe do zrozumienia.
Nick Steele
0

Bardzo mi pomaga, kiedy pracuję w czystym 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;
}
Alan Ktquez
źródło
0

Podejście Ivana Kuckira można również dostosować do stworzenia nowego prototypu obiektu:

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
źródło