繁体   English   中英

在 JavaScript 中使用“原型”与“这个”?

[英]Use of 'prototype' vs. 'this' in JavaScript?

有什么区别

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

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

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

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

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

所以这里是有问题的片段:

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

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

如果是:

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

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

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

另一个例子如下。 它类似于第一个(也许你想问的是什么):

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

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

要检查A是否具有x属性:

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

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

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

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

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

在这种情况下, A分配了调用右侧函数的返回值。 同样,由于this未在调用中设置,它将引用全局对象并且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

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

以这两个例子为例:

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

对比

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 // undefined
A = function() {}
A.prototype // {} // got created when function() {} was defined

A.prototype__proto__属性完全不同。 在我们的示例中,'A' 现在有两个属性,称为 'prototype' 和__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()时,后台会发生 3 件事:

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

  3. 函数A()正在执行与this组在步骤1中创建的新的空物体(读上面提到的答案我,为什么this改变a1

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

var a2 = new A();

步骤 1、2、3 将重复。 你注意到什么了吗? 关键词是重复。 第 1 步: a2将是一个新的空对象,第 2 步:它的__proto__属性将指向A.prototype指向的同一事物,最重要的是,第 3 步:再次执行函数A() ,这意味着a2将得到hey包含函数的属性。 a1a2有两个名为hey SEPARATE 属性,它们指向 2 个单独的函数! 我们现在在两个不同的对象中有重复的函数做同样的事情,哎呀...如果我们有 1000 个用new A创建的对象,你可以想象这对内存的影响,毕竟函数声明比数字 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 ,像这样的问题就会很明显了。

注意:有些人倾向于不将内部 Prototype 属性称为__proto__ ,我在帖子中使用了这个名称来将它与Functional.prototype属性明确区分为两个不同的东西。

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

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

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 的函数,但不能用于对象之外。

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

使用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' method
    BaseClass.call(this);

    // Keeping a reference to the superclass' method
    var super_setText = this.setText;
    // Overriding
    this.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 overridden
console.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 expected
    BaseClass.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 leak
        console.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

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

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


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

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 function
a.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

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

有什么不同? => 很多。

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

让我们看看下面的例子:

var AdultPerson = function() {

  var age;

  this.setAge = function(val) {
    // some housekeeping
    age = val >= 18 && val;
  };

  this.getAge = function() {
    return age;
  };

  this.isValid = function() {
    return !!age;
  };
};

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

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

AdultPerson.prototype.getRights = function() {
  // Should be valid
  return 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 ***

希望这可以帮助。

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

直接作用于对象:

 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方法创建了 2,000,000 个新对象。 我们将每个对象存储在一个数组中。 在原型上print大约需要 1/2 的时间。

让我给你一个更全面的答案,这是我在 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是一个函数认为是object ),可以像你习惯的那样被调用。 它有一个属性methods (它是一个具有move功能的对象)。 Car被调用extend函数被调用,其中做了一些魔法,并延长了Car功能(认为对象)与内定义的方法methods

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

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对象, prototype对象通过constructor链接到Car本身,即Car.prototype.constructorCar本身。 这使您可以确定哪个构造函数构建了某个对象。

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

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

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 中应用超类或子类等概念。

随意添加或编辑。 再完成一次,我可以把它变成一个社区维基。

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

我相信@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 中定义我的类。 变量名称已更改以反映这一点。

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

我整理了一个 jsperf 来展示这一点。 实例化类所花费的时间存在显着差异,尽管它仅在您创建多个实例时才真正相关。

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

想想静态类型语言,对事物prototype是静态的,对事物this是与实例相关。

当您使用原型时,该函数只会被加载到内存中一次(与您创建的对象数量无关),您可以随时覆盖该函数。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM