JavaScript

JavascriptでObjectをextend / assign / mergeする4つの方法

More than 1 year has passed since last update.

何が問題か

JavascriptでObjectを拡張する際、Object.assignを使うことが多いと思いますが、レガシーなライブラリにはjQuery.extend, angular.extend等、似たようなものがいっぱいあります。そして、少しずつ仕様が違うため、古いコードのメンテナンス等の際には、混乱することがあります。そこで、レガシーなライブラリのextend / assign系の機能の違いを整理してみました。

1. Object階層:たどらない / prototype chain: たどらない

Object.assign
lodash.assign
angular.extend (Angular1.xのみ)

疑似コード

function assign(base, ...args){
  for(obj of args){
      for(const key in obj) {
      if(obj.hasOwnProperty(key){
              base[key] = obj[key];
          }  
      }
  }
  return base;
}

2. Object階層:たどらない / prototype chain: たどる

lodash.assignIn
jQuery.extend

疑似コード

function assignIn(base, ...args){
  for(obj of args){
      for(const key in obj) {
          base[key] = obj[key];
      }
  }
  return base;
}

3. Object階層:たどる / prototype chain: たどらない

lodash.merge
angular.merge (Angular1.xのみ)

解説

この場合、各種オブジェクトをどう扱うかが問題になります。angularの場合、Date, RegExp, DOMElement, Arrayに対応していて、それぞれ適切な形に変換されます。これ以外の一般的なオブジェクト(自分で作ったクラスなど)は、Plain Objectになってしまいます。lodashは調べていません。ちなみに、自分で作ったクラスの__proto__も適切にコピーするためには、以下の擬似コードのような実装が適切です。ただし、組み込みオブジェクトは、別に扱う必要があり、DateとRegExp以外の組み込みオブジェクトには対応していません。

余談ですが、angular.mergeは、deprecatedで、loash.mergeを使えとドキュメントに書いてあります。個人的には、なんでangular.extendがdeprecatedにならないのかが不思議です。

疑似コード

function merge(base, ...args) {
  for(const obj of args){
    if(typeof obj == 'object' && (obj instanceof Date || obj instanceof RegExp)) {
      base = new obj.__proto__.constructor(obj)
      continue;
    }
    if(typeof base != 'object') {
      base = Object.create(obj.__proto__)
    }
    for(const key in obj){
      if(obj.hasOwnProperty(key)){
        if(typeof obj[key] == 'object'){
          base[key] = merge(base[key], obj[key])
        }else {
          base[key] = obj[key];            
        }
      }
    }
  }
  return base;
}

4. Object階層:たどる / prototype chain: たどる

jQuery.extend(true)

解説

対応しているオブジェクトは、Arrayのみで、それ以外はPlain Objectになってしまいます。

まとめ

オブジェクトを拡張 / マージする際には、

  1. Objectの階層をたどるかどうか
  2. キーの列挙の際に、prototype chain をたどってコピーするか、
  3. __proto__をコピーするか、コピーせず、Plain Objectに変換するか
  4. 組み込みオブジェクトのコピーにどこまで対応するか

といった複雑な要素が絡み合っています。これらを考慮して、適切なライブラリを選択する必要があります。

多くの場合は、Object.assignとlodash.mergeで問題ないと思いますが、古いのコードの移植の際などは、機械的に変換してしまったりすると、痛い目遭いますので、くれぐれも注意してください。