【问题标题】:Prototype and constructor in JavaScript (plain English)?JavaScript 中的原型和构造函数(简单英语)?
【发布时间】:2013-08-20 08:13:46
【问题描述】:

“JavaScript 是世界上最容易被误解的语言”-D.Crockford

我的问题:

  1. 简单英语的构造函数和原型?
  2. 使用原型有什么需要?使用的目的是什么 原型和构造函数?我的意思是他们提供更多 灵活性。我问这个是因为我一直在使用这种语言 在过去的六个月里,我从来没有使用过原型和 构造函数。

我不是在寻找任何语法以及如何进行事物解释,因为我确实了解它们的某些部分,只是想以更简单的方式了解这些事物。一个类比(非技术)或例子会很棒。*

详细说明我问这个问题的原因(如果你想请忽略):

过去六个月我一直在使用 JavaScript,当我知道 JavaScript 是一种基于原型的语言时,我感到很震惊。

我遇到了一些关于应该如何使用 JavaScript 的 StackOverflow 问题,并遇到了原型和构造函数。

我学会了,现在我可以说在构造函数和原型方面我不是菜鸟。我熟悉语法。但是我仍然认为我遗漏了一些东西并且没有深入到这种语言的核心,而且我有时会感到困惑。

我希望我清楚。

【问题讨论】:

  • 也许这有点帮助:stackoverflow.com/a/17393153/218196
  • 我不敢相信你从未使用过它们。你可能没有注意到,但你肯定注意到了。
  • @Bergi:我只是用来声明我的函数并在某些事件上调用它,我想我的项目太基础了。我尝试了一个带有画布的项目并尝试使用 const 和 proto ......但是作为我说,在实际不知道它的全部潜力的情况下使用它。
  • @Jack:我会奖励它..这是你和 t.j crowder 之间的艰难选择...

标签: javascript constructor prototype prototypal-inheritance prototype-programming


【解决方案1】:

简单英语的构造函数和原型?

构造函数创建对象并为其分配原型。原型是对象可以通过原型链继承的具有各种属性的对象。与往常一样,示例有所帮助:

function Foo() {
}
Foo.prototype.answer = 42;

var f = new Foo();
console.log(f.answer); // "42"

Foo 是一个构造函数。当您使用new Foo 时,Foo.prototype 指向的对象将成为所创建对象的原型。当您执行f.answer 时,由于f 没有其名为answer自己的 属性,JavaScript 引擎会查看f 的原型以查看是否 有一个。既然这样做了,它就使用原型中的值,我们在控制台中看到“42”。这是解析属性的方式:通过查看一个对象,看看它是否具有给定名称的属性,如果没有,则转到其原型以查看 it 是否具有该属性,如果没有到它的原型,等等。

请注意,上面的结果是在使用该原型创建对象之后向原型添加属性可以正常工作;您可以通过对象使用这些新属性:

function Foo() {
}
Foo.prototype.answer = 42;

var f = new Foo();
console.log(f.question); // "undefined", neither `f`, nor `Foo.prototype`, nor
                         // `Object.prototype` has a `question` property

Foo.prototype.question = "Life, the Universe, and Everything";
console.log(f.question); // "Life, the Universe, and Everything"

从 ES5 开始,构造函数不再是您将原型分配给对象的唯一方法。现在您也可以通过Object.create 进行操作。以上大致相当于:

var fooProto = {
    answer: 42
};
var f = Object.create(fooProto);
console.log(f.answer); // "42"

使用原型和构造函数的目的是什么?

在对象之间共享特征。原型的属性可以是函数或数据,使用该原型的对象都可以访问和重用。

在下方回复您的评论:

我了解关于共享特征的部分,但我可以更详细地了解它

好吧,考虑一个Circle 构造函数:

function Circle(radius) {
    this.r = radius;
}
Circle.prototype.radius = function() {
    return this.r;
};
Circle.prototype.diameter = function() {
    return this.r * 2;
};
Circle.prototype.circumference = function() {
    return 2 * Math.PI * this.r;
};
Circle.prototype.area = function() {
    return Math.PI * this.r * this.r;
};

Circle 构造的所有对象都会得到Circle.prototype 作为它们的原型,所以它们都有方便的diametercircumference 等。人。功能。

var c1 = new Circle(3);
console.log(c1.area());          // 28.274333882308138
console.log(c1.circumference()); // 18.84955592153876

var c2 = new Circle(5);
console.log(c2.area());          // 78.53981633974483
console.log(c2.circumference()); // 31.41592653589793

它们以节省内存的方式共享这些属性:每个实例没有这些属性的自己的副本(这意味着在每个对象中保留每个属性名称及其值);相反,他们只是引用了他们共享的具有这些属性的原型。

【讨论】:

  • 我理解了关于共享特性的部分,但我能否更详细地了解它。我从这个问题中受益匪浅..
  • @VAGABOND:我已经添加了一些内容。
  • @T.J.Crowder: 非常感谢..还有一件事,我想知道你对这个stackoverflow.com/questions/18433059/…的看法
  • @VAGABOND:上面的答案回答了你的问题吗?
  • @TJCrowder:它做得很好...现在我正在学习设置原型的各种方法..ericleads.com/2013/02/… ..如果我遇到麻烦会告诉你将来……除非你不介意。
【解决方案2】:

首先,我建议你看一下this playlist 的主角本人(Crockford)。它可能很老,但它确实很好地解释了 JavaScript“逻辑”,您的问题在第三个视频中得到了特别解答。

我将通过描述在其他传统的面向对象编程语言中如何描述对象来开始回答这个问题,因为我还想针对您在问题开头发布的 Crockford 评论。

为了理解构造函数,你首先必须对对象有一个很好的理解。在传统的 OOP 语言中,对象是描述对象状态的变量(称为属性或字段)以及描述其行为的函数(称为方法)的集合。在那些(非 JavaScript)语言中,这些对象的“蓝图”称为类。

所以,如果我在 Java 中创建一个 Human 类,一个非常简单的描述将如下所示:

class Human {
    String name;
    int weight; // kg
    int height; // cm

    void eat(int foodWeight) {
        this.weight += foodWeight;
    }

    Human(int weight, int height, int name) {
        this.weight = weight; 
        this.height = height;
        this.name   = name;
    }
}

然后,我将使用上面的“蓝图”创建一个 对象,如下所示:

Human Joe = new Human(90, 180, "Joe");

现在,我们说Joe Human 的一个实例,其体重为 90 公斤,身高为 180 厘米。

在上面的类中,您注意到我有一个函数Human(),用于创建对象并在创建对象时定义它的状态。这基本上就是构造函数所做的。

那么 JavaScript 有什么不同呢?

为了在创建时吸引大众(正如您将在我发布的视频系列中听到的那样),JavaScript 结合了一些类似 Java 的语法。根据 Crockford 的说法,这样做的目的是让程序员认为,因为他们已经知道/学习了一些 Java,所以他们可以只学习一些新命令,然后继续使用 JavaScript 编程,而实际上,两者之间的差异两者的相似之处远远超过了它们的相似之处。

在 JavaScript 中,为了创建一个看起来像 Java 类的对象,您可以使用如下的函数语法:

var Human = function(name, height, weight) {
    this.name   = name;
    this.height = height;
    this.weight = weight;

    this.eat    = function(foodWeight) {
        this.weight += foodWeight;
    };
};

然后,如果您想像我们上面所做的那样定义Joe,您可以执行以下操作:

var Joe = new Human("Joe", 180, 90);

您可以看到所示的 Java 和 JavaScript 语法之间的相似之处。因此,回答您的第一个问题:JavaScript 构造函数是在使用 new 调用时创建并返回由 this 指向的隐式创建对象的函数。

那么原型从何而来?好吧,在 JavaScript 中,函数本身也是 JS 对象,它们有一个名为 prototype 的属性。所以,我们上面创建的Human()构造函数有一个属性叫prototype,这个属性指的是一个属性和方法都被Joe继承的对象,以及Human的所有其他实例,而这个对象可以扩展以创建将由所有这些实例继承的属性。

例如,Function.prototype 中的方法之一就是著名的toString 方法。你可以定义

Human.prototype.toString = function() {
    return this.name + " is " + this.height + " cm tall and weighs " + this.weight + " kg";
}

然后,如果您调用Joe.toString() 或当您执行类似alert(Joe) 之类的自动调用toString() 时,返回的值将是“Joe 身高 190 厘米,体重 80 公斤”。

关于 OOP 和 JavaScript 的更多细节可以在您的问题的上下文中介绍,但我认为我的回答已经足够长了!我希望这能回答你的问题。

【讨论】:

  • 在您的 Human JS 示例中,函数eat 更适合 Human.prototype,因为它不会在实例之间发生变化(它对 Joe.eat 和 Jane.eat 也是如此)。因此,无需在每次创建 Human 实例时都启动 eat。
  • 我同意,但我只是把它放在构造函数中,以便与Java类平行。非常感谢您指出这一点!
  • 我喜欢使用日常示例,foo & bar 让我发疯,让侮辱飞起来。使用 foo & bar 会导致向乔治的“人力基金”支付 32.74 美元的罚款
【解决方案3】:

简单英语的构造函数和原型?

正如“构造器”这个名字所暗示的那样,它创建了一些新的东西(一个对象),并且它创建的所有东西都遵循一个模板,即原型。

在 JavaScript 中,任何函数都可以用作构造函数,只需与普通函数调用不同地调用它们即可;例如:

function Foo()
{
}

Foo(); // normal function call, returns nothing
var f = new Foo(); // constructor call, returns a new Foo object

alert(f instanceof Foo) // "true"

如前所述,原型就像一个模板;您可以在运行时更改原型,并且更改会影响从该原型继承的所有对象。任何对象的原型都可以通过其构造函数的.prototype 属性访问。例如:

var f = new Foo();
Foo.prototype.bar = 'baz';

alert(f.bar) // "baz"

使用 Prototype 有什么需要?我想了解使用原型和构造函数背后的目的?我的意思是它们是否提供了更多的灵活性。

原型用于使用方法和属性定义共享行为和/或数据,类似于您对面向类语言的期望。它们也可以相互继承,创建一个原型链一直到Object;甚至函数实际上都是Function 对象。

如果没有原型,您将不得不在构造函数中完成所有工作:

function Foo()
{
    // add methods and data
    this.bar = 'baz';
}

在上面的例子中你可能看不到直接的好处,但有一些:

  1. 内存;向每个对象实例添加方法比通过原型链提供方法消耗更多的内存。不必遍历原型链的优势通常取决于实例化对象所花费的时间。

  2. 层次结构;当您的项目变得更大时,您最终将需要创建某种对象层次结构,如果没有原型,这会更加麻烦。

但是,如果您希望创建特权方法,则需要在构造函数本身中附加这些方法;从原型中不可能做到这一点;例如:

function Foo()
{
    var bar = 'baz';

    // privileged method
    this.bar = function() {
        return bar;
    }
}
var f = new Foo();
alert(f.bar()); // "baz"

我问这个问题是因为我在过去 6 个月里一直在使用这种语言,并且从未遇到过使用原型和构造函数的情况。

如果你在任何地方使用过new Option(...)new XYZ(),那么你就使用了构造函数;如果您在任何时候都使用过.hasOwnProperty().toString(),那么您将使用原型链:)

【讨论】:

  • 我使用了 hasownproperty 和 tostring..所以我猜他们在幕后使用它
  • @VAGABOND 它们来自Object.hasOwnProperty()Object.toString(),如果它们至少没有被覆盖:)
  • @VAGABOND 我可以想象 :) 如果我的答案中有任何不清楚的地方,请告诉我。
  • 您能否更详细地说明我们为什么使用原型...我的意思是我理解它是为了共享特性...但我可以了解更多细节吗?
  • @VAGABOND 我已经更新了答案以添加更多细节和一个您不想使用原型的示例。
【解决方案4】:

其他答案已经很好地回答了您的问题,但我想在组合中添加prototypes 的另一个方面:继承

正如其他答案已经显示的那样,附加到 myObject.prototype 的任何属性或方法都在实例之间共享:

var Car = function(color) {
    this.color = color;
}; 
Car.prototype.openDoor = function() {
    alert("Door is open!");
}

现在,您可以在每个实例上调用honk 方法:

var car1 = new Car('red');
var car2 = new Car('blue');
car1.openDoor();
car2.openDoor();

我们可以在Car 函数中包含openDoor,即

var Car = function(color) {
    this.color = color;
    this.openDoor = function() { alert("Door is open!"); }
}; 

但是,这会为Car 的每个实例添加一个openDoor 方法,这是非常浪费的,特别是如果它对所有实例执行完全相同的操作。通过将其添加到原型中,我们可以与所有实例共享它。

到目前为止一切都很好,但是当您将另一个对象分配给原型时,prototypes 的力量真正体现出来了:

var Vehicle = function(color) {
    this.color = color;
}; 
Vehicle.prototype.honk = function() {
    alert("Honk Honk! I am " + this.color);
}

var Car = function(color, maxPassengers){ 
    this.color = color;
    this.maxPassengers = maxPassengers;
} 
Car.prototype = new Vehicle();  
Car.prototype.constructor = Car;
Car.prototype.openDoor = function(){ 
    alert("Door is open! I have space for " + this.maxPassengers);
}

由于我们将Car.prototype 分配给Vehicle 构造函数,我们基本上将Car 链接到Vehicle,因此继承了它的所有属性和方法。实际上,我们inheritVehicles 的所有功能。

【讨论】:

    【解决方案5】:

    到目前为止你显然使用了什么

    由于到目前为止您还没有使用过构造函数(和原型),这意味着您或多或少地编写了procedural JavaScript code,从头到尾看起来像是一系列串行执行的代码。如果您想重用某些代码行,您可以将它们放在一个函数中,并在适当的时候调用它。

    只要您的页面上没有太多代码并且不需要任何模块 可重用性,即对象,就可以了。因为代码库越大,维护就越困难。模块化很有帮助,因为它遵循divide and conquer 原则。

    构造函数和原型

    这就是构造函数和原型发挥作用的地方。如果您使用 new 关键字正确执行 JavaScript,则 JavaScript 中的每个函数都可以是构造函数。基本上使用构造函数和原型,您可以以面向对象的方式实现您的代码,您可以在其中定义适当的对象 [proto] 类型并使用 OOP fundamentals,如 inheritanceencapsulation多态性

    对我有什么好处?

    OOP 相对于过程式编程的主要优势在于短期和长期的可维护性。

    好的,让我们创建一个对象,看看原型在哪里发挥作用

    让我们创建一个对象Rectangle

    var Rectangle = function(width, height) {
        this.width = width;
        this.height = height;
    };
    
    var instance = new Rectangle(4, 8);
    console.log(instance.width); // 4
    console.log(instance.height); // 8
    

    这将创建一个指定尺寸的矩形。让我们也向这个类flip 添加一个特殊的方法来翻转矩形。我们可以通过两种不同的方式做到这一点:

    1. 在构造函数中将其定义为实例方法

      var Rectangle = function(width, height) {
          this.width = width;
          this.height = height;
          this.flip = function() {
               var temp = this.width;
               this.width = this.height;
               this.height = temp;
          };
      };
      
    2. 在矩形type上定义它或者更好的说prototype

      var Rectangle = function(width, height) {
          this.width = width;
          this.height = height;
      };
      
      Rectangle.prototype.flip = function() {
           var temp = this.width;
           this.width = this.height;
           this.height = temp;
      };
      

    不过我们定义flip方法用法是一样的:

    var instance = new Rectangle(4, 8);
    instance.flip();
    console.log(instance.width); // 8
    console.log(instance.height); // 4
    

    但是还是有区别的。在案例 #1 中,当我们创建实例方法时,我们创建的每个对象都将拥有该方法的单独副本,但如果我们使用 #2,所有对象实例将共享相同方法。

    因此,使用 prototype 级别的方法将节省内存资源,并且以后对此方法的任何运行时修改都将反映在所有实例(已经实例化的实例和未来实例)上。

    但还有更多

    没有人说我们不能同时以两种方式创建相同的方法:作为实例和原型。

    var Rectangle = function(width, height) {
        this.width = width;
        this.height = height;
        this.flip = function() {
            var temp = this.width;
            this.width = this.height * 2;
            this.width = temp / 2;
        };
    };
    
    Rectangle.prototype.flip = function() {
        var temp = this.width;
        this.width = this.height;
        this.width = temp;
    };
    

    在这种情况下,我们的实例方法翻转并拉伸我们的矩形,同时保持其面积相同。原型方法只是翻转它。

    var instance = new Rectangle(4, 8);
    console.log(instance.width); // 4
    console.log(instance.height); // 8
    
    instance.flip();
    console.log(instance.width); // 16 = 8 * 2
    console.log(instance.height); // 2 = 4 / 2
    
    delete instance.flip;
    instance.flip();
    console.log(instance.width); // 2
    console.log(instance.height); // 16
    

    在这个例子中,我们创建了两个flip 方法。实例方法优先于原型方法,因此我们可以在特定对象实例上重新定义/重写默认原型功能。

    在调用实例方法后,我们将其删除并调用flip。由于实例方法不再存在,原型方法被执行,因此矩形只被翻转而没有尺寸变化。

    我在现实生活中为什么以及在哪里使用它?

    真的在任何地方,因为每当您的页面有例如 200 行代码时,以后扩展和维护它可能会变得越来越困难。将其更改为 OOP 会有所帮助。但是当您开始使用它时,您会以任何一种方式使用它,因为当页面的代码增长时您不必重构任何东西,并且还会与您的应用程序的其余部分保持一致。

    现实生活中的例子

    您可以想象 Stack Overflow 定义了一个类 Question,它具有问题的所有属性(id、标题、详细信息、标签数组、统计信息、cmets 等)以及与问题相关的所有方法(upvote 、downvote、编辑、删除、评论、回答等)。

    StackOverflow 的首页只会请求一个JSON 问题对象数组,并使用一些使用这些属性的 HTML 模板列出它们。用户对问题所做的任何事情都会反映到调用其方法之一。

    因此,所有内容都包含在内,并且仅具有所需的功能,而没有与页面其他部分(广告、导航、登录工具栏等)相关的任何其他混乱。这意味着只要与问题相关的功能出现错误,开发人员只需检查与Question原型相关的代码。他们不会被任何其他与页面相关的代码分心。

    【讨论】:

    • 哇..在赏金期结束后出现最佳答案..thx..老实说,我没想到会有这么多答案..现在我正处于选择最好的边缘!!
    • @VAGABOND:谢谢伙计。选择对您来说最清楚的答案。与您处于相同情况的其他用户可能会遇到类似情况。
    【解决方案6】:

    嗯,一些简单的东西可以帮助您入门,而不是过多的技术内容。

    考虑一下:

    function Person(){
        this.name = '';
        this.lastname = '';
        this.age = '';
    
        this.speak = function(msg){
            alert(msg);
        }
    }
    

    正如您已经知道的那样,这是一个简单的对象,具有自己独特的属性和方法/功能 您会同意每个人都有唯一的姓名、姓氏和年龄。

    到目前为止一切都很好...但是 99.999%(假设 100%)的人会说话...所以他们有一个共同的能力或 称它为方法或函数。

    换句话说,“说话能力”不是独一无二的东西,而是人们普遍存在的东西。 因此,为了内存消耗和其他各种技术问题,您可以像这样实现“说话”:

    Person.prototype.speak = function(msg){
        alert(msg);
    }
    

    我们现在所做的是,每当您创建一个人对象 ( var someone = new Person(); ) 他/她将拥有 3 个独特的属性和 1 个“通用”能力(方法函数)。

    简而言之,这更有效。

    还要考虑这个:

    function Person(){
        this.name = '';
        this.lastname = '';
        this.age = '';
        this.category = 'human';
    }
    

    VS

    function Person(){
        this.name = '';
        this.lastname = '';
        this.age = '';
    }
    
    Person.prototype.category = 'human'; // common among all people same as speak was.
    

    还有一些可以在您的控制台上尝试的东西, 粘贴最后一个 Person 函数及其原型声明后,执行此操作。

    var a = new Person();
    var b = new Person();
    

    然后:

    键入 a 和/或 b 并按 Enter 然后尝试这 2 个“命令”并重新检查您的对象。

    a.category = 'whatever';
    Person.prototype.category = 'whatever';
    

    【讨论】:

      【解决方案7】:

      原型是您通常定义函数或默认值的地方。如果我定义了 Person 对象和方法 getName,那么我可以肯定地说 getName 对 Jon、Mike 和 Betty 实例执行相同的操作(它将返回 this.name)。因为函数 getName 对 Person 的每个实例都执行相同的操作,所以您不希望在 Person 构造函数体中定义它:

      function Person(name){
        this.name = name; // This refers to the current instance
        this.getName = function(){
          return this.name;
        }
      }
      var Paul = new Person("Paul");// Paul has its own getName function
      var Ben = new Person("Ben");// Ben has its own getName function
      ...
      

      在上面的代码中 Person 被称为构造函数,你可以通过调用 constrictor 来创建 Person 的新实例:var someone=new Person。现在someone 是一个人的实例。 您在上面的代码中看到每个实例都有自己的 getName,如果对象有很多函数并且您正在创建许多实例,那么每次创建实例和内存时都会通过初始化函数来浪费 CPU 时间(因为每个实例都有一堆与其他实例做同样事情的函数。

      对于上面创建的对象,Paul 和 Ben,声明 Paul.hasOwnProperty('getName') 将是真的。

      如果您将 getName 放在 Person.prototype 上,那么实际上对于所有 Person 实例只有一个 getName 函数。一个新的 Person 实例将通过 Person.prototype 获得 getName,但每次创建 Person 时都不会初始化 getName。当我创建一百个 Person 实例然后更改 Person.prototype.getName 时,所有这些创建的实例都将使用更改后的 getName 函数。

      然后是您要考虑的继承(JavaScript 没有类)。您可以将所有这些 Person 的共享方法复制到(例如)Employee 的原型中。因为 getName 是 Person.prototype 上的一个函数,并且 Emloyee 继承了它,所以您可以直接调用 employeeInstance.getName()。如果 Employee 需要在 getName 中做一些额外的工作,你可以重写 Person 函数但仍然调用它(见下面的代码)

      Employee.prototype.getName=function(){
        return Person.getName.call(this) + " " + this.jobTitle;
      }
      

      有关构造函数、继承和覆盖函数的更多信息check out this answer

      如果你不明白这些话,我建议阅读Java tutorial。它解释了为什么要这样做。尽管 Java 在技术上使用类,但它会解释什么是继承和覆盖以及为什么要使用它。

      OOP 在一篇文章中很难解释,但上面的教程将涵盖其中的一部分。 Java 不是 JavaScript,JavaScript 不支持私有成员、类型检查和接口之类的东西。另一方面,当您想要更改对象的实例时,JavaScript 更加灵活。

      当您查看模式时,OOP 的真正威力就会显现出来。你可以google一下,网上的文章数不胜数。

      【讨论】:

        【解决方案8】:

        一个类为构建对象提供了一个模板(如模板)。在大多数语言中,模板是由钻石制成的,因此您无法更改它。

        在基于原型的语言中,就像您正在跟踪现有对象的轮廓以创建新对象。如果您随后决定“我需要在这个雪人对象上做一个更大的嘴”,那么您将在您用作原型的对象上设置更大的嘴,并且从这个修改后的雪人对象创建的任何对象都将具有更大的嘴。如果您随后使用其中一个旧雪人对象作为原型,则从中创建的雪人对象将具有原始的、较小的嘴。

        构造函数是用于在给定类或原型对象(取决于语言)的情况下创建新对象的代码。

        【讨论】:

        • “如果您随后使用其中一个旧雪人对象作为原型,则由它创建的雪人对象将具有原始的、较小的嘴。” 不,原因您在上一句中声明:“...您将用作 pfototype 的对象的嘴巴变大,并且从此修改后的雪人对象创建的任何对象都将具有更大的嘴巴...”
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2016-01-07
        • 2014-01-01
        • 2017-08-21
        • 1970-01-01
        • 2020-10-19
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多