在javascript中模拟super

Emulate super in javascript

本文关键字:super 模拟 javascript      更新时间:2023-09-26

基本上是否存在一种很好的优雅机制来模拟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到JavaScript编译器或JavaScript预处理器。
  5. 必须符合ES5标准

朴素的实现是这样的:

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

但是这违反了条件2。

迄今为止我见过的最优雅的机制是IvoWetzel的eval hack,它几乎是一个JavaScript预处理器,因此不符合标准4。

我不认为有"免费"的方法可以解决你提到的"递归超级"问题。

我们不能乱搞this,因为这样做要么会迫使我们以一种非标准的方式改变原型,要么将我们移动到原型链的上游,失去实例变量。因此,"当前类"answers"超类"必须知道,当我们做超,而不是传递这个责任给this或它的一个属性。

我们可以尝试做很多事情,但我能想到的都有一些不良后果:

  • 在创建时向函数添加超级信息,使用参数访问它。calee或类似的邪恶。
  • 在调用超方法

    时添加额外的信息
    $super(CurrentClass).method.call(this, 1,2,3)
    

    这迫使我们复制当前的类名(这样我们就可以在某个超类字典中查找它的超类),但至少没有复制超类名那么糟糕,(因为对继承关系的耦合比与类自身名称的内部耦合更糟糕)

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

    虽然这远非理想,但至少有一些历史先例。x Python。(他们为3.0"修复"了super,所以它不再需要参数了,但我不确定这涉及了多少魔法,以及它在JS中的可移植性如何)


编辑:Working 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;
        }
    }
}

John Resig发布了一个具有简单但出色的super支持的内在机制。唯一的区别是super指向调用它的基方法。

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

请注意,对于下面的实现,当您在通过$super调用的方法内部时,在父类中工作时访问属性永远不会解析到子类的方法或变量,除非您访问直接存储在对象本身上的成员(而不是附加到原型上)。这避免了大量的混淆(读作细微的bug)。

更新:这是一个没有__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);

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

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

本着完整的精神(也感谢大家的这篇文章,它是一个很好的参考点!)我想加入这个实现。

如果我们承认没有好的方法可以满足上述所有标准,那么我认为这是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'

您还可以调整它以返回正确的方法(如在原始文章中),或者您可以通过将名称传递给超函数(而不是使用getter)来消除用名称注释每个方法的需要

这里是一个演示该技术的工作小提琴:jsfiddle

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

为那些不理解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是动态绑定的。

我想出了一种方法,允许您通过更改执行上下文来使用伪关键字Super(我还没有看到在这里展示的方法)。我发现我不满意的缺点是,它不能将"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的所有条件!

我的版本:

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

尝试无效的成员访问或无效的_super使用将抛出错误。

关于要求:

  1. 。$super必须是对原型的引用。也就是说,如果我在运行时改变了超级原型,这个改变就会被反映出来。也就是说,如果父元素有了一个新属性那么这个属性应该在运行时通过super显示在所有子元素上就像对父元素的硬编码引用会反映变化

    不,_super助手不返回原型,只返回一个带有复制描述符的对象,以避免修改受保护的和私有的原型。此外,复制描述符的原型保存在Class/subclass调用的作用域中。有这个就好了。一般来说,本地class的行为是一样的。

  2. super.f美元。应用(这个参数);必须适用于递归调用。对于任何链式继承集,当你沿着继承链往上走的时候会有多个超调用,你一定不能遇到递归问题。

    好的,没问题。

  3. 你不能在你的子对象中硬编码超对象的引用。即Base.prototype.f。应用(这个参数);

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

    是的,所有的运行时间

  5. 必须符合ES5标准

    是的,它包含一个基于babel的构建步骤(例如,lowclass使用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/lowclass@3.1.0/index.js"></script>
<script> var Class = module.exports // get the export </script>

(类头对于这些小类来说有点长。我有几个想法,使它有可能减少,如果不是所有的助手都需要提前)

我想我有一个更简单的方法....

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