JavaScript でカスタム オブジェクトを「適切に」作成するにはどうすればよいでしょうか? 質問する

JavaScript でカスタム オブジェクトを「適切に」作成するにはどうすればよいでしょうか? 質問する

プロパティとメソッドを持つ JavaScript オブジェクトを作成するための最良の方法は何か疑問に思っています。

スコープが常に正しいことを確認するために、すべての関数で を使用しvar self = this、その後 を使用する例を見たことがあります。self.

.prototype次に、プロパティを追加するために を使用しており、他の例ではインラインでそれを行っている例を見てきました。

いくつかのプロパティとメソッドを持つ JavaScript オブジェクトの適切な例を教えていただけますか?

ベストアンサー1

JavaScript でクラスとインスタンスを実装するには、プロトタイピング方式とクロージャ方式の 2 つのモデルがあります。どちらにも利点と欠点があり、さまざまなバリエーションがあります。多くのプログラマーとライブラリは、言語の見苦しい部分を覆い隠すために、さまざまなアプローチとクラス処理ユーティリティ関数を持っています。

その結果、混在した環境では、メタクラスの寄せ集めとなり、それぞれが少しずつ異なる動作をします。さらに悪いことに、ほとんどの JavaScript チュートリアルの資料はひどいもので、あらゆる点を網羅するために何らかの妥協案を提示するため、非常に混乱してしまいます。(おそらく著者も混乱しているでしょう。JavaScript のオブジェクト モデルは、ほとんどのプログラミング言語と大きく異なり、多くの部分でまったく設計が間違っています。)

まずはプロトタイプの方法から始めましょう。これは最も JavaScript ネイティブな方法です。オーバーヘッド コードは最小限で、instanceof はこの種のオブジェクトのインスタンスで機能します。

function Shape(x, y) {
    this.x= x;
    this.y= y;
}

このコンストラクター関数のルックアップnew Shapeにメソッドを書き込むことで、作成されたインスタンスにメソッドを追加できます。prototype

Shape.prototype.toString= function() {
    return 'Shape at '+this.x+', '+this.y;
};

さて、これをサブクラス化します。JavaScript が行うことはサブクラス化と呼べるでしょう。これは、その奇妙な魔法のprototypeプロパティを完全に置き換えることによって行います。

function Circle(x, y, r) {
    Shape.call(this, x, y); // invoke the base class's constructor function to take co-ords
    this.r= r;
}
Circle.prototype= new Shape();

メソッドを追加する前に:

Circle.prototype.toString= function() {
    return 'Circular '+Shape.prototype.toString.call(this)+' with radius '+this.r;
}

この例はうまく機能し、多くのチュートリアルで同様のコードを目にすることになります。しかし、これはnew Shape()見苦しいものです。実際の Shape を作成する必要がないのに、基本クラスをインスタンス化しているのです。この単純なケースでは、JavaScript がずさんなためにうまく機能しています。JavaScript では引数を 0 個渡すことができ、その場合、xyが になりundefined、プロトタイプの と に割り当てられますthis.xthis.yコンストラクター関数がこれより複雑な処理を行っていた場合、完全に失敗するでしょう。

そこで、基本クラスのコンストラクター関数を呼び出さずに、クラス レベルで必要なメソッドやその他のメンバーを含むプロトタイプ オブジェクトを作成する方法を見つける必要があります。これを行うには、ヘルパー コードを書き始める必要があります。これは私が知っている最もシンプルなアプローチです。

function subclassOf(base) {
    _subclassOf.prototype= base.prototype;
    return new _subclassOf();
}
function _subclassOf() {};

これにより、プロトタイプ内の基本クラスのメンバーが、何もしない新しいコンストラクター関数に転送され、そのコンストラクターが使用されます。これで、次のように簡単に記述できます。

function Circle(x, y, r) {
    Shape.call(this, x, y);
    this.r= r;
}
Circle.prototype= subclassOf(Shape);

間違いの代わりに、new Shape()クラスを構築するための許容可能なプリミティブのセットができました。

このモデルでは、いくつかの改良と拡張を検討することができます。たとえば、以下は構文糖バージョンです。

Function.prototype.subclass= function(base) {
    var c= Function.prototype.subclass.nonconstructor;
    c.prototype= base.prototype;
    this.prototype= new c();
};
Function.prototype.subclass.nonconstructor= function() {};

...

function Circle(x, y, r) {
    Shape.call(this, x, y);
    this.r= r;
}
Circle.subclass(Shape);

どちらのバージョンにも、多くの言語と同様にコンストラクタ関数を継承できないという欠点があります。したがって、サブクラスが構築プロセスに何も追加しない場合でも、ベースが要求する引数を指定してベース コンストラクタを呼び出すことを覚えておく必要があります。これは を使用すると多少自動化できますapplyが、それでも次のように記述する必要があります。

function Point() {
    Shape.apply(this, arguments);
}
Point.subclass(Shape);

したがって、一般的な拡張は、初期化の部分をコンストラクター自体ではなく、独自の関数に分割することです。この関数は、ベースから問題なく継承できます。

function Shape() { this._init.apply(this, arguments); }
Shape.prototype._init= function(x, y) {
    this.x= x;
    this.y= y;
};

function Point() { this._init.apply(this, arguments); }
Point.subclass(Shape);
// no need to write new initialiser for Point!

これで、各クラスに同じコンストラクタ関数の定型文ができました。これを独自のヘルパー関数に移動して、たとえば の代わりに入力し続けなくても済むようにし、これを逆にして、Function.prototype.subclass基本クラスの Function がサブクラスを吐き出すようにします。

Function.prototype.makeSubclass= function() {
    function Class() {
        if ('_init' in this)
            this._init.apply(this, arguments);
    }
    Function.prototype.makeSubclass.nonconstructor.prototype= this.prototype;
    Class.prototype= new Function.prototype.makeSubclass.nonconstructor();
    return Class;
};
Function.prototype.makeSubclass.nonconstructor= function() {};

...

Shape= Object.makeSubclass();
Shape.prototype._init= function(x, y) {
    this.x= x;
    this.y= y;
};

Point= Shape.makeSubclass();

Circle= Shape.makeSubclass();
Circle.prototype._init= function(x, y, r) {
    Shape.prototype._init.call(this, x, y);
    this.r= r;
};

...これは、構文が少し不格好ではあるものの、他の言語に少し似てきました。必要に応じて、いくつかの追加機能を散りばめることができます。makeSubclassクラス名を取得して記憶し、それを使用してデフォルトを提供することtoStringもできます。コンストラクターが誤って演算子なしで呼び出されたときにそれを検出するようにすることもできますnew(そうしないと、非常に面倒なデバッグが発生することがよくあります)。

Function.prototype.makeSubclass= function() {
    function Class() {
        if (!(this instanceof Class))
            throw('Constructor called without "new"');
        ...

おそらく、すべての新しいメンバーを渡してmakeSubclassプロトタイプに追加し、Class.prototype...それほど多くのコードを書く手間を省きたいでしょう。多くのクラス システムがこれを行います。例:

Circle= Shape.makeSubclass({
    _init: function(x, y, z) {
        Shape.prototype._init.call(this, x, y);
        this.r= r;
    },
    ...
});

オブジェクト システムに望ましいと考えられる潜在的な機能は多数あり、特定の公式に誰も同意していません。


それでは、クロージャ方式です。継承をまったく使用しないことで、JavaScript のプロトタイプベースの継承の問題を回避します。代わりに、次のようになります。

function Shape(x, y) {
    var that= this;

    this.x= x;
    this.y= y;

    this.toString= function() {
        return 'Shape at '+that.x+', '+that.y;
    };
}

function Circle(x, y, r) {
    var that= this;

    Shape.call(this, x, y);
    this.r= r;

    var _baseToString= this.toString;
    this.toString= function() {
        return 'Circular '+_baseToString(that)+' with radius '+that.r;
    };
};

var mycircle= new Circle();

これで、 のすべてのインスタンスに、メソッド (および追加した他のメソッドや他のクラス メンバー)Shapeの独自のコピーが含まれるようになります。toString

各インスタンスが各クラス メンバーの独自のコピーを持つことの悪い点は、効率が悪いことです。サブクラス化されたインスタンスを大量に処理する場合は、プロトタイプ継承の方が適している可能性があります。また、ベース クラスのメソッドを呼び出すのも、ご覧のとおり少し面倒です。サブクラスのコンストラクターが上書きする前のメソッドを覚えておかないと、失われてしまいます。

[また、ここでは継承がないため、instanceof演算子は機能しません。必要な場合は、クラスをスニッフィングするための独自のメカニズムを提供する必要があります。プロトタイプ継承と同様にプロトタイプ オブジェクトをいじることもできますが、少しトリッキーで、instanceof動作させるためだけに行う価値はあまりありません。]

すべてのインスタンスが独自のメソッドを持つことの利点は、メソッドを、それを所有する特定のインスタンスにバインドできることです。これは、JavaScript のthisメソッド呼び出しでのバインド方法が奇妙であるため便利です。その結果、メソッドをその所有者から切り離すと、次のようになります。

var ts= mycircle.toString;
alert(ts());

メソッド内には、this期待どおりに Circle インスタンスが存在しません (実際にはグローバルwindowオブジェクトであるため、広範囲にわたるデバッグの問題が発生します)。実際には、メソッドが取得されて に割り当てられた場合setTimeoutonclickまたはEventListener一般に、これが通常発生します。

プロトタイプ方式では、このような割り当てごとにクロージャを含める必要があります。

setTimeout(function() {
    mycircle.move(1, 1);
}, 1000);

または、将来的には (または Function.prototype をハックすれば今) 次のようにすることもできますfunction.bind():

setTimeout(mycircle.move.bind(mycircle, 1, 1), 1000);

インスタンスがクロージャ方式で実行される場合、バインディングはインスタンス変数 (通常はthatまたは と呼ばれますselfが、個人的には後者はselfJavaScript で別の意味を持っているため、後者はお勧めしません) に対するクロージャによって無料で実行されます。ただし、上記のスニペットの引数は無料では取得できないため、必要な場合は1, 1別のクロージャまたは が必要になります。bind()

クロージャ メソッドにもさまざまなバリエーションがあります。演算子を使用する代わりに、this完全に省略して新しいものを作成し、それを返すことを好むかもしれません。thatnew

function Shape(x, y) {
    var that= {};

    that.x= x;
    that.y= y;

    that.toString= function() {
        return 'Shape at '+that.x+', '+that.y;
    };

    return that;
}

function Circle(x, y, r) {
    var that= Shape(x, y);

    that.r= r;

    var _baseToString= that.toString;
    that.toString= function() {
        return 'Circular '+_baseToString(that)+' with radius '+r;
    };

    return that;
};

var mycircle= Circle(); // you can include `new` if you want but it won't do anything

どちらの方法が「適切」でしょうか? 両方です。どちらが「最良」でしょうか? それは状況によります。参考までに、私は、OO に重点を置いた作業を行う場合は、実際の JavaScript 継承のプロトタイプを作成し、単純な使い捨てのページ効果の場合はクロージャを使用する傾向があります。

しかし、どちらの方法もほとんどのプログラマーにとって直感に反するものです。どちらも多くの潜在的な厄介なバリエーションがあります。他の人のコードやライブラリを使用すると、両方の方法 (および中間の、一般的には破綻した多くの方法) に遭遇することになります。一般的に受け入れられている答えは 1 つではありません。JavaScript オブジェクトの素晴らしい世界へようこそ。

[これは、「JavaScript が私のお気に入りのプログラミング言語ではない理由」の第 94 回目です。]

おすすめ記事