Подтвердить что ты не робот

Эмуляция супер в javascript

В принципе есть хороший элегантный механизм для эмуляции super с синтаксисом, который прост как один из следующих

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

Критерии для подтверждения:

  • this.$super должен быть ссылкой на прототип. т.е. если я изменю суперпроиотип во время выполнения, это изменение будет отражено. Это в основном означает, что у родителя есть новое свойство, тогда это должно отображаться во время выполнения для всех детей с помощью super точно так же, как жестко закодированная ссылка на родителя будет отражать изменения
  • this.$super.f.apply(this, arguments); должен работать для рекурсивных вызовов. Для любого набора цепочек наследования, когда совершаются множественные супервызывы при продвижении цепи наследования, вы не должны ударять по рекурсивной проблеме.
  • Вы не должны указывать ссылки на супер объекты в своих дочерних элементах. То есть Base.prototype.f.apply(this, arguments); побеждает точку.
  • Вы не должны использовать компилятор X-JavaScript или препроцессор JavaScript.
  • Должен быть совместим с ES5

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

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

Но это прерывает условие 2.

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

4b9b3361

Ответ 1

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

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

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

  • Добавить супер информацию к функциям во время создания, получить доступ к ней с помощью arguments.calee или подобного зла.
  • Добавьте дополнительную информацию при вызове метода super

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

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

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

    Хотя это далеко не идеальный, есть хотя бы какой-то исторический прецедент из 2.x Python. (Они "исправлены" супер для 3.0, поэтому больше не требуются аргументы, но я не уверен, сколько магии было задействовано и как переносимо было бы JS)


Изменить: Работа fiddle

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;
        }
    }
}

Ответ 2

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

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

Ответ 3

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

Ответ 4

Основная трудность с 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

Ответ 5

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

Ответ 6

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

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

Ответ 7

Для тех, кто не понимает проблему рекурсии, представленную 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

Причина: this в Javascript динамически связана.

Ответ 8

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

Ответ 9

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

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

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

Ответ 10

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

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