初めに
前回でコンストラクタ関数の継承方法をまとめて紹介しました。
JavaScriptのinheritanceについて(Constructor Function)
今回はコンストラクタ関数を使わない方法をまとめたいと思います。
↓は今回デモの初期コードです。
const Person = {
nation: 'Japan'
}
const Occupation = {
career: 'Business Man'
}
object()
これはjson
の発明者Douglas Crockfordの発想です。
function object(obj) {
function F() { }
F.prototype = obj
return new F()
}
コンストラクタ関数でなく、一般関数の内部に空関数のプロトタイプに継承対象のプロパティやメソッドを受け継がせ、新しいイスタンスを返すという方法です。
const Taro = object(Person)
console.log(Taro) // {}
Taro.career = Occupation.career
console.log(Taro.nation) // Japan
console.log(Taro.career) // Business Man
console.log(Taro.hasOwnProperty('nation')) // false // inherited from Person
console.log(Taro.hasOwnProperty('career')) // true
console.log(Taro.__proto__) // { nation: 'Japan' }
変数Taro
は関数F
の新しいインスタンスが付与され、同じ親F
のプロトタイプから受け継いだオブジェクト{ nation: 'Japan' }
が利用できるようになります。
F
は空関数だから、Taro
はローカルから'nation'
プロパティを受け継いだのではなかったため、hasOwnProperty()
の判定ではfalse
になります。
簡単に言うと、object()
はインスタンスの親のprototype
プロパティを継承対象へ変えることで継承を実現させました。
関数F
はnew
でインスタンスを創るからコンストラクタ関数じゃないんですか?
確かに私も書きながら疑問に思います。前回の文章書くとき、コンストラクタ関数の定義がnew
でインスタンス創るときthis
がコンテキストを自分のスコープを指すということで、this
オブジェクトで自分のスコープにプロパティを保存するという仕組みを書きましたが、でもこれはコンストラクタ関数の特徴でした。
もう一度調べたら、実はコンストラクタ関数というのはただの関数です。
関数を創るにつれてconstructor
プロパティも創られ、new
を通じてインスタンスを創ると、自分の親である関数に参照(値)を保存し、その親がコンストラクタ関数です。
なのでこのobject()
は間違いなくコンストラクタ関数を用いた継承方法です。
Copy
ここからCopy方法を紹介したいと思います。
Shallow copy
function extendCopy(obj) {
let copy = {}
for (let item in obj) {
copy[item] = obj[item]
}
copy.uber = obj
return copy
}
const Taro = extendCopy(Person)
console.log(Taro)
// { nation: 'Japan', uber: { nation: 'Japan' } }
Taro.career = Occupation.career
console.log(Taro.nation) // Japan
console.log(Taro.career) // Business Man
console.log(Taro.uber) // { nation: 'Japan' }
Shallow copyは浅いコピーのことです。
関数extendCopy
のように空オブジェクトcopy
に、obj
にある全てのitem
を参照し入れさせるということで、参照元はobj
オブジェクトにあります。
そしてcopy.uber
で自分の参照元がobj
とのことを明記します。(uberのドイツ語がその上という意味です。)
変数Taro
はコピーされたオブジェクトを受け取り、オブジェクトのプロパティやメソッドを使うようになります。
もしここで参照元に新しいプロパティができたらどうなるでしょう。
Person.birthPlaces = ['Tokyo', 'Osaka', 'Hokaidou']
const Jiro = extendCopy(Person)
Jiro.career = Occupation.career
console.log(Jiro.nation) // Japan
console.log(Jiro.career) // Business Man
console.log(Jiro.birthPlaces)
// [ 'Tokyo', 'Osaka', 'Hokaidou' ]
console.log(Jiro.uber)
// { nation: 'Japan', birthPlaces: [ 'Tokyo', 'Osaka', 'Hokaidou' ] }
console.log(Taro.birthPlaces) // undefined
変数Jiro
のように、新しいプロパティが書き込まれた後にコピーされたオブジェクトを保存したから、新しいプロパティが使えますが、Taro
は保存したオブジェクトにはbirthPlaces
がないため使えないです。
そしてこのShallow copy方法に一番デメリットなところです。
もしJiro
のプロパティを操作したらどうなるでしょう。
Jiro.birthPlaces.push('Fukuoka')
console.log(Jiro.birthPlaces)
// [ 'Tokyo', 'Osaka', 'Hokaidou', 'Fukuoka' ]
console.log(Person.birthPlaces)
// [ 'Tokyo', 'Osaka', 'Hokaidou', 'Fukuoka' ]
// note: JQuery
Jiro
もPerson
も同じオブジェクト(参照元)を保存しているので、片方がプロパティを変更したらもう片方を影響してしまいます。
Deep copy
Deep copy
function deepCopy(obj, copy) {
let copies = copy || {}
for (let item in obj) {
if (typeof obj[item] === 'object') {
copies[item] = (obj[item].constructor === Array) ? [] : {}
deepCopy(obj[item], copies[item])
} else {
copies[item] = obj[item]
}
}
return copies
}
Person.birthPlaces = ['Tokyo', 'Osaka', 'Hokaidou']
const Taro = deepCopy(Person)
Taro.career = Occupation.career
console.log(Taro.nation) // Japan
console.log(Taro.career) // Business Man
console.log(Taro.birthPlaces)
// [ 'Tokyo', 'Osaka', 'Hokaidou' ]
Deep copyは深いコピー、つまり参照元を保存するのでなく、その中のプロパティをコピーして、プロパティを保存する新しい参照元を創り出すということです。
最初この方法を見たとき分かりづらいだなあって思っちゃいました。
自分で変数のネーミングを変えたら少し良くなってきて、この部分も少し理解できました。
for (let item in obj) {
if (typeof obj[item] === 'object') {
copies[item] = (obj[item].constructor === Array) ? [] : {}
deepCopy(obj[item], copies[item])
} else {
copies[item] = obj[item]
}
}
return copies
つまりobj
プロパティの中にはArray
配列に該当するものがあれば、空の配列を返してもらう。そして該当の配列と空の配列と共にもう一回関数deepCopy
に入れて、該当配列の値をコピーしてからretrun
。obj
のfor
ループが終わるまでこの作業を続けるんです。
このやり方では完全にobj
のプロパティをコピーして新しいオブジェクト、新しい参照を返すから、これでTaro
オブジェクトにどんな操作してもPerson
オブジェクトに影響を与えることができません。
Taro.birthPlaces.push('Fukuoka')
console.log(Taro.birthPlaces)
// [ 'Tokyo', 'Osaka', 'Hokaidou', 'Fukuoka' ]
console.log(Person.birthPlaces)
// [ 'Tokyo', 'Osaka', 'Hokaidou' ]
逆にPerson
も、Taro
に影響できません。
↓は別の検証です。もし配列とオブジェクトをpush()
したらどうなる話です。
Person.birthPlaces.push(['A', 'B', 'C'])
console.log(Person.birthPlaces)
// [ 'Tokyo', 'Osaka', 'Hokaidou', [ 'A', 'B', 'C' ] ]
Person.birthPlaces.push({
E: 1,
F: 2,
G: 3,
})
console.log(Person.birthPlaces)
// [
// 'Tokyo',
// 'Osaka',
// 'Hokaidou',
// ['A', 'B', 'C'],
// { E: 1, F: 2, G: 3 }
// ]
console.log(Person)
// {
// nation: 'Japan',
// birthPlaces: [
// 'Tokyo',
// 'Osaka',
// 'Hokaidou',
// ['A', 'B', 'C'],
// { E: 1, F: 2, G: 3 }
// ]
// }
console.log(Taro.birthPlaces)
// [ 'Tokyo', 'Osaka', 'Hokaidou', 'Fukuoka' ]
console.log(Person.birthPlaces[4].E) // 1
// note: JQuery
データ(プロパティ)の取り出し方がさらに複雑になる気がする...。
まとめ
object()
はJavaScriptのinheritanceについて(Constructor Function)で空関数という媒介で継承を実現させたが、shallow copyもdeep copy本当の意味では継承ではなくデータをコピーして保存するだけです。copyのどちらでもJQueryで使われる継承方法で、どういった継承を行ったかこれでわかりました。