【问题标题】:Javascript when to use prototypesJavascript 何时使用原型
【发布时间】:2011-06-11 20:04:09
【问题描述】:

我想了解什么时候在 js 中使用原型方法比较合适。是否应该始终使用它们?或者是否存在不首选使用它们和/或导致性能损失的情况?

在本站搜索 js 中命名空间的常用方法时,似乎大多数使用非基于原型的实现:简单地使用对象或函数对象来封装命名空间。

来自基于类的语言,很难不尝试并得出相似之处,并认为原型就像“类”,而我提到的命名空间实现就像静态方法。

【问题讨论】:

    标签: javascript performance prototype function-prototypes


    【解决方案1】:

    原型是一种优化

    很好地使用它们的一个很好的例子是 jQuery 库。每次使用$('.someClass') 获取一个jQuery 对象时,该对象都有几十个“方法”。该库可以通过返回一个对象来实现:

    return {
       show: function() { ... },
       hide: function() { ... },
       css: function() { ... },
       animate: function() { ... },
       // etc...
    };
    

    但这意味着内存中的每个 jQuery 对象都会有几十个包含相同方法的命名槽,一遍又一遍。

    相反,这些方法是在原型上定义的,所有 jQuery 对象都“继承”该原型,以便以极少的运行时成本获得所有这些方法。

    jQuery 正确处理的一个非常重要的部分是它对程序员是隐藏的。它被视为纯粹的优化,而不是您在使用该库时必须担心的事情。

    JavaScript 的问题在于,裸构造函数要求调用者记住在它们前面加上 new,否则它们通常不起作用。这没有充分的理由。 jQuery 将这些废话隐藏在一个普通函数 $ 后面,因此您不必关心对象是如何实现的。

    为了方便您创建具有指定原型的对象,ECMAScript 5 包含一个标准函数Object.create。它的一个大大简化的版本如下所示:

    Object.create = function(prototype) {
        var Type = function () {};
        Type.prototype = prototype;
        return new Type();
    };
    

    它只是解决了编写构造函数然后用new调用它的痛苦。

    您何时会避免使用原型?

    一个有用的比较是与流行的 OO 语言,如 Java 和 C#。它们支持两种继承:

    • 接口继承,您可以在其中 implementinterface 以便类为接口的每个成员提供其自己的唯一实现。
    • 实现继承,其中extend class 提供某些方法的默认实现。

    在 JavaScript 中,原型继承是一种实现继承。因此,在那些情况下(在 C# 或 Java 中)您将从基类派生以获得默认行为,然后您通过覆盖对其进行小幅修改,然后在 JavaScript 中,原型继承是有意义的。

    但是,如果您在 C# 或 Java 中使用接口的情况,那么您不需要 JavaScript 中的任何特定语言功能。无需显式声明表示接口的内容,也无需将对象标记为“实现”该接口:

    var duck = {
        quack: function() { ... }
    };
    
    duck.quack(); // we're satisfied it's a duck!
    

    换句话说,如果对象的每个“类型”都有自己的“方法”定义,那么从原型继承就没有任何价值。之后,这取决于您为每种类型分配的实例数。但在许多模块化设计中,给定类型只有一个实例。

    事实上,it has been suggested by many people that implementation inheritance is evil。也就是说,如果一个类型有一些常见的操作,那么如果它们不被放入基类/超类中,而是在某个模块中作为普通函数公开,那么可能会更清楚,你将对象传递给这些模块您希望它们进行操作。

    【讨论】:

    • 很好的解释。那么你是否同意,既然你认为原型是一种优化,那么它们总是可以用来改进你的代码?我想知道是否在某些情况下使用原型没有意义,或者实际上会导致性能损失。
    • 在您的跟进中,您提到“这取决于您为每种类型分配的实例数量”。但是您提到的示例没有使用原型。分配实例的概念在哪里(你还会在这里使用“新”吗)?另外:假设 quack 方法有一个参数——duck.quack(param) 的每次调用会导致在内存中创建一个新对象(如果它有参数可能无关紧要)?
    • 1. 我的意思是,如果一种鸭子有大量实例,那么修改示例以使quack 函数是有意义的在原型中,许多鸭子实例都链接到该原型。 2. 对象字面量语法{ ... } 创建一个实例(无需使用new)。 3. 调用任何函数 JS 会导致在内存中创建至少一个对象 - 它被称为 arguments 对象并存储调用中传递的参数:developer.mozilla.org/en/JavaScript/Reference/…
    • 谢谢我接受了你的回答。但是我仍然对您的观点(1)有些困惑:我没有理解您所说的“一种鸭子的大量实例”是什么意思。就像你在 (3) 中所说的,每次调用 JS 函数时,都会在内存中创建一个对象 - 所以即使你只有一种鸭子,每次调用鸭子的函数时你不会分配内存(在在哪种情况下使用原型总是有意义的)?
    • +1 与 jQuery 的比较是我读过的关于何时以及为什么使用原型的第一个清晰简洁的解释。非常感谢。
    【解决方案2】:

    如果您希望声明对象的“非静态”方法,则应使用原型。

    var myObject = function () {
    
    };
    
    myObject.prototype.getA = function (){
      alert("A");
    };
    
    myObject.getB = function (){
      alert("B");
    };
    
    myObject.getB();  // This works fine
    
    myObject.getA();  // Error!
    
    var myPrototypeCopy = new myObject();
    myPrototypeCopy.getA();  // This works, too.
    

    【讨论】:

    • @keatsKelleher 但是我们可以通过使用this示例this.getA = function(){alert("A")}在构造函数中定义方法来为对象创建一个非静态方法,对吗?
    【解决方案3】:

    使用内置prototype 对象的一个​​原因是,如果您要多次复制一个共享通用功能的对象。通过将方法附加到原型,您可以节省为每个 new 实例创建的重复方法。但是,当您将方法附加到 prototype 时,所有实例都可以访问这些方法。

    假设您有一个基础Car() 类/对象。

    function Car() {
        // do some car stuff
    }
    

    然后您创建多个Car() 实例。

    var volvo = new Car(),
        saab = new Car();
    

    现在,您知道每辆车都需要行驶、启动等。您可以将方法附加到原型中,而不是将方法直接附加到 Car() 类(每个创建的实例都会占用内存)而是(只创建一次方法),因此可以同时访问新的 volvosaab 的这些方法。

    // just mapping for less typing
    Car.fn = Car.prototype;
    
    Car.fn.drive = function () {
        console.log("they see me rollin'");
    };
    Car.fn.honk = function () {
        console.log("HONK!!!");
    }
    
    volvo.honk();
    // => HONK!!!
    saab.drive();
    // => they see me rollin'
    

    【讨论】:

    • 其实这是不正确的。 volvo.honk() 将不起作用,因为您完全替换了原型对象,而不是扩展它。如果你要做这样的事情,它会像你期望的那样工作: Car.prototype.honk = function() { console.log('HONK');} volvo.honk(); //'喇叭'
    • @29er - 按照我写这个例子的方式,你是对的。顺序确实很重要。如果我要保持这个例子不变,Car.prototype = { ... } 必须在调用 new Car() 之前出现,如这个 jsfiddle 所示:jsfiddle.net/mxacA。至于您的论点,这将是正确的方法:jsfiddle.net/Embnp。有趣的是,我不记得回答这个问题了=)
    • @hellatan 你可以通过设置 constructor: Car 来解决这个问题,因为你用对象文字覆盖了原型属性。
    • @josh 感谢您指出这一点。我已经更新了我的答案,这样我就不会用对象文字覆盖原型,因为它应该从一开始就是。
    【解决方案4】:

    当您要创建特定类型对象的大量副本并且它们都需要共享共同行为时,将函数放在原型对象上。这样一来,每个函数只需一个副本即可节省一些内存,但这只是最简单的好处。

    更改原型对象的方法或添加方法会立即改变相应类型的所有实例的性质。

    现在正是为什么您要做所有这些事情主要是您自己的应用程序设计的功能,以及您需要在客户端代码中执行的各种事情。 (一个完全不同的故事是服务器内部的代码;更容易想象在那里编写更大规模的“OO”代码。)

    【讨论】:

    • 所以当我使用原型方法(通过 new 关键字)实例化一个新对象时,该对象不会获得每个函数的新副本(只是一种指针)?如果是这样,你为什么不想使用原型?
    • @opi 是的,你是对的 - 没有复制。相反,原型对象上的符号(属性名称)只是作为每个实例对象的虚拟部分自然地“存在”。人们不想为此烦恼的唯一原因是对象是短暂且独特的,或者没有太多“行为”可共享的情况。
    【解决方案5】:

    如果我用基于类的术语进行解释,那么 Person 是类,walk() 是 Prototype 方法。所以 walk() 只有在你用 this 实例化新对象后才会存在。

    因此,如果您想创建像 Person 这样的对象的副本,您可以创建多个用户 Prototype 是一个很好的解决方案,因为它通过为内存中的每个对象共享/继承相同的函数副本来节省内存。

    而静态在这种情况下并没有太大帮助。

    function Person(){
    this.name = "anonymous";
    }
    
    // its instance method and can access objects data data 
    Person.prototype.walk = function(){
    alert("person has started walking.");
    }
    // its like static method
    Person.ProcessPerson = function(Person p){
    alert("Persons name is = " + p.name);
    }
    
    var userOne = new Person();
    var userTwo = new Person();
    
    //Call instance methods
    userOne.walk();
    
    //Call static methods
    Person.ProcessPerson(userTwo);
    

    因此,它更像是实例方法。 对象的方法类似于静态方法。

    https://developer.mozilla.org/en/Introduction_to_Object-Oriented_JavaScript

    【讨论】:

      【解决方案6】:

      这里只包含一个视频链接供参考,当使用原型时:https://youtu.be/JCXZhe6KsxQ?t=2m30s

      这是来自 NGConf 的 Ben Lesh 的演讲,为什么 rxjs 删除了修补原型(可链接函数)以支持可管道函数。

      【讨论】:

      • 这很酷。感谢分享。因此,RxJS 6+ 将专注于函数式编程而不是 OOP。
      猜你喜欢
      • 2020-01-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-02-11
      • 1970-01-01
      • 2010-10-08
      • 2011-11-10
      相关资源
      最近更新 更多