演示Javascript继承与基于类的模型的关系

Demonstrating Javascript Inheritance Benefits in Relation to a Class-Based Model

本文关键字:模型 关系 于类 Javascript 继承 演示      更新时间:2023-09-26

谁能提供一个具体的例子来展示Javascript的原型继承,说明什么时候使用它比传统的基于类的(经典)模型更有益?

我看到的其他问题(如经典Vs原型继承,为什么JavaScript使用原型继承实现?(基于原型与基于类的继承)只给出一个高层次的讨论,而不是一个具体的例子(最好是你在产品代码中使用过的例子)。

原型继承优于经典继承的原因有很多:

    原型继承可以用来模拟经典继承。这是经典遗传的超集。反之是不可能的。这是因为在经典继承中,类只能继承其他类。然而,在原型继承中,任何对象都可以从任何其他对象继承(在JavaScript中,一切都是对象)。在JavaScript中,每个对象都有一个内部的proto属性指向它的原型对象(Object对象有一个原型null)。由于JavaScript是动态的,你可以对原型对象进行更改,这些更改将反映在每个内部proto属性指向该原型的对象上(即使在对象创建之后)。因此,它可以用来扩展一组对象的功能。

还有很多原因。当我能回忆起来的时候,我会继续更新的。

下面是一些展示经典继承的Java代码:

public class Employee {
    public String name;
    public String dept;
    public Employee () {
        this("", "general");
    }
    public Employee (String name) {
        this(name, "general");
    }
    public Employee (String name, String dept) {
        this.name = name;
        this.dept = dept;
    }
}
public class WorkerBee extends Employee {
    public String[] projects;
    public WorkerBee () {
        this(new String[0]);
    }
    public WorkerBee (String[] projs) {
        projects = projs;
    }
}
public class Engineer extends WorkerBee {
    public String machine;
    public Engineer () {
        dept = "engineering";
        machine = "";
    }
    public Engineer (String mach) {
        dept = "engineering";
        machine = mach;
    }
}
下面是等效的JavaScript代码:
function Employee (name, dept) {
    this.name = name || "";
    this.dept = dept || "general";
}
function WorkerBee (projs) {
    this.projects = projs || [];
}
WorkerBee.prototype = new Employee;
function Engineer (mach) {
    this.dept = "engineering";
    this.machine = mach || "";
}
Engineer.prototype = new WorkerBee;

我希望你能在下面的例子中看到的动态和灵活性是什么给了javascript相对于基于静态类的模型的优势。

在这个例子中,在生产页面上使用,jQuery在lte8浏览器中根本不会动画。因为它只发生在一个特定的页面中,所以没有意义jQuery核心,也加载在其他页面(更不用说它必须托管,而不是从谷歌加载)。相反,我做了ltIE8条件脚本块,修改了fx原型内联的cur方法,修复了它将返回NaN值的问题对于动画步骤:

<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>
<!--[if lt IE 8]>
<script type="text/javascript">
jQuery.fx.prototype.cur = function() {
var parsed, r;
    if ( this.elem[this.prop] != null && (!this.elem.style || this.elem.style[this.prop] == null) ) {
        return this.elem[ this.prop ];
    }
r = this.elem.style[this.prop] || jQuery.css( this.elem, this.prop );
return isNaN( parsed = parseFloat( r ) ) ? !r || r === "auto" ? 0 : r : parsed;
};
</script>
<![endif]-->

另一个例子是我为模拟经典继承而编写的库的实现。具体实现没有在生产页面上使用,但是我在生产页面上创建的所有"类"都是以这种方式创建的。的重要这样做的好处是,您可以添加和修改功能(例如本例中所示的内联混叠),因为它在语言中没有硬编码。

var Person = function Person( name, age ){ //Declare the constructor
this.name = name || "John";
this.age = age || "20";
this.instanceId = "person"+this.constructor._getId(); //Create unique instance id
this.constructor._addInstance( this ); //Make this instance accessible from Person
}
.Inherits( Animal, Monkey )
.Class({
     "static _instances": {},
     "static _curId": 0,
     "static _getId": function(){
     return this._curId++; //In static methods "this" refers to the Person constructor, not to an instance.
     },
     "static _addInstance": function( instance ) {
     this._instances[instance.instanceId] = instance;
     },
     "static alias byId": "getInstanceById", //Inline alias for Person.getInstanceById === Person.byId
     "static getInstanceById": function( id ){
     return ( id in this._instances ) && this._instances[id];
     },
     "alias greet": "sayHello", //alias for the instance method
     "alias sayHi": "sayHello",
     sayHello: function(){
     return "hello from "+this.name;
     },
     eat: function(){
     return this.__super__( "eat", "pizza" ); //Super method call, not really useful in this particular implementation
     }
})
.Implements( whatever ); //emulating interfaces, whatever should be an object that describes how the methods must be implemented

//Instantiating and such works like regular js
var mike = new Person( "mike" );
mike.greet(); //"hello from mike"
mike.sayHi(); //"hello from mike"
mike.sayHello(); //"hello from mike"
mike === Person.byId( "person0" ); //true

没有实现使以_underscore为前缀的方法实际上从外部无法访问,因为在js繁重的页面上,这样的开销是不值得的。只支持多重继承和超方法为上一代工作。

我有一些我想说的明显的优势。与强类型的基于类的语言相比,它们都代表着重大的安全弱点,但它们为熟练的用户提供了很多功能。

子类化实例化对象

你可以为任何已经实例化的对象创建一个特别的"子类",甚至不需要声明一个类:

// Magic code
function child(src) {
    function Child() {};
    Child.prototype = src;
    return new Child;
}
// Base object
var default_options = {
    color: 'red',
    size:  'large',
    font:  'arial'
};
// Child object
var my_options = child(default_options);
my_options.size = 'small';
my_options.font = 'verdana';
my_options.color == 'red';
default_options.font == 'arial';

在支持__proto__的浏览器上,这可以更容易:

var my_options = {
    size: 'small',
    font: 'verdana'
};
// When applying options:
my_options.__proto__ = default_options;
my_options.color == 'red';

这意味着你也可以传递简单的对象,然后通过将它们附加到完整的类原型来丰富它们:

my_options.__proto__ = OptionsProcessor.prototype;

扩展现有对象

当然,JavaScript继承如此伟大的真正原因是,你将要处理的环境已经相当好地建立了成千上万的对象,你可能想要增强。假设您想在旧浏览器上使用element.querySelectorAll。对于经典继承,你就没那么幸运了,但是对于JavaScript继承,这很简单:

(HTMLElement || Object).prototype.querySelectorAll = function(selector) { ... }

这种类型的polyfill比jQuery有很大的优势,因为你可以在整个应用程序中使用标准代码,只在需要时导入JavaScript。

<标题> 重新布线功能

假设我们想知道每次使用querySelectorAll的情况,以便用更快的函数替换它以进行更简单的查询。我们可以劫持该函数并在每次调用它时输出到控制台:

var oldFunction = HTMLElement.prototype.querySelectorAll;
HTMLElement.prototype.querySelectorAll = function(selector) {
    console.log(selector);
    oldFunction.prototype.apply(this, arguments);
};

将方法应用到其他类

JavaScript有很多类似数组的函数。arguments不是数组。document.getElementsByTagName('div')也不是。这意味着如果我们想要数组中的前5个元素,我们不能使用list.slice(0, 5)。但是,您可以将Array.prototype.slice应用于list对象:
var divs = document.getElementsByTagName('div');
var first5divs = Array.prototype.slice.call(divs, 0, 5);

我认为我们没有直接看到更多原型模式的最大原因是Javascript的默认语法是伪经典畸变,而不是更有利的Object.create。如果您想真正看到原型模式的光芒,请搜索使用此函数的地方。下面的示例来自Dojo工具包:


警告:自从我最初写这个答案以来,我有点改变了我对这种代码有多好的看法。虽然基本思想仍然存在,但如果您的方法会改变实例("this")属性,则应该小心。这是因为如果你通过委托对象调用一个方法,那么你最终可能会在委托中设置变量,而不是在委托中,如果其他人最终直接访问委托,这可能会破坏一些不变量。

如果你有不可变对象,整个想法是100%好的。


Dojo定义了一个通用的存储接口(带有get()、add()等方法),例如,可以使用它从服务器抽象REST API。我们希望创建一个缓存函数,它接收任何数据存储,并返回一个缓存对get()方法的任何调用的新版本(这允许我们将缓存与实现实际get()的存储特定行为解耦)

第一个想法是利用Javascript是高度动态的这一事实来代替get方法:

//not the actual implementation. Things get more complicated w/ async code.
var oldGet = store.get;
store.get = function(id){
    if(!cache[id]){ cache[id] = oldGet(id); }
    return cache[id];
}

然而,这会破坏原来的对象,所以你不能再访问原来的方法了,也使得并行添加其他修改变得更加棘手。

第二个想法是使用委托创建一个更健壮的解决方案:

function Cache(obj){
    this.obj = obj;
}
Cache.prototype = {
    get: function(){
        //do things involving this.obj...
    }
};

这看起来很有希望,直到您记住结果缓存对象需要实现存储接口。我们可以尝试手工添加所有的方法:

Cache.prototype = {
    //...
    put: function(){ return this.obj.apply(this, arguments); },
    //...
}

,但这不仅麻烦而且容易出错(它很容易忘记一些东西),而且甚至不如对象修改解决方案强大,因为我们失去了对原始对象中不是来自存储接口的方法的访问。

嗯,实现这种"自动委托"的方法是继承,但在这种情况下,它最初似乎是无用的,因为你需要为每个可能的存储类创建一个新的缓存子类,或者你需要某种花哨的多继承混合。输入原型继承来挽救局面。我们可以很容易地创建一个新对象,为旧对象添加功能,而不需要修改它,也不需要修改类的层次

dojo.store.Cache = function(masterStore, cachingStore, options){
    //...
    return dojo.delegate(masterStore, {
        //... 
        get: function(id, directives){
            //...
        }
        //...
    }
}

其中dojo.delegate是一个函数,它创建一个具有第二个参数中所有属性的新对象,其原型将是第一个参数。


非JS理论:原型继承可以在像Self这样的语言中更积极地用于更多委托场景,它允许多个原型,也允许在运行时直接访问和修改原型。例如,可以通过将所有合适的方法委托给原型并在状态更改时更改原型来实现GoF中的状态模式。

我认为原型要灵活得多。

  1. 我只能让我的类Bar的一个实例从Foo继承,而不是所有的Bar的实例。

  2. 我可以通过设置我的Bar来决定我不想让Bar从Foo继承。

  3. 我可以在任何时候决定我想要Bar继承Array而不是Foo。

然而,古典语言确实有其优势。比如更好的封装。有了原型,你必须做很多闭包魔法来让你的对象的属性"充当"私有属性。