何が問題か
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になってしまいます。
まとめ
オブジェクトを拡張 / マージする際には、
- Objectの階層をたどるかどうか
- キーの列挙の際に、prototype chain をたどってコピーするか、
- __proto__をコピーするか、コピーせず、Plain Objectに変換するか
- 組み込みオブジェクトのコピーにどこまで対応するか
といった複雑な要素が絡み合っています。これらを考慮して、適切なライブラリを選択する必要があります。
多くの場合は、Object.assignとlodash.mergeで問題ないと思いますが、古いのコードの移植の際などは、機械的に変換してしまったりすると、痛い目遭いますので、くれぐれも注意してください。