web-dev-qa-db-ja.com

javascriptでスーパーをエミュレートする

基本的に、次のいずれかと同じくらい単純な構文でsuperをエミュレートするための優れたエレガントなメカニズムがあります。

  • this.$super.prop()
  • this.$super.prop.apply(this, arguments);

守るべき基準は次のとおりです。

  1. _this.$super_はプロトタイプへの参照である必要があります。つまり、実行時にスーパープロトタイプを変更すると、この変更が反映されます。これは基本的に、親が新しいプロパティを持っていることを意味します。これは、親へのハードコードされた参照が変更を反映するのと同じように、実行時にsuperを介してすべての子に表示される必要があります
  2. this.$super.f.apply(this, arguments);は、再帰呼び出しで機能する必要があります。継承チェーンを上るときに複数のスーパーコールが行われる継承のチェーンセットの場合、再帰的な問題にぶつかってはなりません。
  3. お子様のスーパーオブジェクトへの参照をハードコーディングしてはなりません。つまりBase.prototype.f.apply(this, arguments);はポイントを打ち負かします。
  4. X toJavaScriptコンパイラまたはJavaScriptプリプロセッサを使用しないでください。
  5. ES5に準拠している必要があります

素朴な実装は次のようになります。

_var injectSuper = function (parent, child) {
  child.prototype.$super = parent.prototype;
};
_

しかし、これ 条件2を破る

これまでに見た中で最も洗練されたメカニズムは IvoWetzelのeval hack です。これは、ほとんどJavaScriptプリプロセッサであるため、基準4に失敗します。

25
Raynos

あなたが言及した「再帰的スーパー」問題からの「自由な」方法はないと思います。

thisをいじることはできません。そうすると、非標準的な方法でプロトタイプを変更するか、プロトタイプチェーンを上に移動して、インスタンス変数が失われるためです。したがって、スーパークラスを実行するときは、その責任をthisまたはそのプロパティの1つに渡さずに、「現在のクラス」と「スーパークラス」を知っている必要があります。

私たちが試してみることができることはたくさんありますが、私が考えることができるすべては、いくつかの望ましくない結果をもたらします:

  • 作成時に関数にスーパー情報を追加し、arguments.caleeまたは同様の悪を使用してアクセスします。
  • スーパーメソッドを呼び出すときに追加情報を追加する

    $super(CurrentClass).method.call(this, 1,2,3)
    

    これにより、現在のクラス名を複製する必要があります(したがって、スーパークラスをスーパーディクショナリで検索できます)が、少なくともスーパークラス名を複製する必要があるほど悪くはありません(継承関係との結合が悪い場合は、クラス自身の名前との内部結合)

    //Normal Javascript needs the superclass name
    SuperClass.prototype.method.call(this, 1,2,3);
    

    これは理想からはほど遠いですが、 2.x Python から少なくともいくつかの歴史的な前例があります。 (3.0のスーパーを「修正」したので、引数はもう必要ありませんが、それに伴う魔法の量とJSへの移植性はわかりません)


編集:作業中 フィドル

var superPairs = [];
// An association list of baseClass -> parentClass

var injectSuper = function (parent, child) {
    superPairs.Push({
        parent: parent,
        child: child
    });
};

function $super(baseClass, obj){
    for(var i=0; i < superPairs.length; i++){
        var p = superPairs[i];
        if(p.child === baseClass){
            return p.parent;
        }
    }
}
10
hugomg

John Resigは、シンプルでありながら優れたsuperサポートを備えた非固有メカニズムを投稿しました。唯一の違いは、superが呼び出し元の基本メソッドを指していることです。

http://ejohn.org/blog/simple-javascript-inheritance/ をご覧ください。

5
ngryman

superの主な問題は、私がhereと呼ぶもの、つまりスーパーリファレンスを作成するメソッドを含むオブジェクトを見つける必要があることです。これは、セマンティクスを正しくするために絶対に必要です。明らかに、hereのプロトタイプがあることも同様に優れていますが、それでも大きな違いはありません。以下は静的な解決策です。

// Simulated static super references (as proposed by Allen Wirfs-Brock)
// http://wiki.ecmascript.org/doku.php?id=harmony:object_initialiser_super

//------------------ Library

function addSuperReferencesTo(obj) {
    Object.getOwnPropertyNames(obj).forEach(function(key) {
        var value = obj[key];
        if (typeof value === "function" && value.name === "me") {
            value.super = Object.getPrototypeOf(obj);
        }
    });
}

function copyOwnFrom(target, source) {
    Object.getOwnPropertyNames(source).forEach(function(propName) {
        Object.defineProperty(target, propName,
            Object.getOwnPropertyDescriptor(source, propName));
    });
    return target;
};

function extends(subC, superC) {
    var subProto = Object.create(superC.prototype);
    // At the very least, we keep the "constructor" property
    // At most, we preserve additions that have already been made
    copyOwnFrom(subProto, subC.prototype);
    addSuperReferencesTo(subProto);
    subC.prototype = subProto;
};

//------------------ Example

function A(name) {
    this.name = name;
}
A.prototype.method = function () {
    return "A:"+this.name;
}

function B(name) {
    A.call(this, name);
}
// A named function expression allows a function to refer to itself
B.prototype.method = function me() {
    return "B"+me.super.method.call(this);
}
extends(B, A);

var b = new B("hello");
console.log(b.method()); // BA:hello
2

次の実装では、$superを介して呼び出されるメソッド内にいる場合、親クラスでの作業中のプロパティへのアクセスは、格納されているメンバーにアクセスしない限り、子クラスのメソッドまたは変数に解決されないことに注意してください。オブジェクト自体に直接(プロトタイプにアタッチされるのではなく)。これにより、多くの混乱を回避できます(微妙なバグとして読み取られます)。

更新:これは__proto__なしで機能する実装です。キャッチは、$superを使用すると、親オブジェクトが持つプロパティの数が線形になることです。

function extend (Child, prototype, /*optional*/Parent) {
    if (!Parent) {
        Parent = Object;
    }
    Child.prototype = Object.create(Parent.prototype);
    Child.prototype.constructor = Child;
    for (var x in prototype) {
        if (prototype.hasOwnProperty(x)) {
            Child.prototype[x] = prototype[x];
        }
    }
    Child.prototype.$super = function (propName) {
        var prop = Parent.prototype[propName];
        if (typeof prop !== "function") {
            return prop;
        }
        var self = this;
        return function () {
            var selfPrototype = self.constructor.prototype;
            var pp = Parent.prototype;
            for (var x in pp) {
                self[x] = pp[x];
            }
            try {
                return prop.apply(self, arguments);
            }
            finally {
                for (var x in selfPrototype) {
                    self[x] = selfPrototype[x];
                }
            }
        };
    };
}

次の実装は、__proto__プロパティをサポートするブラウザ用です。

function extend (Child, prototype, /*optional*/Parent) {
    if (!Parent) {
        Parent = Object;
    }
    Child.prototype = Object.create(Parent.prototype);
    Child.prototype.constructor = Child;
    for (var x in prototype) {
        if (prototype.hasOwnProperty(x)) {
            Child.prototype[x] = prototype[x];
        }
    }
    Child.prototype.$super = function (propName) {
        var prop = Parent.prototype[propName];
        if (typeof prop !== "function") {
            return prop;
        }
        var self = this;
        return function (/*arg1, arg2, ...*/) {
            var selfProto = self.__proto__;
            self.__proto__ = Parent.prototype;
            try {
                return prop.apply(self, arguments);
            }
            finally {
                self.__proto__ = selfProto;
            }
        };
    };
}

例:

function A () {}
extend(A, {
    foo: function () {
        return "A1";
    }
});

function B () {}
extend(B, {
    foo: function () {
        return this.$super("foo")() + "_B1";
    }
}, A);

function C () {}
extend(C, {
    foo: function () {
        return this.$super("foo")() + "_C1";
    }
}, B);


var c = new C();
var res1 = c.foo();
B.prototype.foo = function () {
    return this.$super("foo")() + "_B2";
};
var res2 = c.foo();

alert(res1 + "\n" + res2);
2
Thomas Eding

完全性の精神で(また、このスレッドをありがとうございました。これは優れた参照ポイントでした!)私はこの実装を投げたかったのです。

上記のすべての基準を満たす良い方法がないことを認めている場合、これはSalsifyチームによる勇敢な努力だと思います(私はちょうどそれを見つけました) ここにあります 。これは、再帰の問題を回避するだけでなく、事前コンパイルなしで.superを正しいプロトタイプへの参照にすることができる唯一の実装です。

したがって、基準1を破る代わりに、5を破ります。

この手法はFunction.callerの使用に依存します(es5に準拠していませんが、ブラウザーで広くサポートされており、es6は将来の必要性を排除します)が、他のすべての問題に対して非常に洗練されたソリューションを提供します(私は思います)。 .callerを使用すると、プロトタイプチェーン内のどこにいるかを特定できるメソッド参照を取得し、getterを使用して正しいプロトタイプを返します。それは完璧ではありませんが、私がこのスペースで見たものとは大きく異なる解決策です

var Base = function() {};

Base.extend = function(props) {
  var parent = this, Subclass = function(){ parent.apply(this, arguments) };

    Subclass.prototype = Object.create(parent.prototype);

    for(var k in props) {
        if( props.hasOwnProperty(k) ){
            Subclass.prototype[k] = props[k]
            if(typeof props[k] === 'function')
                Subclass.prototype[k]._name = k
        }
    }

    for(var k in parent) 
        if( parent.hasOwnProperty(k)) Subclass[k] = parent[k]        

    Subclass.prototype.constructor = Subclass
    return Subclass;
};

Object.defineProperty(Base.prototype, "super", {
  get: function get() {
    var impl = get.caller,
        name = impl._name,
        foundImpl = this[name] === impl,
        proto = this;

    while (proto = Object.getPrototypeOf(proto)) {
      if (!proto[name]) break;
      else if (proto[name] === impl) foundImpl = true;
      else if (foundImpl)            return proto;
    }

    if (!foundImpl) throw "`super` may not be called outside a method implementation";
  }
});

var Parent = Base.extend({
  greet: function(x) {
    return x + " 2";
  }
})

var Child = Parent.extend({
  greet: function(x) {
    return this.super.greet.call(this, x + " 1" );
  }
});

var c = new Child
c.greet('start ') // => 'start 1 2'

これを調整して(元の投稿のように)正しいメソッドを返すことも、(ゲッターを使用する代わりに)スーパー関数に名前を渡すことで、各メソッドに名前で注釈を付ける必要をなくすこともできます。

これがテクニックを示す実用的なフィドルです: jsfiddle

1
monastic-panic

JsFiddle

これの何が問題になっていますか?

'use strict';

function Class() {}
Class.extend = function (constructor, definition) {
    var key, hasOwn = {}.hasOwnProperty, proto = this.prototype, temp, Extended;

    if (typeof constructor !== 'function') {
        temp = constructor;
        constructor = definition || function () {};
        definition = temp;
    }
    definition = definition || {};

    Extended = constructor;
    Extended.prototype = new this();

    for (key in definition) {
        if (hasOwn.call(definition, key)) {
            Extended.prototype[key] = definition[key];
        }
    }

    Extended.prototype.constructor = Extended;

    for (key in this) {
        if (hasOwn.call(this, key)) {
            Extended[key] = this[key];
        }
    }

    Extended.$super = proto;
    return Extended;
};

使用法:

var A = Class.extend(function A () {}, {
    foo: function (n) { return n;}
});
var B = A.extend(function B () {}, {
    foo: function (n) {
        if (n > 100) return -1;
        return B.$super.foo.call(this, n+1);
    }
});
var C = B.extend(function C () {}, {
    foo: function (n) {
        return C.$super.foo.call(this, n+2);
    }
});

var c = new C();
document.write(c.foo(0) + '<br>'); //3
A.prototype.foo = function(n) { return -n; };
document.write(c.foo(0)); //-3

パブリックメソッドの代わりに特権メソッドを使用した使用例。

var A2 = Class.extend(function A2 () {
    this.foo = function (n) {
        return n;
    };
});
var B2 = A2.extend(function B2 () {
    B2.$super.constructor();
    this.foo = function (n) {
        if (n > 100) return -1;
        return B2.$super.foo.call(this, n+1);
    };
});
var C2 = B2.extend(function C2 () {
    C2.$super.constructor();
    this.foo = function (n) {
        return C2.$super.foo.call(this, n+2);
    };
});

//you must remember to constructor chain
//if you don't then C2.$super.foo === A2.prototype.foo

var c = new C2();
document.write(c.foo(0) + '<br>'); //3
1
Bill Barry

実行コンテキストを変更することで、疑似キーワードSuperを使用できるようにする方法を考え出しました(ここではまだ説明していません)。私がまったく満足していないことがわかった欠点は、 「スーパー」変数をメソッドの実行コンテキストに追加することはできませんが、代わりに実行コンテキスト全体を置き換えます。これは、メソッドで定義されたプライベートメソッドが使用できなくなることを意味します。

このメソッドは、提示された「eval hack」OPと非常に似ていますが、関数のソース文字列に対して処理を行わず、現在の実行コンテキストでevalを使用して関数を再宣言するだけです。どちらの方法にも前述の同じ欠点があるため、少し良くなります。

非常に簡単な方法:

function extend(child, parent){

    var superify = function(/* Super */){
        // Make MakeClass scope unavailable.
        var child = undefined,
            parent = undefined,
            superify = null,
            parentSuper = undefined,
            oldProto = undefined,
            keys = undefined,
            i = undefined,
            len = undefined;

        // Make Super available to returned func.
        var Super = arguments[0];
        return function(/* func */){
            /* This redefines the function with the current execution context.
             * Meaning that when the returned function is called it will have all of the current scopes variables available to it, which right here is just "Super"
             * This has the unfortunate side effect of ripping the old execution context away from the method meaning that no private methods that may have been defined in the original scope are available to it.
             */
            return eval("("+ arguments[0] +")");
        };
    };

    var parentSuper = superify(parent.prototype);

    var oldProto = child.prototype;
    var keys = Object.getOwnPropertyNames(oldProto);
    child.prototype = Object.create(parent.prototype);
    Object.defineProperty(child.prototype, "constructor", {enumerable: false, value: child});

    for(var i = 0, len = keys.length; i<len; i++)
        if("function" === typeof oldProto[keys[i]])
            child.prototype[keys[i]] = parentSuper(oldProto[keys[i]]);
}

クラス作りの例

function P(){}
P.prototype.logSomething = function(){console.log("Bro.");};

function C(){}
C.prototype.logSomething = function(){console.log("Cool story"); Super.logSomething.call(this);}

extend(C, P);

var test = new C();
test.logSomething(); // "Cool story" "Bro."

前述の欠点の例。

(function(){
    function privateMethod(){console.log("In a private method");}

    function P(){};

    window.C = function C(){};
    C.prototype.privilagedMethod = function(){
        // This throws an error because when we call extend on this class this function gets redefined in a new scope where privateMethod is not available.
        privateMethod();
    }

    extend(C, P);
})()

var test = new C();
test.privilagedMethod(); // throws error

また、このメソッドは子コンストラクターを「スーパーライズ」していないことに注意してください。つまり、Superは使用できません。実用的なライブラリを作成するのではなく、概念を説明したかっただけです:)

また、OPのすべての条件を満たしていることに気づきました。 (実際には実行コンテキストに関する条件があるはずですが)

0
BAM5

これが私のバージョンです: lowclass

そして、これがtest.jsファイルのsuperスパゲッティスープの例です(編集:実行例になりました):

var SomeClass = Class((public, protected, private) => ({

    // default access is public, like C++ structs
    publicMethod() {
        console.log('base class publicMethod')
        protected(this).protectedMethod()
    },

    checkPrivateProp() {
        console.assert( private(this).lorem === 'foo' )
    },

    protected: {
        protectedMethod() {
            console.log('base class protectedMethod:', private(this).lorem)
            private(this).lorem = 'foo'
        },
    },

    private: {
        lorem: 'blah',
    },
}))

var SubClass = SomeClass.subclass((public, protected, private, _super) => ({

    publicMethod() {
        _super(this).publicMethod()
        console.log('extended a public method')
        private(this).lorem = 'baaaaz'
        this.checkPrivateProp()
    },

    checkPrivateProp() {
        _super(this).checkPrivateProp()
        console.assert( private(this).lorem === 'baaaaz' )
    },

    protected: {

        protectedMethod() {
            _super(this).protectedMethod()
            console.log('extended a protected method')
        },

    },

    private: {
        lorem: 'bar',
    },
}))

var GrandChildClass = SubClass.subclass((public, protected, private, _super) => ({

    test() {
        private(this).begin()
    },

    reallyBegin() {
        protected(this).reallyReallyBegin()
    },

    protected: {
        reallyReallyBegin() {
            _super(public(this)).publicMethod()
        },
    },

    private: {
        begin() {
            public(this).reallyBegin()
        },
    },
}))

var o = new GrandChildClass
o.test()

console.assert( typeof o.test === 'function' )
console.assert( o.reallyReallyBegin === undefined )
console.assert( o.begin === undefined )
<script> var module = { exports: {} } </script>
<script src="https://unpkg.com/[email protected]/index.js"></script>
<script> var Class = module.exports // get the export </script>

無効なメンバーアクセスまたは_superの無効な使用を試みると、エラーがスローされます。

要件について:

  1. this。$ superは、プロトタイプへの参照である必要があります。つまり、実行時にスーパープロトタイプを変更すると、この変更が反映されます。これは基本的に、親が新しいプロパティを持っていることを意味します。これは、親へのハードコードされた参照が変更を反映するのと同じように、実行時にすべての子にスーパーを介して表示される必要があります

    いいえ、_superヘルパーはプロトタイプを返しません。保護されたプロトタイプとプライベートなプロトタイプの変更を避けるために、記述子がコピーされたオブジェクトのみを返します。さらに、記述子のコピー元のプロトタイプは、Class/subclass呼び出しのスコープ内に保持されます。これがあればいいのに。 FWIW、ネイティブclassesは同じように動作します。

  2. this。$ super.f.apply(this、arguments);再帰呼び出しで機能する必要があります。継承チェーンを上るときに複数のスーパーコールが行われる継承のチェーンセットの場合、再帰的な問題にぶつかってはなりません。

    はい、問題ありません。

  3. お子様のスーパーオブジェクトへの参照をハードコーディングしてはなりません。つまりBase.prototype.f.apply(this、arguments);ポイントを打ち負かします。

    うん

  4. X toJavaScriptコンパイラまたはJavaScriptプリプロセッサを使用しないでください。

    うん、すべてのランタイム

  5. ES5に準拠している必要があります

    はい、Babelベースのビルドステップが含まれています(つまり、lowclassはWeakMapを使用します。WeakMapはリークのないES5フォームにコンパイルされます)。これが要件4を無効にすることはないと思います。これにより、ES6 +を記述できるようになりますが、ES5でも機能するはずです。確かに、私はES5でこれをあまりテストしていませんが、試してみたい場合は、私の側でビルドの問題を確実に解決できます。あなたの側からは、ビルド手順なしでそれを消費できるはずです。 。

満たされていない唯一の要件は1です。それは素晴らしいでしょう。しかし、プロトタイプを交換するのは悪い習慣かもしれません。しかし実際には、メタのものを実現するためにプロトタイプを交換したいという用途があります。 'この実装ではもちろん、ネイティブのsuper(静的:()でこの機能を使用できると便利です。

要件2を再確認するために、基本的な再帰テストをtest.jsに追加しました。これは機能します(編集:実行例になりました)。

const A = Class((public, protected, private) => ({
    foo: function (n) { return n }
}))

const B = A.subclass((public, protected, private, _super) => ({
    foo: function (n) {
        if (n > 100) return -1;
        return _super(this).foo(n+1);
    }
}))

const C = B.subclass((public, protected, private, _super) => ({
    foo: function (n) {
        return _super(this).foo(n+2);
    }
}))

var c = new C();
console.log( c.foo(0) === 3 )
<script> var module = { exports: {} } </script>
<script src="https://unpkg.com/[email protected]/index.js"></script>
<script> var Class = module.exports // get the export </script>

(これらの小さなクラスのクラスヘッダーは少し長いです。すべてのヘルパーが事前に必要ではない場合でも、それを減らすことができるようにするためのアイデアがいくつかあります)

0
trusktr

OPが提示する再帰の問題を理解していない人のために、次の例を示します。

function A () {}
A.prototype.foo = function (n) {
    return n;
};

function B () {}
B.prototype = new A();
B.prototype.constructor = B;
B.prototype.$super = A.prototype;
B.prototype.foo = function (n) {
    if (n > 100) return -1;
    return this.$super.foo.call(this, n+1);
};

function C () {}
C.prototype = new B();
C.prototype.constructor = C;
C.prototype.$super = B.prototype;
C.prototype.foo = function (n) {
    return this.$super.foo.call(this, n+2);
};


alert(new C().foo(0)); // alerts -1, not 3

理由:Javascriptのthisは動的にバインドされます。

0
Thomas Eding

Classy ライブラリをご覧ください。 this.$superを使用して、クラスと継承、およびオーバーライドされたメソッドへのアクセスを提供します。

0
ThiefMaster