在JavaScript中使用“原型”与“此”?

之间有什么区别

var A = function () {this.x = function () {//do something};};

var A = function () { };A.prototype.x = function () {//do something};
127479 次浏览

第一个示例仅更改该对象的接口。第二个示例更改该类的所有对象的接口。

在大多数情况下,它们本质上是相同的,但第二个版本节省了内存,因为每个对象只有一个函数实例,而不是一个单独的函数。

使用第一种形式的原因是访问“私有成员”。例如:

var A = function () {var private_var = ...;
this.x = function () {return private_var;};
this.setX = function (new_x) {private_var = new_x;};};

由于javascript的作用域规则,private_var可用于分配给this. x的函数,但不能在对象之外。

我相信@Matthew Crumley是对的。它们是功能,如果不是结构上,等效的。如果你使用Firebug查看使用new创建的对象,你可以看到它们是相同的。然而,我的偏好如下。我猜它更像是我在C#/Java中习惯的。即定义类、定义字段、构造函数和方法。

var A = function() {};A.prototype = {_instance_var: 0,
initialize: function(v) { this._instance_var = v; },
x: function() {  alert(this._instance_var); }};

编辑并不意味着变量的范围是私有的,我只是想说明我如何在javascript中定义我的类。变量名称已被更改以反映这一点。

这些例子有非常不同的结果。

在查看差异之前,应注意以下几点:

  • 构造函数的原型提供了一种通过实例的私有[[Prototype]]属性在实例之间共享方法和值的方法。
  • 函数的这个由函数的调用方式或绑定的使用来设置(此处不讨论)。如果在对象上调用函数(例如myObj.method()),则方法中的这个引用该对象。如果这个未通过调用或使用绑定设置,则默认为全局对象(浏览器中的窗口)或严格模式,保持未定义。
  • JavaScript是一种面向对象的语言,即大多数值都是对象,包括函数。(字符串、数字和布尔值是没有对象。)

以下是有问题的片段:

var A = function () {this.x = function () {//do something};};

在这种情况下,变量A被分配了一个值,该值是对函数的引用。当使用A()调用该函数时,调用不会设置函数的这个,因此它默认为全局对象,表达式this.x有效window.x。结果是右侧对函数表达式的引用被分配给window.x

在以下情况下:

var A = function () { };A.prototype.x = function () {//do something};

发生了一些非常不同的事情。在第一行中,变量A被分配了对函数的引用。在JavaScript中,所有函数对象默认都有原型属性,因此没有单独的代码来创建A.prototype对象。

在第二行中,A.prototype.x被分配了一个对函数的引用。如果x属性不存在,这将创建一个x属性,如果它存在,则分配一个新值。因此,与第一个示例的区别在于表达式中涉及对象的x属性。

下面是另一个例子。它与第一个相似(也许你想问的是什么):

var A = new function () {this.x = function () {//do something};};

在此示例中,在函数表达式之前添加了new运算符,以便将函数作为构造函数调用。当使用new调用时,函数的这个设置为引用一个新Object,其私有[[Prototype]]属性设置为引用构造函数的公共原型。因此在赋值语句中,将在这个新对象上创建x属性。当作为构造函数调用时,函数默认返回其这个对象,因此不需要单独的return this;语句。

要检查一个是否具有x属性:

console.log(A.x) // function () {//   //do something// };

这是新的的不常见用法,因为引用构造函数的唯一方法是通过A.constructor。这样做更常见:

var A = function () {this.x = function () {//do something};};var a = new A();

实现类似结果的另一种方法是使用立即调用的函数表达式:

var A = (function () {this.x = function () {//do something};}());

在这种情况下,A在右侧分配了调用函数的返回值。同样,由于调用中没有设置这个,它将引用全局对象,this.x有效window.x。由于函数没有返回任何东西,A的值将为undefined

如果您将Javascript对象序列化和反序列化到/从JSON,这两种方法之间的这些差异也会体现出来。序列化对象时,在对象原型上定义的方法不会序列化,例如,当您只想序列化对象的数据部分而不是方法时,这可能很方便:

var A = function () {this.objectsOwnProperties = "are serialized";};A.prototype.prototypeProperties = "are NOT serialized";var instance = new A();console.log(instance.prototypeProperties); // "are NOT serialized"console.log(JSON.stringify(instance));// {"objectsOwnProperties":"are serialized"}

相关问题

附注:两种方法之间可能没有任何显着的内存节省,但是使用原型共享方法和属性可能会比每个实例拥有自己的副本使用更少的内存。

JavaScript不是一种低级语言。将原型设计或其他继承模式视为显式更改内存分配方式的一种方式可能不是很有价值。

正如其他人所说的第一个版本,使用“this”会导致类A的每个实例都有自己独立的函数方法“x”的副本。而使用“原型”则意味着类A的每个实例都将使用方法“x”的相同副本。

以下是一些代码来显示这种微妙的差异:

// x is a method assigned to the object using "this"var A = function () {this.x = function () { alert('A'); };};A.prototype.updateX = function( value ) {this.x = function() { alert( value ); }};
var a1 = new A();var a2 = new A();a1.x();  // Displays 'A'a2.x();  // Also displays 'A'a1.updateX('Z');a1.x();  // Displays 'Z'a2.x();  // Still displays 'A'
// Here x is a method assigned to the object using "prototype"var B = function () { };B.prototype.x = function () { alert('B'); };
B.prototype.updateX = function( value ) {B.prototype.x = function() { alert( value ); }}
var b1 = new B();var b2 = new B();b1.x();  // Displays 'B'b2.x();  // Also displays 'B'b1.updateX('Y');b1.x();  // Displays 'Y'b2.x();  // Also displays 'Y' because by using prototype we have changed it for all instances

正如其他人提到的,选择一种方法或另一种方法有各种原因。我的示例只是为了清楚地展示差异。

原型是类的模板;它适用于它的所有未来实例。而这是对象的特定实例。

使用this而不是prototype的最终问题是,当覆盖一个方法时,基类的构造函数仍然会引用被覆盖的方法。考虑一下:

BaseClass = function() {var text = null;
this.setText = function(value) {text = value + " BaseClass!";};
this.getText = function() {return text;};
this.setText("Hello"); // This always calls BaseClass.setText()};
SubClass = function() {// setText is not overridden yet,// so the constructor calls the superclass' methodBaseClass.call(this);
// Keeping a reference to the superclass' methodvar super_setText = this.setText;// Overridingthis.setText = function(value) {super_setText.call(this, "SubClass says: " + value);};};SubClass.prototype = new BaseClass();
var subClass = new SubClass();console.log(subClass.getText()); // Hello BaseClass!
subClass.setText("Hello"); // setText is already overriddenconsole.log(subClass.getText()); // SubClass says: Hello BaseClass!

对:

BaseClass = function() {this.setText("Hello"); // This calls the overridden method};
BaseClass.prototype.setText = function(value) {this.text = value + " BaseClass!";};
BaseClass.prototype.getText = function() {return this.text;};
SubClass = function() {// setText is already overridden, so this works as expectedBaseClass.call(this);};SubClass.prototype = new BaseClass();
SubClass.prototype.setText = function(value) {BaseClass.prototype.setText.call(this, "SubClass says: " + value);};
var subClass = new SubClass();console.log(subClass.getText()); // SubClass says: Hello BaseClass!

如果你认为这不是问题,那么这取决于你是否可以在没有私有变量的情况下生活,以及你是否有足够的经验在看到泄漏时知道泄漏。此外,必须将构造函数逻辑放在方法定义之后是不方便的。

var A = function (param1) {var privateVar = null; // Private variable
// Calling this.setPrivateVar(param1) here would be an error
this.setPrivateVar = function (value) {privateVar = value;console.log("setPrivateVar value set to: " + value);
// param1 is still here, possible memory leakconsole.log("setPrivateVar has param1: " + param1);};
// The constructor logic starts here possibly after// many lines of code that define methods
this.setPrivateVar(param1); // This is valid};
var a = new A(0);// setPrivateVar value set to: 0// setPrivateVar has param1: 0
a.setPrivateVar(1);//setPrivateVar value set to: 1//setPrivateVar has param1: 0

对:

var A = function (param1) {this.setPublicVar(param1); // This is valid};A.prototype.setPublicVar = function (value) {this.publicVar = value; // No private variable};
var a = new A(0);a.setPublicVar(1);console.log(a.publicVar); // 1

有什么区别?=>很多。

我认为,this版本用于启用封装,即数据隐藏。它有助于操作私有变量。

让我们看看下面的例子:

var AdultPerson = function() {
var age;
this.setAge = function(val) {// some housekeepingage = val >= 18 && val;};
this.getAge = function() {return age;};
this.isValid = function() {return !!age;};};

现在,prototype结构可以应用如下:

不同的成年人有不同的年龄,但所有的成年人都有相同的权利。
所以,我们使用原型添加它,而不是这个。

AdultPerson.prototype.getRights = function() {// Should be validreturn this.isValid() && ['Booze', 'Drive'];};

现在让我们看看执行情况。

var p1 = new AdultPerson;p1.setAge(12); // ( age = false )console.log(p1.getRights()); // false ( Kid alert! )p1.setAge(19); // ( age = 19 )console.log(p1.getRights()); // ['Booze', 'Drive'] ( Welcome AdultPerson )
var p2 = new AdultPerson;p2.setAge(45);console.log(p2.getRights()); // The same getRights() method, *** not a new copy of it ***

希望这有帮助。

正如在其他答案中所讨论的,这实际上是一个性能考虑因素,因为原型中的函数与所有实例化共享-而不是为每个实例化创建的函数。

我整理了一个jspef来展示这一点。实例化类所需的时间存在巨大差异,尽管只有在您制作许多实例时才真正相关。

http://jsperf.com/functions-in-constructor-vs-prototype

让我给你一个更全面的答案,我在JavaScript培训课程中学到了。

大多数答案已经提到了区别,即当原型化函数与所有(未来)实例共享时。而在类中声明函数将为每个实例创建一个副本。

一般来说,没有对与错,这更多的是品味或设计决策的问题,取决于您的需求。然而,原型是用于以面向对象的方式开发的技术,我希望您会在本答案的末尾看到。

你在你的问题中显示了两种模式。我会试着再解释两个,如果相关,我会试着解释差异。请随意编辑/扩展。在所有示例中,它都是关于一个具有位置并且可以移动的汽车对象。

对象装饰器模式

不确定这种模式现在是否仍然相关,但它存在。了解它是件好事。您只需将一个对象和一个属性传递给装饰器函数。装饰器返回带有属性和方法的对象。

var carlike = function(obj, loc) {obj.loc = loc;obj.move = function() {obj.loc++;};return obj;};
var amy = carlike({}, 1);amy.move();var ben = carlike({}, 9);ben.move();

功能类

JavaScript中的函数是一个专门的对象。除了被调用之外,函数还可以像任何其他对象一样存储属性。

在这种情况下,Car是一个函数认为对象),可以像你习惯的那样调用。它有一个属性methods(它是一个带有move函数的对象)。当Car被调用时,extend函数被调用,它做了一些魔法,并使用methods中定义的方法扩展了Car函数(思考对象)。

这个例子虽然不同,但最接近问题中的第一个例子。

var Car = function(loc) {var obj = {loc: loc};extend(obj, Car.methods);return obj;};
Car.methods = {move : function() {this.loc++;}};
var amy = Car(1);amy.move();var ben = Car(9);ben.move();

原型类

前两种模式允许讨论使用技术来定义共享方法或使用在构造函数主体中内联定义的方法。在这两种情况下,每个实例都有自己的move函数。

原型模式不能很好地用于相同的检查,因为通过原型委托的函数共享是原型模式的目标。正如其他人指出的那样,预计它会有更好的内存占用。

然而,有一点很有趣:每个prototype对象都有一个便利属性constructor,它指向它所附加的函数(想想对象)。

关于最后三行:

在此示例中,Car链接到prototype对象,该对象通过constructor链接到Car本身,即Car.prototype.constructorCar本身。这允许您找出哪个构造函数构建了某个对象。

amy.constructor的查找失败,因此委托给Car.prototype,它确实具有构造函数属性。所以amy.constructorCar

此外,amyinstanceofCarinstanceof运算符的工作原理是查看右操作数的原型对象(Car)是否可以在左操作数的原型(amy)链的任何位置找到。

var Car = function(loc) {var obj = Object.create(Car.prototype);obj.loc = loc;return obj;};
Car.prototype.move = function() {this.loc++;};
var amy = Car(1);amy.move();var ben = Car(9);ben.move();
console.log(Car.prototype.constructor);console.log(amy.constructor);console.log(amy instanceof Car);

一些开发人员一开始可能会感到困惑。参见下面的示例:

var Dog = function() {return {legs: 4, bark: alert};};
var fido = Dog();console.log(fido instanceof Dog);

instanceof运算符返回false,因为在fido的原型链中找不到Dog的原型。fido是一个使用对象文字创建的简单对象,即它只委托给Object.prototype

伪古典模式

这实际上只是简化形式的原型模式的另一种形式,对于那些在Java中编程的人来说更熟悉,因为它使用new构造函数。

它实际上和原型模式是一样的,它只是原型模式之上的语法糖。

然而,主要区别在于JavaScript引擎中实现的优化仅在使用伪经典模式时适用。想想伪经典模式可能是原型模式的更快版本;两个示例中的对象关系是相同的。

var Car = function(loc) {this.loc = loc;};
Car.prototype.move = function() {this.loc++;};
var amy = new Car(1);amy.move();var ben = new Car(9);ben.move();

最后,实现如何做面向对象程序设计应该不会太难。有两个部分。

定义原型(链)中的公共属性/方法的部分。

另一个部分,您放置了区分对象的定义(示例中的loc变量)。

这就是允许我们在JavaScript中应用超类或子类等概念的原因。

随意添加或编辑。一旦完成,我可以将其作为社区wiki。

举这两个例子:

var A = function() { this.hey = function() { alert('from A') } };

vs.

var A = function() {}A.prototype.hey = function() { alert('from prototype') };

这里的大多数人(尤其是排名最高的答案)试图在不解释为什么的情况下解释它们有何不同。我认为这是错误的,如果你先了解基本面,差异就会变得明显。让我们先解释基本面…

a)函数是JavaScript中的一个对象。JavaScript中的每个对象都有一个内部属性(这意味着,你不能像其他属性一样访问它,除非在Chrome这样的浏览器中),通常被称为__proto__(你实际上可以在Chrome中键入anyObject.__proto__来查看它引用了什么。这只是一个属性,仅此而已。JavaScript中的属性=一个对象内部的变量,仅此而已。变量做什么?它们指向事物。

那么这个__proto__属性指向什么?嗯,通常是另一个对象(我们稍后会解释为什么)。强制JavaScript__proto__属性不指向另一个对象的唯一方法是使用var newObj = Object.create(null)。即使你这样做,__proto__属性仍然作为对象的属性存在,只是它不指向另一个对象,它指向null

这是大多数人感到困惑的地方:

当你在JavaScript中创建一个新函数(它也是一个对象,记得吗?)时,当它被定义时,JavaScript会自动在该函数上创建一个名为prototype的新属性。试试看:

var A = [];A.prototype // undefinedA = function() {}A.prototype // {} // got created when function() {} was defined

A.prototype__proto__属性完全不同。在我们的示例中,'A'现在有两个属性,称为'原型'和__proto__。这对人们来说是一个很大的困惑。prototype__proto__属性没有任何关系,它们是指向单独值的独立事物。

你可能想知道:为什么JavaScript在每个对象上都创建了__proto__属性?好吧,一个词:代表团。当你调用一个对象的属性而该对象没有它时,JavaScript会查找__proto__引用的对象,看看它是否拥有它。如果没有,那么它会查看该对象的__proto__属性,依此类推……直到链条结束。因此,名称原型链。当然,如果__proto__没有指向一个对象,而是指向null,运气不好,JavaScript意识到这一点,并将返回你undefined的属性。

你可能还想知道,为什么JavaScript在定义函数时为函数创建一个名为prototype的属性?因为它试图欺骗你,是的愚弄你,它的工作方式就像基于类的语言。

让我们继续我们的示例并从A中创建一个“对象”:

var a1 = new A();

当这件事发生时,后台发生了一些事情。a1是一个普通变量,它被分配了一个新的空对象。

在函数调用A()之前使用运算符new这一事实在后台做了一些额外的事情。new关键字创建了一个新对象,现在引用a1并且该对象为空。这是另外发生的事情:

我们说在每个函数定义上都创建了一个名为prototype的新属性(您可以访问它,与__proto__属性不同)创建?好吧,现在正在使用该属性。

所以我们现在有一个新鲜出炉的空a1对象。我们说JavaScript中的所有对象都有一个内部的__proto__属性,它指向某个东西(a1也有它),无论是null还是其他对象。new运算符所做的是将__proto__属性设置为指向函数的prototype属性。再读一遍。基本上是这样的:

a1.__proto__ = A.prototype;

我们说A.prototype只不过是一个空对象(除非我们在定义a1之前将其更改为其他对象)。所以现在基本上a1.__proto__指向A.prototype指向的同样的东西,也就是那个空对象。它们都指向发生这一行时创建的同一个对象:

A = function() {} // JS: cool. let's also create A.prototype pointing to empty {}

现在,当处理var a1 = new A()语句时会发生另一件事。基本上A()被执行,如果A是这样的:

var A = function() { this.hey = function() { alert('from A') } };

function() { }中的所有内容都将执行。当你到达this.hey..行时,this被更改为a1,你会得到这个:

a1.hey = function() { alert('from A') }

我不会介绍为什么this更改为a1,而是这是一个很好的答案以了解更多信息。

所以总结一下,当你做var a1 = new A()时,后台会发生三件事:

  1. 一个全新的空对象被创建并分配给a1a1 = {}
  2. a1.__proto__属性被分配指向与A.prototype指向(另一个空对象{})相同的东西

  3. 函数A()正在执行,this设置为步骤1中创建的新空对象(阅读我上面引用的关于为什么this更改为a1的答案)

现在,让我们尝试创建另一个对象:

var a2 = new A();

步骤1,2,3将重复。你注意到什么了吗?关键字是__proto__0步骤1:a2将是一个新的空对象,步骤2:它的__proto__属性将指向A.prototype指向的同样的事情,最重要的是,步骤3:函数A()再次执行,这意味着a2将获得hey包含函数的属性。a1a2有两个名为hey的SEPARATE属性,指向2个SEPARATE函数!我们现在在同一个两个不同的对象中有重复的函数做同样的事情,哎呀…如果我们用new A创建了1000个对象,你可以想象这对内存的影响,毕竟函数声明比数字2占用更多的内存。那么我们如何防止这种情况呢?

还记得为什么每个对象上都有__proto__属性吗?所以如果你在a1上检索yoMan属性(它不存在),它的__proto__属性将被咨询,如果它是一个对象(大多数情况下是),它将检查它是否包含yoMan,如果没有,它将咨询该对象的__proto__等。如果是,它将获取该属性值并显示给你。

所以有人决定使用这个事实+当你创建a1时,它的__proto__属性指向同一个(空)对象A.prototype指向并执行以下操作:

var A = function() {}A.prototype.hey = function() { alert('from prototype') };

酷!现在,当你创建a1时,它再次完成上面的所有3个步骤,在第3步中,它什么也不做,因为function A()没有什么要执行的。如果我们这样做:

a1.hey

它会看到a1不包含hey,它会检查它的__proto__属性对象是否有它,情况就是这样。

通过这种方法,我们从步骤3中删除了在每个新对象创建时重复函数的部分。而不是a1a2有一个单独的hey属性,现在他们都没有。我想,你现在已经明白了自己。这是件好事……如果你理解__proto__Function.prototype,像这样的问题将非常明显。

注意:有些人倾向于不把内部原型属性称为__proto__,我通过这篇文章使用了这个名字来清楚地将它与Functional.prototype属性区分开来。

每个对象都链接到一个原型对象。当尝试访问不存在的属性时,JavaScript将在对象的原型对象中查找该属性,并在它存在时返回它。

函数构造函数的prototype属性是指使用new时使用该函数创建的所有实例的原型对象。


在第一个示例中,您将属性x添加到使用A函数创建的每个实例。

var A = function () {this.x = function () {//do something};};
var a = new A();    // constructor function gets executed// newly created object gets an 'x' property// which is a functiona.x();              // and can be called like this

在第二个示例中,您将向原型对象添加一个属性,所有使用A创建的实例都指向该属性。

var A = function () { };A.prototype.x = function () {//do something};
var a = new A();    // constructor function gets executed// which does nothing in this example
a.x();              // you are trying to access the 'x' property of an instance of 'A'// which does not exist// so JavaScript looks for that property in the prototype object// that was defined using the 'prototype' property of the constructor

总之,在第一个例子将函数的副本分配给每个实例中。在第二个例子该函数的单个副本由所有实例共享中。

我知道这已经被回答死了,但我想展示一个速度差异的实际例子。

直接在对象上运行:

function ExampleFn() {this.print = function() {console.log("Calling print! ");}}
var objects = [];console.time('x');for (let i = 0; i < 2000000; i++) {objects.push(new ExampleFn());}console.timeEnd('x');
//x: 1151.960693359375ms

原型函数:

function ExampleFn() {}ExampleFn.prototype.print = function() {console.log("Calling print!");}
var objects = [];console.time('y');for (let i = 0; i < 2000000; i++) {objects.push(new ExampleFn());}console.timeEnd('y');
//x: 617.866943359375ms

在这里,我们在Chrome中使用print方法创建了200万个新对象。我们将每个对象存储在一个数组中。将print放在原型上大约需要1/2的时间。

想想静态类型语言,prototype上的东西是静态的,this上的东西是实例相关的。

当您使用原型时,该函数只会加载一次到内存中(独立于您创建的对象数量),并且您可以随时覆盖该函数。