Core JavaScript #16 Chap.07 Class - 2

この記事はweb開発に欠かせないJavaScriptのコアな概念をしっかり理解しようということでCore Javascript(韓国語)を要約した内容になります。

目次

  1. JavaScriptにおけるクラス
  2. prototypeによるクラスの実現
    1. 基本実装
    2. クラスに具体的なデータを持たせない
    3. コンストラクタの復旧
    4. 上位クラスへのアクセサ提供
  3. ES6からのクラスと継承
  4. まとめ

prototypeによるクラスの実現

ES6からclassが導入される前はどのようにclassを実現したか理解する。

基本実装

多重プロトタイプチェーンを使って継承を実現する。

Gradeの例

// Arrayを継承したクラスを作成する例
var Grade = function () {
  var args = Array.prototype.slice.call(arguments);
  for (var i = 0; i < args.length; i++) {
    this[i] = args[i];
  }
  this.length = args.length;
};

Grade.prototype = [];

var g = new Grade(100, 80);
console.log(g.length);
console.log(g.filter(function (v) { return v > 90; }));
console.log(g.every(function (v) { return v > 50; }));

プロトタイプのチェーンを連結させるだけで継承が実現できる。 だが、上の例はlengthプロパーティが削除できてしまう(configurableな状態である)こととGrade.prototypeが空の配列を参照していることに問題がある。

g.push(90);
console.log(g); // Grade: { 0: 100, 1:80, 2:90, length:3 }

delete g.length;
g.push(70);
console.log(g); // Grade: { 0: 70, 1:80, 2:90, length:1 }

lengthが削除されるとg.lengthg.__proto__.lengthを参照することになる。 pushで要素が1個追加されたのでg.lengthは0→1になる。

RectangleとSquareの例

var Rectangle = function (width, height) {
  this.width = width;
  this.height = height;
};

Rectangle.prototype.getArea = function () {
  return this.width * this.height;
};

var rect = new Rectangle(3, 4);
console.log(rect.getArea()); // 12

var Square = function (width) {
  this.width = width;
};

Square.prototype.getArea = function () {
  return this.width * this.width;
}

var sq = new Square(5);
console.log(sq.getArea()); // 25

そもそも正方形は長方形に四つの辺の長さが等しいという条件が追加された図形なので 長方形を親クラス、正方形を子クラスとして抽象化することができる。

// Rectangleのコンストラクタをcallする
var Square = function (width) {
  Rectangle.call(this, width, width);
};
Square.prototype = new Rectangle();

var sq = new Square(5); // Rectangle { width:5, height:5, getArea: f() }
console.log(sq.getArea()); // 25

上の例もクラスの値の変更がインスタンスに影響を与えてしまう問題を抱えている。 deletesq.widthを削除しSquare.prototype.width4を割り当てるとgetAreaが20を返してしまうことになる。

delete sq.width;
Square.prototype.width = 4;
// sq.widthがundefinedのためsq.__proto__.widthを参照するため
// NaNが返される想定だったが4 * 5 = 20が返される
console.log(sq.getArea());

クラスに具体的なデータを持たせない

クラス(prototype)の値の変更がインスタンスに影響を与えてしまう問題の対策として、 シンプルにprototypeのプロパーティを削除した後凍結させる方法がある。

delete Square.prototype.width;
delete Square.prototype.height;
Object.freeze(Square.prototype);

2つ目の方法として、空のコンストラクタ(Bridge)を作成し そのprototypeは親クラスのprototypeを参照するようにして 子クラスのprototypeにはBridgeのインスタンスを割り当てる方法がある。

var Bridge = function () {};
Bridge.prototype = Rectangle.prototype;
Square.prototype = new Bridge();
Object.freeze(Square.prototype);

Square.prototypeRectangle.prototypeを参照しつつ 不要なインスタンスプロパーティを持たないようにすることが重要だ。

コンストラクタの復旧

SquareのコンストラクタがRectangleを参照しているのでSquare.prototype.constructorがSquareを参照するようにする

Square.prototype.constructor = Square;

これで継承と抽象化の実現が一通りできるようになった。

上位クラスへのアクセサ提供

親クラスのメソッドを呼び出すsuperを追加する。

Square.prototype.super = function (propName) {
  var self = this;
  if (!propName) {
    return function () {
      Rectangle.apply(self, arguments);
    }
  }
  var prop = Rectangle.prototype[propName];
  if (typeof prop !== 'function') {
    return prop;
  }
  return function () {
    prop.apply(self, arguments);
  }
}

var sq = new Square(10);
console.log(sq.getArea());          // 100
console.log(sq.super('getArea')()); // 100

ES6からのクラスと継承

ES6(ECMAScript2015)からはclass文法が正式導入された。 上の実装をせずclasssuperextends等のキーワードを使ってクラスを定義することができる。 ただし、ES6のクラスもprototypeで実現されたのでJavaScriptがプロトタイプベース言語であることに変わりはない。

JavaScriptのclass文法はあくまでプロトタイプを使って他の言語のクラスに相当することを実現するためのシンタックスシュガー(糖衣構文)に過ぎません。1

//---------ES6以前----------
var ES5 = function (name) {
  this.name = name;
};
ES5.staticMethod = function () {
  return this.name + ' staticMethod';
}

ES5.prototype.method = function () {
  return this.name = + ' method';
}

var es5Instance = new ES5('es5');
console.log(ES5.staticMethod()); // es5 staticMethod
console.log(es5Instance.method()); // es5 method

//---------ES6以降----------
var ES6 = class {
  constructor (name) {
    this.name = name;
  }

  static staticMethod () {
    return this.name + ' staticMethod';
  }

  method () {
    return this.name + ' method';
  }
};

var es6Instance = new ES6('es6');
console.log(ES6.staticMethod()); // es6 staticMethod
console.log(es6Instance.method()); // es6 method

継承も簡単に実現できる

var Rectangle = class {
  constructor (width, height) {
    this.width = width;
    this.height = height;
  }
  getArea () {
    return this.width * this.height;
  }
};

var Square = class extends Rectangle {
  constructor (width) {
    super(width, width);
  }
  getArea () {
    // superをオブジェクトとして使用することも可能
    // Rectangle.prototypeを参照することになる
    console.log('size is ', super.getArea());
  }
};

var sq = new Square(10);
sq.getArea(); // size is 100

まとめ

参考