0
0

More than 3 years have passed since last update.

【忘備録】Javascript : インスタンス生成、メソッド定義

Last updated at Posted at 2020-05-07

はじめに

今回はJavascriptでインスタンス(オブジェクト)生成からprototypeを使ってメソッド定義を学んで行きます。

前提

Javascriptにはインスタンスという概念は存在するものの、クラスという概念はありません。代わりにプロトタイプを使ってオブジェクト生成を行なっていきます。プロトタイプとはあくまでクラスではなく、「オブジェクトの基となるオブジェクト」です。Javascriptはこのプロトタイプを使ってオブジェクトを生成することから「プロトタイプベースのオブジェクト指向」と言われるそうです。

1, コンストラクターで初期化する

下記のコードでは、関数リテラルの内部にプロパティを二つ、メソッドを一つ定義しています。一見、関数リテラルを変数に代入しているだけと思われますが、これでJavascriptでのクラス定義が完了です。

javascript.js
let member = function(lastName,firstName){
  this.firstName = firstName; // プロパティの定義
  this.lastName = lastName; // プロパティの定義

  // メソッドの定義 (厳密にはメソッドがなく関数リテラルをプロパティに渡すことでメソッド的役割になる)
  this.getName = function(){
    return this.lastName + '' + this.firstName;
  }
}

// コンストラクターで初期化。インスタンスを生成。ここでは member1
let member1 = new member('山田','太郎');
console.log(member1.getName()); // => 山田 太郎
console.log(member1.firstName); // => 山田 (プロパティも参照可)
// 関数内の this はコンストラクターによって生成されるインスタンスを指す。ここでは member

変数memberには関数リテラルが代入されています。それをコンストラクターで初期化することでオブジェクトの生成ができます。let member1 = new member('山田','太郎');初期化する際に引数('山田','太郎')を渡す事でクラスのfirstNamelastNameにそれぞれ値が渡されます。これらがthis.firstNamethis.lastNameに代入されます。このthisは大体、予想できると思いますが変数memberを指しています。
このmemberを初期化することでコピーされオブジェクトが生成される訳なのでオブジェクトにも勿論、これらのプロパティを持っています。
同様にクラス内に定義されているgetNameメソッドもオブジェクトの中にコピーされています。クラス内に関数リテラルを再度、定義していますがこれでJavascriptにおけるメソッドの定義ができます。

補足(thisの参照先)

もし以下のようなコンストラクターで初期化しなかった場合の挙動について見てみます。

javascript.js
let member = function(firstName,lastName){
  this.firstName = firstName;
  this.lastName = lastName;

  getName = function(){
    return this.lastName + ' ' + this.firstName;
  }
}
let m = member('田中','太郎'); // new member('田中','太郎') となっていない。
// ここで member オブジェクトは生成されていない。
console.log(firstName); // => 田中(グローバルオブジェクトの firstName)
console.log(m.firstName); // Cannot read property 'firstName' of undefined
console.log(m.getName()); // Uncaught TypeError: Cannot read property 'getName' of undefined

当然、上記のlet m = member('田中','太郎');のように初期化しないままだとインスタンスは生成されず、変数mからプロパティやメソッドを呼び出そうとしても定義されていない為、エラーとなります。
しかし、console.log(firstName);はしっかり田中と出力されています。
実はこれはグローバル変数firstNameを出力しています。グローバル変数に定義した覚えがありませんが実は、let m = member('田中','太郎');の処理の際に関数リテラルmemberに引数が渡され、this.lastNamethis.firstNameに代入されるのですが、ここで使われているthisが指しているのはグローバルオブジェクトを指しています。なのでこの関数リテラルmemberを呼び出し、値を渡した時点でグローバル変数firstNamelastNameが生成されてしまっていたのです。

thisを使う際に気をつけるべきなのが使う箇所によって参照先が変わってしまうということです。

以下、 thisを使う箇所とその際の参照先を纏めておきます。
1、トップレベル(関数の外) : グローバルオブジェクト
2、関数 : グローバルオブジェクト
3、call/apply メソッド : 引数で指定されたオブジェクト
4、コンストラクター : 生成したインスタンス
5、メソッド : 呼び出し基のオブジェクト

2, メモリを意識したメソッド定義の方法

上記まで紹介してきた方法でクラスを定義し、内部にプロパティとメソッドを定義する方法だと挙動に問題は無いものの、消費メモリの観点からみて問題があります。クラス内部にメソッドを定義して、そのクラスを基にオブジェクトを生成した際にその内部全てがコピーされ、一意の独立したオブジェクトが生成されます。
しかし、オブジェクトによっては使わないメソッドが存在するケースがある為、使わないメソッドを都度、コピーしてしまってはメモリの無駄です。一つ、インスタンスを生成するだけならいいですが、これが大量のインスタンスを生成した時には、その分のメモリが無駄になってしまいます。

それを防ぐためにメソッドを定義する際には以下のようにプロトタイプを使って定義しましょう。

javascript.js

let member = function(lastName,firstName){
  this.lastName = lastName;
  this.firstName = firstName;
}

// プロトタイプとして定義するメンバー(メソッド)を指定
member.prototype = {
  getName : function(){
    return this.lastName + ' ' + this.firstName;
  },
  sayHello : function(){
    return 'Hello, ' + this.lastName;
  }
}

member1 = new member('鈴木','二郎');
console.log(member1.getName()); // => 鈴木 二郎
console.log(member1.sayHello()); // => Hello, 鈴木

メソッドを定義する際には上記のmember.prototype = {メソッド定義}のように定義しましょう。
このようにすることで、インスタンス化されたオブジェクト内部にはメソッドはコピーされず、インスタンスの元となるオブジェクト、(この場合のmember)の内部にprototypeプロパティとして格納されます。
インスタンス化されたオブジェクト(この場合のmember1)がメソッドgetNamesayHelloを呼び出した際にはインスタンスの元となるオブジェクト(この場合のmember)の内部に存在するprototypeまで参照しに行ってくれます。

補足(プロトタイプの隠蔽)

あまり、使用例が私には思いつかないのですがプロトタイプで定義したメソッドの処理内容を上書きしたり新たに追加したりできるのもプロトタイプの利点です。以下のコードをみてください。

javascript.js

let member = function(lastName,firstName){
    this.lastName = lastName;
    this.firstName = firstName;
  }

  // プロトタイプとして定義するメンバー(メソッド)を纏めて指定する方法
  member.prototype = {
    getName : function(){
      return this.lastName + ' ' + this.firstName;
    },
    sayHello : function(){
      return 'Hello, ' + this.lastName;
    }
  }

  member1 = new member('鈴木','二郎');
  console.log(member1.getName()); // => 鈴木 二郎
  console.log(member1.sayHello()); // => Hello, 鈴木

  member2 = new member('鈴木','二郎');
  member2.getName = function(){
    return '上書き'
  }
  console.log(member1.getName()); // => 鈴木 二郎
  console.log(member2.getName()); // => 上書き

member1に加えて全く同じ引数でmember2をインスタンス化して生成しました。
生成した後、member2.getName = function(){return '上書き'}というメソッドをmember2に追加します。
この状態で、console.log(member2.getName());getNameメソッドを呼び出すと狙い通り、上書き処理が出力されました。勿論、member1には上書きしたメソッドは存在しないのでしっかり、プロトタイプを参照しに行ってくれてます。
このようにプロトタイプで定義したメソッドを上書きすることを、プロトタイプの隠蔽と言います。このようにリアルタイムで定義を上書きしたりすることでより柔軟にオブジェクト毎に処理を定義することができました。

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