『Effective JavaScript』を読んで(項目30~36)

エフェクティブジャバスクリプトシリーズの続き。ようやく半分きた・・・。

本自体は今日読み終わったけど、まとめるのはまだ半分だ。 ただ、こうやってまとめるためにもう一度目を通すことで理解度は高まるし、記憶への定着度合いも高まった気がする(まぁ、まだ時間が経ってないから計測できてないけど)

一年くらい前に Coursera の Programming Language の講義で静的スコープと動的スコープを学習するときに「オブジェクト指向の this は動的スコープだ」というようなことを習った記憶がある(言語は Ruby だったな)。当時はなんとなーく理解した感じだったけど、この本で JavaScript の this の仕組みを勉強してより深く理解できた気がする。たしかに this は動的に解決されていて、呼び出しのコンテキストに依存して値が結合されているなぁと思った。

項目30:prototype、getPrototypeOf、__proto__ の違いを理解する

プロトタイプに関連してオブジェクトは 3 種類のアクセッサを持っている。

  • C.prototype:new C() によって作成されるオブジェクトのプロトタイプを決める
  • Object.getPrototypeOf(obj):obj のプロトタイプオブジェクトを取り出すための標準機構
  • obj._proto__:obj のプロトタイプオブジェクトを取り出すための非標準機構
function User(name, passwordHash) {
    this.name = name;
    this.passwordHash = passwordHash;
}

User.prototype.toString = function () {
    return "[User " + this.name + "]";
}

User.prototype.checkPassword = function(password) { 
    return hash(password) === this.passwordHash;
}

var user = new User("hjm333", "hash value");

上記 User 関数はデフォルトでプロトタイプオブジェクトを持っていて、prototype プロパティを持っている。

この例では prototype プロパティを介してプロトタイプオブジェクトに toString と checkPassword メソッドを追加している。 new 演算子を使って User のインスタンスを作成するとき、作成されるオブジェクト user にはプロトタイプオブジェクトとして、User.prototype に格納されたオブジェクトが自動的に代入される。

プロパティのルックアップは、そのオブジェクトの独自プロパティ(own property)のサーチから開始される。この例でいえば、user.name と user.passwordHash である。user で見つからなかったプロパティは u のプロトタイプオブジェクトの中からルックアップされる。u.checkPassword をアクセスすると User.prototypte に格納された checkPassword メソッドが取り出される。

コンストラクタ関数(User)の prototype プロパティ(一番目)が、新しいインスタンスのプロトタイプ関係を表すのに対して、Object.getPrototypeOf() 関数(二番目)はオブジェクトのプロトタイプを取り出すのに使える。__proto__ プロパティ(三番目)はこのプロパティを取り出す方法の非標準の方法である。

Object.getPrototypeOf(u) === User.prototype; // => true
u.__proto__ === User.prototype; // => true

JavaScript におけるクラスとは、コンストラクタ関数と、クラスのインスタンスの間でメソッドを共有するためのプロトタイプオブジェクトを組み合わせたものである。

項目31:__proto__ よりも Object.getPrototypeOf が好ましい

非標準の方法である __proto__ では完全な互換性がないため、Object.getPrototypeOf を利用するのが望ましい。

たとえば一部の環境では null をプロトタイプとするオブジェクトは _proto__ プロパティを持っていないが、ほとんどの環境では _proto__ プロパティを持っている。

var empty = Object.create(null);
"__proto__" in empty; // => false (一部の環境で)
"__proto__" in empty; // => true (ほとんどの環境で)

Object.getPrototypeOf が提供されない環境では、以下のように __proto__ を使った実装を提供できる。

if (typeof Object.getPrototypeOf === "undefined") {
    Object.getPrototypeOf = function(obj) { 
        var t = typeof obj;
        if (!obj || (t !== "object" && t !== "function")) {
            throw new TypeError("not an object");
        }
        return obj.__proto__;
    } 
}

項目32:__proto__ は決して変更しないこと

Object.getPrototypeOf が提供していない __proto__ が持つ特別な能力は、プロトタイプリンクを変更する能力である。しかし、様々な問題があるので、プロトタイプリンクを変更するのは避けた方が良い。

理由として以下のような問題が挙げられる。

  • 可搬性の問題。すべてのプラットフォームがこの変更をサポートしているわけではない
  • 性能の問題。__proto__ の変更はオブジェクトの継承構造そのものを変更する。これによって最適化の一部が無効になってしまう場合がある。
  • メンテナンス性の問題。プロトタイプリンクを動的に変更すると継承構造が動的に変更され、プログラムをメンテナンスするのが困難になる。

新しいオブジェクトにプロトタイプを提供するのなら Object.create(prototype) という標準の関数を利用しよう。

項目33:new に依存しないコンストラクタの作り方

項目30で示した User 関数のようなコンストラクタの場合、new 演算子付を呼び出すことを忘れない必要がある。このコンストラクタはレシーバができたてのオブジェクトであることを前提としている。もし、new 演算子を付け忘れると this がグローバルオブジェクトに束縛され、分かりづらいバグをもたらす。

var u = User("hjm333", "passwordHash"); // new 演算子付け忘れ
u; // => undefined
name; // => "hjm333", グローバルオブジェクトに束縛されている

strict モードを利用するともう少し早くエラーを補足できる

function User(name, passwordHash) {
    "use strict"; // strict モードの利用
    this.name = name;
    this.passwordHash = passwordHash
}
var u = User("hjm333", "passwordHash"); // => error: this が undefined

new 演算子をつけてもつけなくても意図通りに動作するようにするために、レシーバ this の値が User のインスタンスとして正しいかを調べて、正しくない場合には明示的に new 演算子をつけた User() を呼んでしまう方法がある。

function User(name, passwordHash) {
    if (!(this, instanceof User)) {
        return new User(name, passwordHash); // 明示的に new 演算子付で呼ぶ
    }
    this.name = name;
    this.passwordHash = passwordhash;
}

他に風変わりな方法として Object.create を使う方法がある。

function User(name, passwordHash) {
    var self = this instanceof User ? this : Object.create(User.prototype);
    self.name = name;
    self.passwordHash = passwordhash;
    return self;
}

Object.create が利用できない環境では、以下のコードで提供できる。

// Object.create が利用できない環境の場合
if (typeof Object.create === "undefined") {
    Object.create = function(prototype) {
        function C() { }
        c.prototype = prototype;
        return new C();
    }
}

新しいバージョンの User 関数を new 演算子付で呼び出すと、コンストラクタの上書きパターンが適用され、関数呼び出しの場合と同じ振る舞いになる。JavaScript では new 式の結果をコンストラクタ関数から明示的に返された結果で上書きすることを許しているためだ。

いろいろと考慮するのは面倒くさいのだが、もし必ずコンストラクタ関数に new 演算子付で呼び出してほしいのならそれを強調して文書化しておこう。

項目34:メソッドをプロトタイプに格納しよう

JavaScript でも、プロトタイプなしでプログラミングすることは可能であるが、プロトタイプオブジェクトで共通してメソッドを実装した方が効率が良い。

function User(name, passwordHash) { 
   this.name = name;
   this.passwordHash = passwordHash;
   this.toString = function() { ... }
   this.checkPassword = function() { ... }
}
var u = User("hjm333", "passwordHash");

プロトタイプを利用しない上記のような方法でも User オブジェクトは構築できるが、複数の User オブジェクトを作成すると toString と checkPassword 関数(オブジェクト)が毎回作成されるため、メモリの無駄になる。

プロトタイプオブジェクトで共通して toString と checkPassowrd を実装すると、メモリの無駄を省ける。加えて、最近の JavaScript エンジンはプロトタイプオブジェクトのルックアップを強力に最適化しており、その恩恵を得られる可能性が高まる。

項目35:プライベートデータの格納にはクロージャを使おう

JavaScript のオブジェクトシステムは、情報隠蔽(Information hiding)を強制する機構を特に持っていない(誰かが「オブジェクト指向の本質は情報隠蔽である」と言ってた気がするのだが…)。 そのため、JavaScript では命名規則(アンダースコアをつけるなど)を使ってユーザーに「これはプライベートプロパティですよ」という意図を明示する場合がある。

もし、厳密にプライベートプロパティを作成したいのであればクロージャを利用する。

function User(name, passwordHash) { 
    this.toString = function () {
        return "[User " + name + "]";
    }
    this.checkPassword = function(password) { 
        return hash(password) === passwordHash;
    };
}

このように定義された User クラスのオブジェクトでは name と passwordHash には直接アクセスできないのでプライベートプロパティとなる。

ただし、このようにクラスを定義すると、各メソッドのコピーがインスタンスごとに増殖するため、項目33の「メモリが無駄になる」というデメリットが露呈するので注意する。

項目36:インスタンスの状態は、インスタンスオブジェクトにだけ保存する

インスタンスの状態を示すプロパティは正しくインスタンスオブジェクトに配置する必要がある。間違ってインスタンスで共有されるプロパティをプロトタイプオブジェクトに配置するとバグとなってしまう。

すなわち、一般的にはプロトタイプオブジェクトに状態を持たせてはいけない。

どうでもいいけど、こういう n 対 n の関係を正しく把握してプロパティに配置したりするのを考える作業ってプログラミングの中の作業でもけっこう好きだったりする。