“JavaScript是世界上最容易被误解的语言”-D.Crockford
我的问题:
我不是在寻找任何语法以及如何解释事物,因为我理解它们的某些部分,只是想以更简单的方式了解这些事情。类比(非技术)或例子会很棒。*
详细说明我问这个问题的原因(如果你愿意,请忽略):
过去六个月我一直在使用JavaScript,当我发现JavaScript是一种基于原型的语言时,我感到震惊。
我经历了一些关于如何使用JavaScript并遇到原型和构造函数的Stack Overflow问题。
我学到了它,现在我可以说在构造函数和原型方面我不是一个菜鸟。我熟悉语法。但我仍然觉得我错过了一些东西而没有深入到这种语言的核心,我有时会感到困惑。
我希望我很清楚。
答案 0 :(得分:19)
简明英语的构造函数和原型?
构造函数创建对象并为其分配原型。原型是具有各种属性的对象,对象可以通过原型链继承这些属性。与往常一样,示例有帮助:
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"
使用Prototypes和构造函数的目的是什么?
分享对象之间的特征。原型的属性可以是函数或数据,使用该原型的对象都可以访问并可以重用。
重新评论以下内容:
我理解了关于共享特征的部分,但是我可以在其中获得更多详细信息
好吧,考虑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
等。人。功能
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
他们以一种以内存效率的方式共享这些属性:每个实例都没有自己的属性副本(这意味着在每个obejct中保留每个属性名称及其值);相反,他们只是引用了他们共享的原型,这些原型具有这些属性。
答案 1 :(得分:7)
首先,我建议你看看this playlist以自己为人物(Crockford)。它可能很旧,但它确实很好地解释了JavaScript“逻辑”,你的问题在第三个视频中得到了特别的回答。
我将通过描述如何在其他传统的面向对象编程语言中描述对象来开始回答这个问题,因为我还想针对您在问题开头发布的Crockford评论。
为了理解构造函数,首先必须对对象有一个很好的理解。在传统的OOP语言中,Object是描述对象状态的变量(称为属性或字段)的集合,以及描述其行为的函数(称为方法)。在那些(非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
指向的隐式创建的对象。
那么Prototype的位置在哪里?好吧,在JavaScript中,函数本身也是JS对象,它们有一个名为prototype
的属性。因此,我们上面创建的Human()
构造函数有一个名为prototype
的属性,此属性引用一个对象,其属性和方法由Joe
继承,以及{的所有其他实例{1}},可以扩展此对象,以便创建将由所有这些实例继承的属性。
例如,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)
的内容,则返回的值为“乔身高190厘米,重80公斤”。
有关OOP和JavaScript的更多详细信息可以在您的问题的上下文中介绍,但我认为我的答案足够长!我希望这能回答你的问题。
答案 2 :(得分: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需要什么?我想了解使用Prototypes和构造函数背后的目的?我的意思是他们提供更多的灵活性。
原型用于使用方法和属性定义共享行为和/或数据,类似于您可能对类面向语言的期望。他们也可以相互继承,创造一系列原型,一直到Object
;偶数函数实际上是Function
个对象。
如果没有原型,您必须在构造函数中完成所有工作:
function Foo()
{
// add methods and data
this.bar = 'baz';
}
在上面的示例中,您可能看不到直接的好处,但有一些:
存储器;向每个对象实例添加方法比通过原型链提供的内存消耗更多的内存。不必遍历原型链的优势通常是实例化对象所需的时间。
层次结构;当你的项目变大时,你最终需要创建某种对象层次结构,没有原型这会更麻烦。
但是,如果您希望创建特权方法,则需要在构造函数本身中附加这些方法;原型不可能做到这一点;例如:
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()
,那么您将使用原型链:)
答案 3 :(得分: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();
我们可以在openDoor
函数中加入Car
,即
var Car = function(color) {
this.color = color;
this.openDoor = function() { alert("Door is open!"); }
};
但是,这会为openDoor
的每个实例添加Car
方法,这非常浪费,特别是如果它对所有实例完全相同的话。通过将其添加到原型中,我们将它与所有实例共享。
到目前为止一切都那么好,但是prototype
的强大功能确实在你将另一个对象分配给原型时显示出来了:
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
,因此继承了它的所有属性和方法。实际上,我们inherit
具有Vehicle
个所有功能。
答案 4 :(得分:3)
由于到目前为止您还没有使用构造函数(和原型),这意味着您或多或少地编写了procedural JavaScript code,它看起来像是从头到尾的一系列连续执行的代码。如果你想重用一些代码行,你可以把它们放在一个函数中,并在适当的时候调用它。
只要您的页面上没有太多代码并且不需要任何模块可重用性,即对象,那就没问题。因为代码库越大,维护就越难。模块化有帮助,因为它遵循divide and conquer原则。
这是构造函数和原型发挥作用的地方。如果使用new
关键字正确执行,JavaScript中的每个函数都可以是构造函数。基本上使用构造函数和原型,您可以以面向对象的方式实现代码,您可以在其中定义适当的对象[proto]类型并使用OOP fundamentals,如继承,封装和多态。
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
添加一个特定的方法。我们可以通过两种不同的方式实现这一目标:
在构造函数中将其定义为实例方法:
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;
};
};
在矩形类型上定义它,或者更好地说原型
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 -level方法将节省内存资源,并且此方法的任何后续运行时修改都将反映在所有实例(已实例化和未来实例)上。
没有人说我们不能同时以两种方式创建相同的方法:作为实例和原型。
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,comment,answer等)。
Stack Overflow首页只会请求JSON个问题对象数组,并使用一些使用这些属性的HTML模板列出它们。用户对问题所做的任何事情都会反映出调用其中一种方法。
所以一切都包含得很好,只有所需的功能,没有任何其他与页面其他部分相关的混乱(广告,导航,登录工具栏等)。这意味着只要问题相关功能存在错误,开发人员只需要查看与Question
原型相关的代码。他们不会被任何其他与页面相关的代码分心。
答案 5 :(得分:2)
Prototype是您通常定义函数或默认值的地方。如果我定义Person对象和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上,那么实际上只有一个getName函数用于所有Person实例。新的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它,因为互联网上有无数的文章。
答案 6 :(得分:2)
嗯,很简单的事情让你开始,而不是太多技术性的东西。
考虑一下:
function Person(){
this.name = '';
this.lastname = '';
this.age = '';
this.speak = function(msg){
alert(msg);
}
}
您已经知道这是一个具有自己独特属性和方法/功能的简单对象 您会同意每个人都有一个唯一的姓名,姓氏和年龄。
到目前为止一切都很好......但99.999%(假设100%)人们可以说...所以他们有共同的能力或 称之为方法或功能。
换句话说,“说话能力”并不是一种独特的东西,而不是人们共同的东西。 因此,为了记忆消耗和其他各种技术内容,你可以像这样实现“说话”:
Person.prototype.speak = function(msg){
alert(msg);
}
我们现在所做的是每当你创建一个person对象时(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键 然后尝试这两个“命令”并重新检查你的对象。
a.category = 'whatever';
Person.prototype.category = 'whatever';
答案 7 :(得分:1)
类为构建对象提供模板(如模板)。在大多数语言中,模板由钻石制成,因此您无法改变它。
在基于原型的语言中,就像是在跟踪现有对象的轮廓来制作新对象。如果你然后决定“我需要在这个雪人物体上放一个更大的嘴”,你就会在你用作原型的物体上做大口,并且用这个修改过的雪人物体创造的任何物体都会有更大的嘴。如果你使用其中一个旧的雪人物体作为你的原型,那么用它制作的雪人物将会有原始的小嘴。
构造函数是用于在给定类或原型对象(取决于语言)的情况下创建新对象的代码。