Эмулировать супер в javascript - PullRequest
24 голосов
/ 07 ноября 2011

По сути, есть хороший элегантный механизм для эмуляции 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 to JavaScript или препроцессор JavaScript.
  5. Должен быть совместим с ES5

Наивная реализация будетбыть примерно таким.

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

Но это нарушает условие 2 .

Самый элегантный механизм, который я когда-либо видел, это IvoWetzel evalhack , который в значительной степени является препроцессором JavaScript и поэтому не соответствует критериям 4.

Ответы [ 11 ]

10 голосов
/ 07 ноября 2011

Я не думаю, что есть "свободный" выход из упомянутой вами "рекурсивной супер" проблемы.

Мы не можем связываться с this, потому что это либо заставит нас менять прототипы нестандартным способом, либо продвинет нас вверх по цепочке прототипов, потеряв переменные экземпляра. Следовательно, «текущий класс» и «супер класс» должны быть известны, когда мы выполняем суперинг, не передавая эту ответственность this или одному из его свойств.

Есть много вещей, которые мы могли бы попытаться сделать, но все, что я могу придумать, имеет некоторые нежелательные последствия:

  • Добавить суперинформацию к функциям во время создания, получить к ней доступ, используя 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 . (Они «исправили» super для 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;
        }
    }
}
5 голосов
/ 07 ноября 2011

Джон Резиг (John Resig) опубликовал механизм непостоянства с простой, но великолепной поддержкой super.Единственное отличие состоит в том, что super указывает на базовый метод, откуда вы его вызываете.

Взгляните на http://ejohn.org/blog/simple-javascript-inheritance/.

2 голосов
/ 15 ноября 2011

Основная сложность с 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 голосов
/ 08 ноября 2011

Обратите внимание, что для следующей реализации, когда вы находитесь внутри метода, который вызывается через $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);
1 голос
/ 07 сентября 2014

В духе полноты (также спасибо всем за эту ветку, это была отличная ссылка!) Я хотел бы добавить в эту реализацию.

Если мы признаем, что нет хорошего способа удовлетворить все вышеперечисленные критерии, то я думаю, что это отважная попытка команды 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 голос
/ 26 июня 2012

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
0 голосов
/ 21 марта 2018

Вот моя версия: низкий класс

А вот пример super супа спагетти из файла test.js (РЕДАКТИРОВАТЬ: превращен в работающий пример):

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/lowclass@3.1.0/index.js"></script>
<script> var Class = module.exports // get the export </script>

Попытка доступа к недопустимому члену или использования _super приведет к ошибке.

О требованиях:

  1. this. $ Super должно быть ссылкой на прототип. то есть, если я изменю супер-прототип во время выполнения, это изменение будет отражено. Это в основном означает, что у родителя есть новое свойство, тогда это должно быть показано во время выполнения для всех потомков через super, как жестко закодированная ссылка на родителя будет отражать изменения

    Нет, помощник _super не возвращает прототип, только объект с скопированными дескрипторами, чтобы избежать модификации защищенных и закрытых прототипов. Кроме того, прототип, из которого копируются дескрипторы, содержится в объеме вызова Class / subclass. Было бы здорово иметь это. FWIW, нативные class ведут себя так же.

  2. this. $ Super.f.apply (this, аргументы); должен работать для рекурсивных вызовов. Для любого связанного набора наследования, когда по мере продвижения по цепочке наследования выполняется несколько супер-вызовов, вы не должны сталкиваться с рекурсивной проблемой.

    Да, нет проблем.

  3. Вы не должны жестко кодировать ссылки на суперобъекты у ваших детей. То есть Base.prototype.f.apply (this, аргументы); побеждает точку.

    1039 * да *

  4. Вы не должны использовать компилятор X to JavaScript или препроцессор JavaScript.

    Да, все время выполнения

  5. Должен соответствовать ES5

    Да, он включает в себя этап сборки на основе Babel (например, lowclass использует WeakMap, который скомпилирован в форму ES5 без утечек). Я не думаю, что это побеждает требование 4, оно просто позволяет мне писать ES6 +, но все равно должно работать в ES5. По общему признанию, я не провел много испытаний этого в ES5, но если вы хотите попробовать это, мы определенно можем устранить любые проблемы со сборкой на моем конце, и с вашей стороны вы сможете использовать его без каких-либо шагов сборки .

Единственное не выполненное требование - 1. Было бы неплохо. Но, может быть, это плохая практика - менять прототипы. Но на самом деле у меня есть способы использования, где я хотел бы поменять прототипы для достижения мета-материала. «Было бы неплохо иметь эту функцию с собственным super (который является статическим :(), не говоря уже о реализации.

Чтобы перепроверить требование 2, я добавил базовый рекурсивный тест в мой test.js, который работает (EDIT: сделано в работающем примере):

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/lowclass@3.1.0/index.js"></script>
<script> var Class = module.exports // get the export </script>

(заголовок класса немного длинен для этих маленьких классов. У меня есть пара идей, чтобы можно было уменьшить это, если не все помощники нужны заранее)

0 голосов
/ 03 декабря 2015

Я придумал способ, который позволит вам использовать псевдоключевое слово Super, изменив контекст выполнения (способ, который мне еще предстоит увидеть, представлен здесь). Недостаток, который я обнаружил, что меня не устраиваетэто вовсе не означает, что он не может добавить переменную «Super» к контексту выполнения метода, а вместо этого заменяет его на весь контекст выполнения, это означает, что любые частные методы, определенные с помощью метода, становятся недоступными ...

Этот методочень похож на представленный OP "eval hack", однако он не выполняет никакой обработки исходной строки функции, просто повторно объявляет функцию с использованием 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 не доступен для него.Я просто хотел объяснить концепцию, а не сделать рабочую библиотеку:)

Кроме того, я просто понял, что я выполнил все условия ОП!(Хотя действительно должно быть условие о контексте выполнения)

0 голосов
/ 17 июня 2013

Я думаю, у меня есть более простой способ ....

function Father(){
  this.word = "I'm the Father";

  this.say = function(){
     return this.word; // I'm the Father;
  }
}

function Sun(){
  Father.call(this); // Extend the Father

  this.word = "I'm the sun"; // Override I'm the Father;

  this.say = function(){ // Override I'm the Father;
    this.word = "I was changed"; // Change the word;
    return new Father().say.apply(this); // Call the super.say()
  }
}

var a = new Father();
var b = new Sun();

a.say() // I'm the father
b.ay() // I'm the sun
b.say() // I was changed
0 голосов
/ 07 ноября 2011

Посмотрите на библиотеку Classy ; он предоставляет классы, наследование и доступ к переопределенному методу, используя this.$super

...