0
0

More than 1 year has passed since last update.

JavaScriptのinheritanceについて part2

Last updated at Posted at 2022-07-12

初めに

前回でコンストラクタ関数の継承方法をまとめて紹介しました。
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プロパティを継承対象へ変えることで継承を実現させました。

関数Fnewでインスタンスを創るからコンストラクタ関数じゃないんですか?

確かに私も書きながら疑問に思います。前回の文章書くとき、コンストラクタ関数の定義が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

JiroPersonも同じオブジェクト(参照元)を保存しているので、片方がプロパティを変更したらもう片方を影響してしまいます。

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に入れて、該当配列の値をコピーしてからretrunobjforループが終わるまでこの作業を続けるんです。

このやり方では完全に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 copydeep copy本当の意味では継承ではなくデータをコピーして保存するだけです。copyのどちらでもJQueryで使われる継承方法で、どういった継承を行ったかこれでわかりました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0