1. ホーム
  2. javascript

[解決済み] JavaScriptでカスタムオブジェクトを「正しく」作成する方法とは?

2022-03-20 16:59:41

質問

プロパティとメソッドを持つJavaScriptオブジェクトを作るにはどうしたらいいのでしょうか?

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

では .prototype を使用してプロパティを追加する人もいれば、インラインで追加する人もいます。

誰か、いくつかのプロパティとメソッドを持つJavaScriptオブジェクトの適切な例を教えてください。

どのように解決するのですか?

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が非常に杜撰で、引数をゼロで渡すことができ、その場合 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 オブジェクトが作成され、デバッグに大きな影響を与えることになります。) 実際には、このような現象は、メソッドを取り出して setTimeout , onclick または EventListener を一般的に使用します。

プロトタイプの方法では、そのような割り当てのためにクロージャを含めなければなりません。

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

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

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

インスタンスがクロージャ方式で作られている場合、バインディングはクロージャによってインスタンス変数(通常は that または self のように、個人的には後者をお勧めします。 self は、JavaScriptではすでに別の別の意味を持っています)。引数 1, 1 を使用すると、別のクロージャまたは bind() を行う必要がある場合。

クロージャーメソッドにもたくさんのバリエーションがあります。を省略することができます。 this を完全に作成し、新しい that を使用する代わりに、それを返します。 new 演算子を使用します。

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の継承はプロトタイピングに、単純な使い捨てのページ効果はクロージャにする傾向があります。

しかし、どちらの方法も、ほとんどのプログラマーにとっては、かなり直感に反するものです。どちらも多くの厄介なバリエーションがある可能性があります。他の人のコードやライブラリを使えば、その両方(と同時に多くの中間的で一般的に壊れたスキーム)に出会うことになります。一般的に受け入れられている答えはひとつもありません。JavaScript のオブジェクトの素晴らしい世界へようこそ。

[これは「なぜJavaScriptは私の好きなプログラミング言語ではないのか」の第94回である] 。