在javascript中模拟超级

时间:2011-11-07 03:56:04

标签: javascript oop super

基本上有一个很好的优雅机制来模拟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
  6. 天真的实现就是这样的。

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

    但是breaks condition 2

    我迄今见过的最优雅的机制是IvoWetzel's eval hack,它几​​乎是一个JavaScript预处理器,因此无法通过标准4。

11 个答案:

答案 0 :(得分:10)

我不认为你提到的“递归超级”问题有“免费”的方法。

我们不能搞砸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至少有一些历史先例。 (他们“固定”超级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;
        }
    }
}

答案 1 :(得分:5)

John Resig发布了一种简单但很好super支持的不一致机制。 唯一的区别是super指向您调用它的基本方法。

看看http://ejohn.org/blog/simple-javascript-inheritance/

答案 2 :(得分: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);

答案 3 :(得分:2)

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

答案 4 :(得分:1)

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

答案 5 :(得分:1)

本着完整的精神(也感谢大家对这个帖子的一个很好的参考点!)我想在这个实现中投入。

如果我们承认没有很好的方法来满足上述所有标准,那么我认为这是Salsify团队的勇敢努力(我刚刚找到它)found here。这是我见过的唯一可以避免递归问题的实现,但也允许.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'

您也可以调整此方法以返回正确的方法(如原始帖子中所示),或者您可以删除使用名称注释每个方法的需要,方法是将名称传递给超级函数(而不是使用getter)

这是一个演示技巧的工作小提琴:jsfiddle

答案 6 :(得分:0)

对于那些不了解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是动态绑定的。

答案 7 :(得分:0)

查看Classy库;它使用this.$super

提供类和继承以及对重写方法的访问

答案 8 :(得分:0)

我提出了一种方法,允许您通过更改执行上下文来使用伪关键字Super(我还没有看到这里的方式。)我发现我没有的缺点很高兴的是它无法添加&#34;超级&#34;变量到方法的执行上下文,但是将其替换为整个执行上下文,这意味着用该方法定义的任何私有方法都变得不可用......

这种方法非常类似于&#34; eval hack&#34;然而,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

另请注意,此方法并非超越&#34;子构造函数意味着Super不可用。我只想解释这个概念,而不是制作一个工作库:)

另外,我意识到我遇到了OP的所有条件! (虽然确实应该有关于执行上下文的条件)

答案 9 :(得分:0)

这是我的版本:lowclass

这是test.js文件中的super spaghetti汤示例(编辑:制作成运行示例):

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. 这个。$ super必须是对原型的引用。即如果我在运行时更改超级原型,则会反映此更改。这基本上意味着父母有一个新的属性然后这应该在运行时通过超级显示所有孩子,就像对父母的硬编码引用会反映更改

    不,_super助手不返回原型,只返回带有复制描述符的对象,以避免修改受保护的原型和私有原型。此外,从中复制描述符的原型保存在Class / subclass调用的范围内。有这个是很好的。 FWIW,原生class表现相同。

  2. 这个。$ super.f.apply(this,arguments);必须适用于递归调用。对于任何链接的继承集,当你继承链上进行多次超级调用时,你不能遇到递归问题。

    是的,没问题。

  3. 您不得硬编码对孩子中超级对象的引用。即Base.prototype.f.apply(this,arguments);打败了这一点。

    是的

  4. 您不得使用X to JavaScript编译器或JavaScript预处理器。

    是的,所有运行时

  5. 必须符合ES5

    是的,它包括一个基于Babel的构建步骤(例如,lowclass使用WeakMap,它被编译为非泄漏的ES5格式)。我不认为这会破坏要求4,它只允许我编写ES6 +但它仍然应该在ES5中工作。不可否认,我没有在ES5中对此进行过多次测试,但是如果你想尝试一下,我们绝对可以解决我的任何构建问题,从最终你应该能够使用它而不需要任何构建步骤

  6. 唯一不符合的要求是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/lowclass@3.1.0/index.js"></script>
    <script> var Class = module.exports // get the export </script>

    (对于这些小班,班级标题有点长。我有几个想法可以减少,如果不是所有的帮助者都需要在前面)

答案 10 :(得分:-1)

我想我的方式更简单......

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