【问题标题】:Demonstrating Javascript Inheritance Benefits in Relation to a Class-Based Model展示与基于类的模型相关的 Javascript 继承优势
【发布时间】:2011-12-04 03:56:45
【问题描述】:

谁能提供一个具体的例子来展示 Javascript 的原型继承,以证明使用传统的基于类(经典)模型的好处?

我看到的其他问题(例如Classical Vs prototypal inheritanceWhy was JavaScript implemented using prototypal inheritance?prototype based vs. class based inheritance)仅提供高级别的讨论,而不是具体示例(最好是您在生产代码中使用过的示例)。

【问题讨论】:

  • 为了确定“更好”,您需要提供一些评估标准。否则,您将无休止地争论什么是“最好的”。
  • 我希望简明扼要地阐明 OP 的目标。
  • @JaredFarrish:感谢编辑
  • 看起来没有人认为原型更好。

标签: javascript inheritance


【解决方案1】:

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

  1. 原型继承可用于模拟经典继承。它是经典继承的超集。反之亦然是不可能的。这是因为在经典继承中,类只能从其他类继承。但是,在原型继承中,任何对象都可以从任何其他对象继承(在 JavaScript 中,一切都是对象)。
  2. 在 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;

【讨论】:

  • 很有趣,但还是差不多的
  • 我喜欢你深入的回答。只是几个 cmets:您的 JS 示例不调用任何超级构造函数,因此 WorkerBee 不会有名称或部门;此外,内部 proto__proto__ 属性是非标准的,因此最好仅将它们用于调试目的,但这不会改变您想说的内容。 :)
  • 另外,“原型”......也许你的意思是“原型”。
  • 所有对象都具有 [[proto]] 属性,无论实现如何。非标准部分是能够在对象创建后更改属性。
【解决方案2】:

在我看来原型更加灵活。

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

  2. 我可以通过将我的 Bar.prototype 设置为 null 或其他对象值来决定不再希望 Bar 从 Foo 继承。

  3. 我可以随时决定我希望 Bar 继承自 Array 而不是 Foo。

不过,古典语言确实有其优势。喜欢更好的封装。使用原型,你必须做很多闭包魔法才能让你的对象的属性“充当”私有属性。

【讨论】:

  • 您希望什么时候执行这些选项?
【解决方案3】:

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

在这个用于生产页面的示例中,jQuery 在 ltIE8 浏览器中根本不会设置动画。 由于仅在一个特定页面中发生这种情况,因此破解 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-heavy 页面上,开销是不值得的。仅多重继承和超级方法 为最后一代工作。

【讨论】:

  • 构造传统继承的方式很好,但要修改它的工作方式。第一个示例实际上可以用 Pytho 等语言完成:ideone.com/ipPIU
【解决方案4】:

我有几件事要说是明显的优势。与基于类的强类型语言相比,它们都代表着重大的安全漏洞,但它们为熟练的用户提供了强大的功能。

子类化实例化对象

即使不声明类,您也可以为任何已实例化的对象创建临时“子类”:

// 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);

【讨论】:

  • 只有第一个例子真正是关于原型继承的。所有其他示例也可以在具有经典 OO 的动态语言(如 Python)上运行。
  • 我认为这使 Python 成为具有原型功能的经典 OO 语言。
  • 没有。猴子补丁只是使 Python 动态(在某种意义上说,任何东西都可以在运行时更改)。从经典与原型的角度来看,Python 仍然是坚定的经典。
  • 猴子补丁并不是 Python 动态的原因。有很多经典的动态语言不允许这样做。事实是,在运行时改变经典结构的能力是原型语言的固有特征,与经典编程直接相反。经典编程在某种意义上是“所有对象都属于类,而此类对象会这样做”。更改已建立的对象类别会融合各种范式。
  • 即使是 Smalltalk 也允许在运行时修改类。我认为说它具有原型风格是在推动它,因为最初的原型语言正是为了与它不同而创建的。
【解决方案5】:

我认为我们没有直接看到更多原型模式的最大原因是 Javascript 中的默认语法是伪经典的异常,而不是更受欢迎的 Object.create。如果你想真正看到原型图案闪亮,请搜索使用此功能的地方。以下示例来自 Dojo 工具包:


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

虽然你有不可变的对象,但整个想法是 100% 没问题的。


Dojo 定义了一个通用的store 接口(使用 get()、add() 等方法),例如,可用于从服务器抽象一个 REST API。我们想创建一个Cache 函数,它接收任何数据存储并返回一个新版本,该版本缓存对 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 对象需要实现存储接口。我们可以尝试手动添加所有方法:

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 中的状态模式。

【讨论】:

    猜你喜欢
    • 2010-10-23
    • 1970-01-01
    • 1970-01-01
    • 2010-10-27
    • 1970-01-01
    • 1970-01-01
    • 2011-10-20
    • 2021-01-27
    相关资源
    最近更新 更多