15

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

我的问题:

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

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

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

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

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

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

我希望我很清楚。

4

8 回答 8

19

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

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

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

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

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

请注意,上述结果是在使用原型创建对象后向原型添加属性就可以了。您可以通过对象使用这些新属性:

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作为它们的原型,因此它们都有方便的diameter, circumference, et。人。功能。

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

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

于 2013-08-18T11:12:11.170 回答
7

首先,我建议你看看这个以他本人(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()叫做在所有这些情况下。prototypeJoeHuman

例如,其中一种方法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(),返回的值将是“乔身高 190 厘米,体重 80 公斤”。

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

于 2013-08-26T19:11:58.147 回答
4

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

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

在 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()在任何时候使用过原型链 :)

于 2013-08-27T05:43:28.743 回答
3

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

正如其他答案已经显示的那样,附加到的任何属性或方法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();

我们可以包含openDoorCar函数内部,即

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

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

到目前为止一切都很好,但是prototype当您将另一个对象分配给原型时, s 的力量确实显示出来了:

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.prototypeVehicle构造函数,因此我们基本上已经链接CarVehicle并因此继承了它的所有属性和方法。实际上,我们inherit所有Vehicle的特征。

于 2013-08-27T04:36:12.363 回答
3

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

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

只要您的页面上没有太多代码并且不需要任何模块可重用性,即objects ,那很好。因为代码库越大,维护就越困难。模块化有帮助,因为它遵循分而治之的原则。

构造函数和原型

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

对我有什么好处?

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. 在矩形类型或更好的原型上定义它

    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,则所有对象实例将共享相同的方法。

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

但还有更多

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

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、标题、详细信息、标签数组、统计信息、评论等)以及与问题相关的所有方法(upvote、downvote、edit、delete 、评论、回答等)。

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

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

于 2013-08-27T11:45:08.263 回答
2

Hmm well something simple to get you started and not into too many technical stuff.

Consider this:

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

    this.speak = function(msg){
        alert(msg);
    }
}

As you will already know this is a simple object with its own unique properties and methods / functions You would agree that each person has a unique name, lastname and an age.

All good so far... But 99.999%(Assume 100%) people can speak... so they have a common ability or call it a method or a function.

In other words the "Speak ability" is not something unique rather than something common among people. So for the sake of memory consumption and other various technical stuff you could implement "speak" like this:

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

What we' ve done now is that whenever you create a person object ( var someone = new Person(); ) he/she will have 3 unique properties and 1 "common" ability (method-function).

In short terms this is more efficient.

Also consider this:

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.

And something to try on your console, after pasting this last Person function and it's prototype declaration, do this.

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

then:

type a and / or b and press enter then try these 2 "commands" and recheck your objects.

a.category = 'whatever';
Person.prototype.category = 'whatever';
于 2013-08-27T11:18:28.563 回答
2

原型是您通常定义函数或默认值的地方。如果我定义了一个 person 对象和一个getNamePerson 的方法,那么我可以肯定地说这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;
}

有关构造函数、继承和覆盖函数的更多信息,请查看此答案

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

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

当您检查模式时,OOP 的真正威力就会显现出来。你可以用谷歌搜索它,因为互联网上有无数的文章。

于 2013-08-22T01:15:02.913 回答
1

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

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

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

于 2013-08-21T17:43:05.063 回答