JavaScript中的.prototype是如何工作的?

2176
我对动态编程语言不是很感兴趣,但我写过相当多的JavaScript代码。我从来没有真正理解原型编程,有人知道这是如何工作的吗?
var obj = new Object();
obj.prototype.test = function() { alert('Hello?'); };
var obj2 = new obj();
obj2.test();

我记得一段时间前我和人们讨论了很多(我不太确定自己在做什么),但据我理解,JavaScript中没有类的概念。它只是一个对象,而这些对象的实例是原始对象的克隆,对吗?
但是,JavaScript中的".prototype"属性的确切目的是什么?它与实例化对象有什么关系?
更新:正确的方式
var obj = new Object(); // not a functional object
obj.prototype.test = function() { alert('Hello?'); }; // this is wrong!

function MyObject() {} // a first class functional object
MyObject.prototype.test = function() { alert('OK'); } // OK

这些幻灯片也帮了很大的忙。

83
John Resig有几张关于函数原型的幻灯片,这对我研究这个主题很有帮助(你也可以更改代码并查看发生了什么...)[http://ejohn.org/apps/learn/#64]。 - John Foster
5
非常好的参考资料,为了使这个问题的信息更加丰富,也许可以将约翰网站上的一些评论放到你的答案中,以防他的网站以一种导致你链接无法使用的方式发生变化。不管怎样,谢谢你的帮助。+1 - Chris
96
感谢您提供John Resig的JavaScript Ninja幻灯片第64页的链接,从那里开始学习对我非常有帮助,现在我感觉我已经正确理解了原型。+1 - a paid nerd
4
我们真的需要一个功能对象来应用原型吗?如果是,为什么? - Anshul
6
这可能会对你有所帮助:http://www.webdeveasy.com/javascript-prototype/ - Naor
显示剩余5条评论
26个回答

1833
在实现经典继承的编程语言中,比如Java、C#或C++,你需要创建一个类——一个对象的蓝图——然后你可以从该类创建新的对象,或者你可以扩展该类,定义一个增强原始类的新类。
而在JavaScript中,你首先创建一个对象(没有类的概念),然后你可以增强你自己的对象或从它创建新的对象。这并不难,但对于习惯了经典方式的人来说,这可能有些陌生和难以理解。
例如:

//Define a functional object to hold persons in JavaScript
var Person = function(name) {
  this.name = name;
};

//Add dynamically to the already defined object a new getter
Person.prototype.getName = function() {
  return this.name;
};

//Create a new object of type Person
var john = new Person("John");

//Try the getter
alert(john.getName());

//If now I modify person, also John gets the updates
Person.prototype.sayMyName = function() {
  alert('Hello, my name is ' + this.getName());
};

//Call the new method on john
john.sayMyName();

到目前为止,我一直在扩展基本对象,现在我创建另一个对象并从Person继承。

//Create a new object of type Customer by defining its constructor. It's not 
//related to Person for now.
var Customer = function(name) {
    this.name = name;
};

//Now I link the objects and to do so, we link the prototype of Customer to 
//a new instance of Person. The prototype is the base that will be used to 
//construct all new instances and also, will modify dynamically all already 
//constructed objects because in JavaScript objects retain a pointer to the 
//prototype
Customer.prototype = new Person();     

//Now I can call the methods of Person on the Customer, let's try, first 
//I need to create a Customer.
var myCustomer = new Customer('Dream Inc.');
myCustomer.sayMyName();

//If I add new methods to Person, they will be added to Customer, but if I
//add new methods to Customer they won't be added to Person. Example:
Customer.prototype.setAmountDue = function(amountDue) {
    this.amountDue = amountDue;
};
Customer.prototype.getAmountDue = function() {
    return this.amountDue;
};

//Let's try:       
myCustomer.setAmountDue(2000);
alert(myCustomer.getAmountDue());

var Person = function (name) {
    this.name = name;
};
Person.prototype.getName = function () {
    return this.name;
};
var john = new Person("John");
alert(john.getName());
Person.prototype.sayMyName = function () {
    alert('Hello, my name is ' + this.getName());
};
john.sayMyName();
var Customer = function (name) {
    this.name = name;
};
Customer.prototype = new Person();

var myCustomer = new Customer('Dream Inc.');
myCustomer.sayMyName();
Customer.prototype.setAmountDue = function (amountDue) {
    this.amountDue = amountDue;
};
Customer.prototype.getAmountDue = function () {
    return this.amountDue;
};
myCustomer.setAmountDue(2000);
alert(myCustomer.getAmountDue());

就像我所说的,我不能在一个人身上调用setAmountDue()和getAmountDue()。

//The following statement generates an error.
john.setAmountDue(1000);

357
我认为Stackoverflow上的答案不仅对原始发布者有趣,而且对许多其他潜在用户或通过搜索进入的人也很有用。我曾经是其中之一,并从旧帖子中受益匪浅。我认为我可以通过添加一些代码示例来为其他回答做出贡献。关于你的问题:如果你去掉“new”,它就不能正常工作。当我调用myCustomer.sayMyName()时,它会返回“myCustomer.sayMyName不是一个函数”。最简单的方法是用Firebug进行实验并观察发生了什么。 - stivlo
7
据我所了解,var Person = function(name) {...}; 定义了一个构造函数,能够构建Person对象。因此,现在还没有对象,只有匿名的构造函数被分配给了Person。这篇文章提供了非常好的解释:http://helephant.com/2008/08/how-javascript-objects-work/。 - stivlo
20
警告:此答案忽略了父类构造函数不是按照每个实例调用的事实。 它之所以有效,是因为他在子类和父类构造函数中都执行了完全相同的操作(设置名称)。 要获取有关在JavaScript中尝试继承时常见错误(以及最终解决方案)的更详细说明,请参阅: 此 Stack Overflow 帖子 - Aaren Cordova
14
关于Customer.prototype = new Person()这行代码,MDN网站提供了一个使用Customer.prototype = Object.create(Person.prototype)的例子,并指出“在这里经常犯的一个错误是使用“new Person()””来源 - Rafael Eyng

1062
每个 JavaScript 对象都有一个内部的“插槽”称为[[Prototype]], 它的值可以是null或者是一个object。你可以把插槽看作是对象上的一个属性,它是 JavaScript 引擎内部的,对于你编写的代码来说是隐藏的。方括号[[Prototype]]是故意这么写的,并且是 ECMAScript 规范约定的一种表示内部插槽的方式。
指向对象[[Prototype]]的值通俗地被称为“该对象的原型”。
如果你通过点(obj.propName)或方括号(obj['propName'])表示法访问一个属性,而该对象没有直接拥有这样的属性(即没有自己的属性,可通过obj.hasOwnProperty('propName')检查),运行时会在[[Prototype]]引用的对象上查找具有该名称的属性。如果[[Prototype]]也没有这样的属性,则依次检查它的[[Prototype]],以此类推。通过这种方式,原始对象的原型链被遍历,直到找到匹配项或到达其末尾。原型链的顶部是null值。
现代 JavaScript 实现允许以下方式读取和/或写入[[Prototype]]
  1. new运算符(在由构造函数返回的默认对象上配置原型链),
  2. extends关键字(使用类语法时配置原型链),
  3. Object.create将提供的参数设置为生成对象的[[Prototype]]
  4. Object.getPrototypeOfObject.setPrototypeOf(在对象创建后获取/设置[[Prototype]]),以及
  5. 标准化的访问器(即getter/setter)属性命名为__proto__(类似于4.)

Object.getPrototypeOfObject.setPrototypeOf优先于__proto__,部分原因是当一个对象具有null原型时,o.__proto__的行为很不寻常

对象的[[Prototype]]最初在对象创建时设置。

如果通过new Func()创建新对象,则该对象的[[Prototype]]默认设置为由Func.prototype引用的对象。

注意,因此所有类以及可以使用new运算符的所有函数,除了其自身的[[Prototype]]内部插槽之外,都有一个名为.prototype的属性。这个单词“prototype”的双重用途是初学者对该语言感到困惑的根源。
使用构造函数和new来模拟JavaScript中的经典继承;尽管JavaScript的继承系统 - 如我们所见 - 是基于原型而不是基于类的。
在引入类语法之前,构造函数是模拟类的唯一方法。我们可以将构造函数的.prototype属性引用的对象的属性视为共享成员;即对于每个实例都相同的成员。在基于类的系统中,方法对于每个实例都以相同的方式实现,因此方法在概念上被添加到.prototype属性;然而,对象的字段是特定于实例的,并且因此在构造期间添加到对象本身中。
没有类语法,开发人员必须手动配置原型链以实现类似经典继承的功能。这导致出现了大量实现此功能的不同方式。
以下是一种方法:
function Child() {}
function Parent() {}
Parent.prototype.inheritedMethod = function () { return 'this is inherited' }

function inherit(child, parent) {
  child.prototype = Object.create(parent.prototype)
  child.prototype.constructor = child
  return child;
}

Child = inherit(Child, Parent)
const o = new Child
console.log(o.inheritedMethod()) // 'this is inherited'

...还有另一种方式:

function Child() {}
function Parent() {}
Parent.prototype.inheritedMethod = function () { return 'this is inherited' }

function inherit(child, parent) {
    function tmp() {}
    tmp.prototype = parent.prototype
    const proto = new tmp()
    proto.constructor = child
    child.prototype = proto
    return child
}

Child = inherit(Child, Parent)
const o = new Child
console.log(o.inheritedMethod()) // 'this is inherited'

ES2015引入的类语法简化了事情,提供了extends作为在JavaScript中模拟经典继承的原型链配置的“唯一正确方法”。
因此,类似于上面的代码,如果您使用类语法创建一个新对象,如下所示:
class Parent { inheritedMethod() { return 'this is inherited' } }
class Child extends Parent {}

const o = new Child
console.log(o.inheritedMethod()) // 'this is inherited'

最终,如果您通过Object.create(foo)创建一个新对象,则该结果对象的[[Prototype]]将被设置为foo

结果对象的[[Prototype]]将被设置为Parent的实例,其[[Prototype]]反过来是Parent.prototype的实例。


198

这是一个非常简单的基于原型的对象模型,会在解释过程中作为示例使用,但尚未做出注释:

function Person(name){
    this.name = name;
}
Person.prototype.getName = function(){
    console.log(this.name);
}
var person = new Person("George");

在进入原型概念之前,我们必须考虑一些关键点。

1- JavaScript 函数的工作原理

首先我们要了解 JavaScript 函数的工作原理。它可以像一个带有this关键字的类函数一样使用,也可以像一个普通函数一样,接受参数并返回结果。

假设我们要创建一个Person对象模型,但在此步骤中,我将尝试不使用prototypenew关键字来完成相同的事情

所以在这一步中,我们只有函数对象this关键字可用。

第一个问题是没有使用new关键字,如何使用this关键字?

为了回答这个问题,假设我们有一个空对象和两个函数:

var person = {};
function Person(name){  this.name = name;  }

function getName(){
    console.log(this.name);
}

现在,不使用new关键字,我们如何使用这些功能。因此,JavaScript 有三种不同的方法:

a.第一种方法就是像普通函数一样调用该函数:

Person("George");
getName();//would print the "George" in the console

在这种情况下,它将成为当前的上下文对象,通常是浏览器中的全局window对象或者Node.js中的GLOBAL。这意味着我们会有window.name在浏览器中或GLOBAL.name在Node.js中,并且它的值为"George"。

b. 我们可以将它们作为对象的属性附加到对象上:

- 最简单的方法是修改空的person对象,例如:

person.Person = Person;
person.getName = getName;

这样我们就可以像这样调用它们:

person.Person("George");
person.getName();// -->"George"

现在 person 对象的内容如下:

Object {Person: function, getName: function, name: "George"}

-另一种将属性附加到对象的方法是使用该对象的prototype,可以在带有名称__proto__的任何JavaScript对象中找到,并且我已尝试在概述部分进行了一些解释。因此,我们可以通过执行以下操作获得类似的结果:

person.__proto__.Person = Person;
person.__proto__.getName = getName;

但是这样做实际上是修改了Object.prototype,因为每当我们使用字面量({...})创建JavaScript对象时,它都是基于Object.prototype创建的,这意味着它以一个名为__proto__的属性附加到新创建的对象上,因此如果我们像之前的代码片段那样对其进行更改,所有的JavaScript对象都会被更改,这不是一个好的实践。那么现在什么是更好的实践呢:

person.__proto__ = {
    Person: Person,
    getName: getName
};

现在其他对象都已经平静了,但这似乎仍然不是一个好的做法。所以我们还有一种解决方案,但要使用这个解决方案,我们应该返回到创建 person 对象的那行代码处(var person = {};),然后像这样进行更改:

var propertiesObject = {
    Person: Person,
    getName: getName
};
var person = Object.create(propertiesObject);

它所做的是创建一个新的JavaScript Object并将propertiesObject附加到__proto__属性上。因此,为了确保您可以这样做:

console.log(person.__proto__===propertiesObject); //true

然而,这里的棘手之处在于您可以访问person对象的第一级别中定义的__proto__的所有属性(请阅读摘要部分以获取更多详细信息)。


如您所见,使用这两种方法之一,this将完全指向person对象。

c. JavaScript还有另一种方式为函数提供this,即使用callapply来调用该函数。

apply() 方法接收数组参数,而 call() 方法接收一系列参数列表。

这种方法是我最喜欢的方式,我们可以轻松地调用我们的函数,例如:

Person.call(person, "George");
或者
//apply is more useful when params count is not fixed
Person.apply(person, ["George"]);

getName.call(person);   
getName.apply(person);

这3种方法是了解 .prototype 功能的重要初始步骤。


2- new 关键字是如何工作的?

这是理解 .prototype 功能的第二步。以下是我用来模拟这个过程的代码:

function Person(name){  this.name = name;  }
my_person_prototype = { getName: function(){ console.log(this.name); } };

在这部分中,我将尝试在不使用new关键字和prototype的情况下,模拟JavaScript使用new关键字时所执行的所有步骤。因此,当我们执行new Person("George")时,Person函数充当构造函数。以下是JavaScript一步一步执行的操作:

a.首先创建一个空对象,类似于一个空哈希表:

var newObject = {};
b. JavaScript接下来执行的操作是将所有原型对象附加到新创建的对象上。

这里我们有一个类似于原型对象的my_person_prototype

for(var key in my_person_prototype){
    newObject[key] = my_person_prototype[key];
}

JavaScript实际上附加在原型中定义的属性的方式并不是这样的。实际的方式与原型链概念有关。


a. & b. 可以通过以下方式来达到完全相同的结果,而不是这两个步骤:

var newObject = Object.create(my_person_prototype);
//here you can check out the __proto__ attribute
console.log(newObject.__proto__ === my_person_prototype); //true
//and also check if you have access to your desired properties
console.log(typeof newObject.getName);//"function"

现在我们可以在my_person_prototype中调用getName函数:

newObject.getName();

c. 然后将该对象传递给构造函数,

我们可以用示例进行如下操作:

Person.call(newObject, "George");
或者
Person.apply(newObject, ["George"]);

如果构造函数使用了 this 关键字,那么在构造函数内部,this 指向的就是刚刚创建的对象。现在在模拟其他步骤之前,得到的最终结果为:

Object {name: "George"}


摘要:

基本上,当您对一个函数使用 new 关键字时,您正在调用该函数并将其作为构造函数使用,因此当您说:

new FunctionName()
JavaScript内部创建一个空哈希对象,并将该对象传递给构造函数。构造函数内部的this指向刚刚创建的对象,因此构造函数可以对其进行任何操作,当然前提是您的函数体中没有使用return语句或者在函数末尾使用了return undefined;语句。
因此,当JavaScript在对象上查找属性时,首先在该对象上查找,然后查找一个密封属性[[prototype]],通常我们将其命名为__proto__。当它在__proto__上查找时,如果发现另一个JavaScript对象,则它也有自己的__proto__属性,依次类推,直到找到下一个__proto__为空的对象。这意味着在JavaScript中,唯一一个__proto__属性为空的对象就是Object.prototype对象。
console.log(Object.prototype.__proto__===null);//true

这就是JavaScript中继承的工作原理。

原型链

换句话说,当你在函数上有一个原型属性,并且你对其调用一个new操作符时,在JavaScript完成查找新创建的对象的属性后,它会去查找该函数的.prototype,而且这个对象可能有自己的内部原型。依此类推。


82

prototype允许您创建类。 如果您不使用prototype,则它将成为静态的。

这里是一个简短的示例。

var obj = new Object();
obj.test = function() { alert('Hello?'); };
在上面的例子中,您有静态函数调用test。这个函数只能通过obj.test访问,其中可以想象obj是一个类。
而在下面的代码中:
function obj()
{
}

obj.prototype.test = function() { alert('Hello?'); };
var obj2 = new obj();
obj2.test();

obj已经成为一个可以实例化的类。可以存在多个obj实例,它们都具有“test”函数。

以上是我的理解。我将其作为社区维基,以便人们可以纠正我如果我错了。


13
“prototype”是构造函数的属性,而不是实例的属性。也就是说,你的代码是错误的!也许你想指的是对象的非标准属性“__proto__”,但那是完全不同的东西... - Christoph

81

原型的七个公案

Ciro San深思熟虑后从火狐山下来,心境清晰宁静。

然而他的手却不安分,自行拿起一支笔,并记录下以下笔记。


0) “原型”这个词可以指两个不同的东西:

  • 原型属性,例如 obj.prototype

  • 原型内部属性,表示为 [[Prototype]] 在 ES5 中

    可以通过 ES5 的 Object.getPrototypeOf() 方法获取它。

    Firefox 通过扩展将其作为 __proto__ 属性访问。ES6 现在提到了 对于 __proto__ 的一些可选要求。


1) 这些概念存在是为了回答以下问题:

当我执行 obj.property 时,JS 在哪里查找 .property

直觉上,经典继承应该会影响属性查找。


2)

  • __proto__用于点.属性查找,如obj.property
  • .prototype不直接用于查找,只在对象创建时间接地确定__proto__

查找顺序为:

  • 使用obj.p = ...Object.defineProperty(obj, ...)添加的obj属性
  • obj.__proto__的属性
  • obj.__proto__.__proto__的属性,以此类推
  • 如果某个__proto__null,则返回undefined

这就是所谓的原型链

您可以使用obj.hasOwnProperty('key')Object.getOwnPropertyNames(f)避免.查找。


3) 设置obj.__proto__有两种主要方式:

  • new:

    var F = function() {}
    var f = new F()
    

    then new has set:

    f.__proto__ === F.prototype
    

    This is where .prototype gets used.

  • Object.create:

     f = Object.create(proto)
    

    sets:

    f.__proto__ === proto
    

4) 代码:

var F = function(i) { this.i = i }
var f = new F(1)

对应以下图表(省略了一些数字内容):

(Function)       (  F  )                                      (f)----->(1)
 |  ^             | | ^                                        |   i    |
 |  |             | | |                                        |        |
 |  |             | | +-------------------------+              |        |
 |  |constructor  | |                           |              |        |
 |  |             | +--------------+            |              |        |
 |  |             |                |            |              |        |
 |  |             |                |            |              |        |
 |[[Prototype]]   |[[Prototype]]   |prototype   |constructor   |[[Prototype]]
 |  |             |                |            |              |        |
 |  |             |                |            |              |        |
 |  |             |                | +----------+              |        |
 |  |             |                | |                         |        |
 |  |             |                | | +-----------------------+        |
 |  |             |                | | |                                |
 v  |             v                v | v                                |
(Function.prototype)              (F.prototype)                         |
 |                                 |                                    |
 |                                 |                                    |
 |[[Prototype]]                    |[[Prototype]]          [[Prototype]]|
 |                                 |                                    |
 |                                 |                                    |
 | +-------------------------------+                                    |
 | |                                                                    |
 v v                                                                    v
(Object.prototype)                                       (Number.prototype)
 | | ^
 | | |
 | | +---------------------------+
 | |                             |
 | +--------------+              |
 |                |              |
 |                |              |
 |[[Prototype]]   |constructor   |prototype
 |                |              |
 |                |              |
 |                | -------------+
 |                | |
 v                v |
(null)           (Object)

This diagram illustrates several pre-defined object nodes in programming languages:
  • null
  • Object
  • Object.prototype
  • Function
  • Function.prototype
  • 1
  • Number.prototype (可以通过使用 (1).__proto__ 找到,必须加括号以满足语法要求)
Our two lines of code only created the following new objects:
  • f
  • F
  • F.prototype

i现在是f的一个属性,因为当你执行以下操作时:

var f = new F(1)

它使用this作为new将返回的值来评估F,然后将其分配给f

5) .constructor 通常通过 . 查找从F.prototype中获取:

f.constructor === F
!f.hasOwnProperty('constructor')
Object.getPrototypeOf(f) === F.prototype
F.prototype.hasOwnProperty('constructor')
F.prototype.constructor === f.constructor

当我们写f.constructor时,JavaScript会进行以下.查找:
  • f没有.constructor
  • f.__proto__ === F.prototype.constructor === F,所以使用它
结果f.constructor == F直观上是正确的,因为F用于构造f,例如设置字段,就像在经典面向对象编程语言中一样。

6) 通过操作原型链,可以实现经典的继承语法。

ES6 添加了 classextends 关键字,这主要是以前可能存在的原型操作疯狂的语法糖。

class C {
    constructor(i) {
        this.i = i
    }
    inc() {
        return this.i + 1
    }
}

class D extends C {
    constructor(i) {
        super(i)
    }
    inc2() {
        return this.i + 2
    }
}

// Inheritance syntax works as expected.
c = new C(1)
c.inc() === 2
(new D(1)).inc() === 2
(new D(1)).inc2() === 3

// "Classes" are just function objects.
C.constructor === Function
C.__proto__ === Function.prototype
D.constructor === Function
// D is a function "indirectly" through the chain.
D.__proto__ === C
D.__proto__.__proto__ === Function.prototype

// "extends" sets up the prototype chain so that base class
// lookups will work as expected
var d = new D(1)
d.__proto__ === D.prototype
D.prototype.__proto__ === C.prototype
// This is what `d.inc` actually does.
d.__proto__.__proto__.inc === C.prototype.inc

// Class variables
// No ES6 syntax sugar apparently:
// https://dev59.com/YGEh5IYBdhLWcg3wVCPK
C.c = 1
C.c === 1
// Because `D.__proto__ === C`.
D.c === 1
// Nothing makes this work.
d.c === undefined

简化的图示,没有预定义的对象:

(c)----->(1)
 |   i
 |
 |
 |[[Prototype]]
 |
 |
 v    __proto__
(C)<--------------(D)         (d)
| |                |           |
| |                |           |
| |prototype       |prototype  |[[Prototype]] 
| |                |           |
| |                |           |
| |                | +---------+
| |                | |
| |                | |
| |                v v
|[[Prototype]]    (D.prototype)--------> (inc2 function object)
| |                |             inc2
| |                |
| |                |[[Prototype]]
| |                |
| |                |
| | +--------------+
| | |
| | |
| v v
| (C.prototype)------->(inc function object)
|                inc
v
Function.prototype

让我们花一点时间来研究以下内容的工作原理:

c = new C(1)
c.inc() === 2

第一行代码将c.i设置为1,如“4)”所述。
在第二行中,当我们执行:
c.inc()
  • .inc可以通过原型链找到:c -> C -> C.prototype -> inc
  • 当我们在JavaScript中调用函数X.Y()时,JavaScript会自动将this设置为X,并在Y()函数调用中使用!

相同的逻辑也解释了d.incd.inc2

这篇文章https://javascript.info/class#not-just-a-syntax-sugar提到了值得知道的class进一步影响。其中一些可能无法在没有class关键字的情况下实现(TODO检查哪些):


69

阅读完这篇文章后,我对 JavaScript 的原型链感到困惑,然后我发现了这些图表:

http://iwiki.readthedocs.org/en/latest/javascript/js_core.html#inheritance *[[protytype]]* and <code>prototype</code> property of function objects

这是一个清晰的图表,展示了 JavaScript 通过原型链实现的继承关系。

以及:

http://www.javascriptbank.com/javascript/article/JavaScript_Classical_Inheritance/

这个网站包含一个带有代码和几个漂亮图表的示例。

原型链最终回溯到 Object.prototype。

原型链可以在技术上无限扩展,每次都将子类的原型设置为父类对象。

希望这也有助于您理解 JavaScript 原型链。


42

每个对象都有一个内部属性[[Prototype]],将其链接到另一个对象:

object [[Prototype]] → anotherObject
在传统的 JavaScript 中,关联的对象是函数的 prototype 属性:
object [[Prototype]] → aFunction.prototype

一些环境将 [[Prototype]] 暴露为 __proto__

anObject.__proto__ === anotherObject

创建对象时,会同时创建[[Prototype]]链接。

// (1) Object.create:
var object = Object.create(anotherObject)
// object.__proto__ = anotherObject

// (2) ES6 object initializer:
var object = { __proto__: anotherObject };
// object.__proto__ = anotherObject

// (3) Traditional JavaScript:
var object = new aFunction;
// object.__proto__ = aFunction.prototype

所以这些语句是等价的:

var object = Object.create(Object.prototype);
var object = { __proto__: Object.prototype }; // ES6 only
var object = new Object;

在使用new语句时,你实际上看不到链接目标(Object.prototype);相反,目标是由构造函数(Object)隐含的。

请记住:

  • 每个对象都有一个连接,[[Prototype]],有时被公开为__proto__
  • 每个函数都有一个prototype属性,最初保存为空对象。
  • 使用new创建的对象与其构造函数的prototype属性相关联。
  • 如果一个函数从来没有被用作构造函数,它的prototype属性将不会被使用。
  • 如果你不需要构造函数,请使用Object.create代替new

1
第5个版本删除了一些有用的信息,包括关于Object.create()的信息。请参见第4个版本 - Palec
@Palec 我应该添加什么? - sam
2
在我看来,至少要包含到Object.create()文档,@sam。同时,包含到__proto__Object.prototype的链接也是不错的补充。我喜欢你关于原型如何与构造函数和Object.create()一起使用的例子,但它们可能是你想要摆脱的冗长且不太相关的部分。 - Palec
根据所有讨论的结论(源自经典继承),如果我创建构造函数并尝试使用 new 运算符创建其实例,则只会获得附加到 proto 对象的方法和属性,因此如果我们想要继承,则必须将所有方法和属性附加到 proto 对象。我的理解是否正确? - blackHawk

31

在通常意义下,Javascript 没有继承机制,但是它有原型链(prototype chain)。

原型链

如果一个对象的成员在对象中找不到,它会在原型链中查找。原型链由其他对象组成。可以通过 __proto__ 变量访问给定实例的原型。在 Javascript 中,类和实例之间没有区别,因此每个对象都有一个。

将函数/变量添加到原型中的优点是,它只需在内存中存在一次,而不是每个实例都需要一次。

这也对继承很有用,因为原型链可以包含许多其他对象。


1
FF和Chrome支持__proto__,但IE和Opera不支持。 - some
Georg,请为新手澄清一下 - “在JavaScript中,类和实例之间没有区别。” - 你能详细说明一下吗?这是如何工作的? - Hamish Grubijan
从所有的讨论中,我得到了这样一个结论(来自经典继承):如果我创建构造函数并尝试使用 new 运算符创建它的实例,那么我只会得到附加到原型对象的方法和属性,因此,如果我们想要继承,就必须将所有的方法和属性都附加到原型对象上,我是对的吗? - blackHawk

28

本文篇幅较长,但我相信它会解答你关于JavaScript继承“原型”特性的大部分疑问,甚至更多。请完整阅读文章。

JavaScript基本上有两种数据类型

  • 非对象
  • 对象

非对象

以下是非对象数据类型

  • 字符串
  • 数字(包括NaN和Infinity)
  • 布尔值(true、false)
  • 未定义(undefined)

当你使用typeof运算符时,这些数据类型返回以下结果

typeof "字符串文字"(或包含字符串文字的变量) === 'string'

typeof 5(或任何数值文字、包含数值文字的变量,或NaN或Infynity) === 'number'

typeof true(或false、包含truefalse的变量) === 'boolean'

typeof undefined(或一个未定义的变量、包含undefined的变量) === 'undefined'

字符串数字布尔数据类型既可以表示为对象,也可以表示为非对象。当它们表示为对象时,它们的typeof始终为'object'。一旦我们了解了对象数据类型,我们将回到这个问题。

对象

对象数据类型可以进一步分为两种类型

函数类型对象
使用 typeof 操作符返回字符串'function'的对象称为函数类型对象。 所有用户定义的函数和所有能够使用new运算符创建新对象的JavaScript内置对象都属于此类别。例如:
  • Object
  • String
  • Number
  • Boolean
  • Array
  • Typed Arrays
  • RegExp
  • Function
  • 所有其他能够使用new运算符创建新对象的内置对象
  • function UserDefinedFunction(){ /*user defined code */ }
因此, typeof(Object) === typeof(String) === typeof(Number) === typeof(Boolean) === typeof(Array) === typeof(RegExp) === typeof(Function) === typeof(UserDefinedFunction) === 'function' 所有函数类型对象实际上是内置JavaScript对象 Function的实例(包括Function对象,即递归定义)。这就好像这些对象已经以以下方式定义:

var Object= new Function ([native code for object Object])
var String= new Function ([native code for object String])
var Number= new Function ([native code for object Number])
var Boolean= new Function ([native code for object Boolean])
var Array= new Function ([native code for object Array])
var RegExp= new Function ([native code for object RegExp])
var Function= new Function ([native code  for object Function])
var UserDefinedFunction= new Function ("user defined code")

如先前所述,函数类型对象可使用new操作符进一步创建新对象。例如,可以使用它创建类型为ObjectStringNumberBooleanArrayRegExpUserDefinedFunction的对象。

var a=new Object() or var a=Object() or var a={} //Create object of type Object
var a=new String() //Create object of type String
var a=new Number() //Create object of type Number
var a=new Boolean() //Create object of type Boolean
var a=new Array() or var a=Array() or var a=[]  //Create object of type Array
var a=new RegExp() or var a=RegExp() //Create object of type RegExp
var a=new UserDefinedFunction() 

因此创建的对象都是非函数类型对象,并且返回它们的typeof==='object'。在所有这些情况下,对象"a"不能使用运算符new进一步创建对象。因此下面是错误的

var b=new a() //error. a is not typeof==='function'

内置对象Math'object'类型的,因此不能使用new运算符创建Math对象。

var b=new Math() //error. Math is not typeof==='function'

还要注意ObjectArrayRegExp函数可以创建一个新的对象,甚至不需要使用operator new。但是以下这些函数则不行。

var a=String() // Create a new Non Object string. returns a typeof==='string' 
var a=Number() // Create a new Non Object Number. returns a typeof==='number'
var a=Boolean() //Create a new Non Object Boolean. returns a typeof==='boolean'
用户自定义函数是一种特殊情况。
var a=UserDefinedFunction() //may or may not create an object of type UserDefinedFunction() based on how it is defined.

函数类型对象可以创建新对象,因此它们也被称为构造函数

每个构造函数/函数(不管是内置还是用户定义的)在定义时都会自动具有一个名为"prototype"的属性,其默认值设置为一个对象。这个对象本身有一个名为"constructor"的属性,默认情况下指向构造函数/函数本身。

例如,当我们定义一个函数:

function UserDefinedFunction()
{
}

以下内容会自动发生

UserDefinedFunction.prototype={constructor:UserDefinedFunction}

这个"prototype"属性仅存在于函数类型对象中(而不会存在于非函数类型对象中)。

这是因为当使用new运算符创建一个新对象时,它会继承当前构造函数的原型对象的所有属性和方法,即在新创建的对象中创建了一个内部引用,该引用指向构造函数的当前原型对象所引用的对象。

在对象中创建的这个内部引用用于引用继承的属性,称为对象的原型(它引用构造函数的"prototype"属性所引用的对象,但与之不同)。可以使用Object.getPrototypeOf()方法检索任何对象(函数或非函数)的原型。使用此方法,可以跟踪对象的原型链。

此外,每个创建的对象(包括函数类型非函数类型)都有一个从构造函数的原型属性所引用的对象继承而来的"constructor"属性。默认情况下,此"constructor"属性引用创建它的构造函数(如果构造函数的默认"prototype"未更改)。

对于所有函数类型对象,构造函数始终是function Function(){}

对于非函数类型对象(例如 JavaScript 内置 Math 对象),构造函数是创建它的函数。对于Math对象,它是function Object(){}

如果没有任何支持代码,上述所有概念可能有点难以理解。请逐行查看以下代码以理解这些概念。尝试执行代码以获得更好的理解。

function UserDefinedFunction()
{ 

} 

/* creating the above function automatically does the following as mentioned earlier

UserDefinedFunction.prototype={constructor:UserDefinedFunction}

*/


var newObj_1=new UserDefinedFunction()

alert(Object.getPrototypeOf(newObj_1)===UserDefinedFunction.prototype)  //Displays true

alert(newObj_1.constructor) //Displays function UserDefinedFunction

//Create a new property in UserDefinedFunction.prototype object

UserDefinedFunction.prototype.TestProperty="test"

alert(newObj_1.TestProperty) //Displays "test"

alert(Object.getPrototypeOf(newObj_1).TestProperty)// Displays "test"

//Create a new Object

var objA = {
        property1 : "Property1",
        constructor:Array

}


//assign a new object to UserDefinedFunction.prototype
UserDefinedFunction.prototype=objA

alert(Object.getPrototypeOf(newObj_1)===UserDefinedFunction.prototype)  //Displays false. The object referenced by UserDefinedFunction.prototype has changed

//The internal reference does not change
alert(newObj_1.constructor) // This shall still Display function UserDefinedFunction

alert(newObj_1.TestProperty) //This shall still Display "test" 

alert(Object.getPrototypeOf(newObj_1).TestProperty) //This shall still Display "test"


//Create another object of type UserDefinedFunction
var newObj_2= new UserDefinedFunction();

alert(Object.getPrototypeOf(newObj_2)===objA) //Displays true.

alert(newObj_2.constructor) //Displays function Array()

alert(newObj_2.property1) //Displays "Property1"

alert(Object.getPrototypeOf(newObj_2).property1) //Displays "Property1"

//Create a new property in objA
objA.property2="property2"

alert(objA.property2) //Displays "Property2"

alert(UserDefinedFunction.prototype.property2) //Displays "Property2"

alert(newObj_2.property2) // Displays Property2

alert(Object.getPrototypeOf(newObj_2).property2) //Displays  "Property2"
每个对象的原型链最终都追溯到Object.prototype(它本身没有任何原型对象)。 以下代码可用于跟踪对象的原型链。

每个对象的原型链最终都追溯到Object.prototype(它本身没有任何原型对象)。 以下代码可用于跟踪对象的原型链。

var o=Starting object;

do {
    alert(o + "\n" + Object.getOwnPropertyNames(o))

}while(o=Object.getPrototypeOf(o))

各种对象的原型链如下所示。

  • 每个函数对象(包括内置函数对象)-> Function.prototype -> Object.prototype -> null
  • 简单对象(通过new Object()或{}创建,包括内置的Math对象)-> Object.prototype -> null
  • 使用new或Object.create创建的对象->一个或多个原型链-> Object.prototype -> null

要创建一个没有任何原型的对象,请使用以下代码:

var o=Object.create(null)
alert(Object.getPrototypeOf(o)) //Displays null

有人可能认为将构造函数的原型属性设置为null会创建一个原型为null的对象。然而,在这种情况下,新创建的对象的原型被设置为Object.prototype,并且它的构造函数被设置为Object函数。下面的代码演示了这一点:

function UserDefinedFunction(){}
UserDefinedFunction.prototype=null// Can be set to any non object value (number,string,undefined etc.)

var o=new UserDefinedFunction()
alert(Object.getPrototypeOf(o)==Object.prototype)   //Displays true
alert(o.constructor)    //Displays Function Object

以下是本文摘要:

  • 有两种类型的对象函数类型非函数类型
  • 只有函数类型对象可以使用operator new创建新对象。因此创建的对象是非函数类型对象。 非函数类型对象无法使用operator new进一步创建对象。

  • 所有函数类型对象默认都有一个"prototype"属性。此"prototype"属性引用一个具有默认"constructor"属性的对象,该属性默认引用函数类型对象本身。

  • 所有对象(函数类型非函数类型)都有一个"constructor"属性,默认引用创建它的函数类型对象/构造函数

  • 每个被创建的对象内部都引用创建它的构造函数的"prototype"属性引用的对象。此对象称为已创建对象的原型(与函数类型对象的"prototype"属性不同)。这样创建的对象可以直接访问构造函数的"prototype"属性引用的对象中定义的方法和属性(在对象创建时)。

  • 可以使用Object.getPrototypeOf()方法检索对象原型(及其继承的属性名称)。实际上,此方法可用于导航对象的整个原型链。

  • 每个对象的原型链最终都追溯到Object.prototype(除非使用Object.create(null)创建对象,在这种情况下,对象没有原型)。

  • typeof(new Array())==='object'是语言设计的结果,不是Douglas Crockford指出的错误。

  • 将构造函数的原型属性设置为null(或undefined,number,true,false,string)不会创建一个具有null原型的对象。在这种情况下,新创建的对象的原型被设置为Object.prototype,并且其构造函数被设置为函数Object。

希望这可以帮助你。


27
可能有助于将原型链分为两类。考虑构造函数:

 function Person() {}
Object.getPrototypeOf(Person)的值是一个函数。实际上,它是Function.prototype。由于Person是作为一个函数创建的,因此它共享所有函数具有的相同原型函数对象。它与Person.__proto__相同,但不应使用该属性。无论如何,使用Object.getPrototypeOf(Person),您实际上正在沿着所谓的原型链向上走。
向上的链看起来像这样:
 PersonFunction.prototypeObject.prototype(终点)
重要的是,这个原型链与Person可以构造的对象几乎没有关系。这些构造出的对象有自己的原型链,这个链可能与上面提到的那个链没有紧密的共同祖先。
例如,考虑以下对象:
var p = new Person();

pPerson之间没有直接的原型链关系。它们之间的关系是不同的。对象p有自己的原型链。使用Object.getPrototypeOf,您会发现链如下:

    pPerson.prototypeObject.prototype(终点)

这个链中没有函数对象(虽然可以有)。

因此,Person似乎与两种链相关联,它们各自过着自己的生活。要从一条链“跳”到另一条链,您可以使用:

  1. .prototype:从构造函数的链跳转到创建的对象的链。因此,此属性仅对函数对象定义(因为new只能用于函数)。

  2. .constructor:从创建的对象的链跳转到构造函数的链。

这里是涉及的两个原型链的可视化呈现,表示为列:

enter image description here

总结一下:

prototype属性不提供主体原型链的信息,而是提供由主体创建的对象的信息。

不难发现,属性prototype的名称可能会导致混淆。如果该属性被命名为prototypeOfConstructedInstances或类似的名称,则可能会更清晰明了。

你可以在两个原型链之间来回跳转:

Person.prototype.constructor === Person

这种对称性可以通过显式地为 prototype 属性分配不同的对象来打破(稍后会详细介绍)。
创建一个函数,获得两个对象 Person.prototype 是在创建函数 Person 的同时创建的一个对象。它将 Person 作为构造函数,尽管该构造函数实际上还没有执行。因此,在同一时间创建了两个对象:
1. 函数 Person 本身 2. 在调用函数时将充当原型的对象
两者都是对象,但它们有不同的角色:函数对象 构造,而另一个对象表示该函数将构造的任何对象的原型。原型对象将成为其原型链中构造对象的父级。
由于函数也是对象,因此它在自己的原型链中也有自己的父级,但请记住,这两个链涉及不同的事物。
以下是一些等式,可以帮助理解这个问题——所有这些都打印出 true

function Person() {};

// This is prototype chain info for the constructor (the function object):
console.log(Object.getPrototypeOf(Person) === Function.prototype);
// Step further up in the same hierarchy:
console.log(Object.getPrototypeOf(Function.prototype) === Object.prototype);
console.log(Object.getPrototypeOf(Object.prototype) === null);
console.log(Person.__proto__ === Function.prototype);
// Here we swap lanes, and look at the constructor of the constructor
console.log(Person.constructor === Function);
console.log(Person instanceof Function);

// Person.prototype was created by Person (at the time of its creation)
// Here we swap lanes back and forth:
console.log(Person.prototype.constructor === Person);
// Although it is not an instance of it:
console.log(!(Person.prototype instanceof Person));
// Instances are objects created by the constructor:
var p = new Person();
// Similarly to what was shown for the constructor, here we have
// the same for the object created by the constructor:
console.log(Object.getPrototypeOf(p) === Person.prototype);
console.log(p.__proto__ === Person.prototype);
// Here we swap lanes, and look at the constructor
console.log(p.constructor === Person);
console.log(p instanceof Person);

向原型链中添加级别

尽管在创建构造函数时会创建一个原型对象,但您可以忽略该对象,并分配另一个应用于由该构造函数创建的任何后续实例的原型对象。

例如:

function Thief() { }
var p = new Person();
Thief.prototype = p; // this determines the prototype for any new Thief objects:
var t = new Thief();

现在,t的原型链比p长一步:
 tpPerson.prototypeObject.prototype (终点)
另一个原型链不再更长: ThiefPerson 是兄弟姐妹,在它们的原型链中共享同一个父对象:
 Person}
 Thief } → Function.prototypeObject.prototype (终点)
前面提供的图示可以扩展到这个(原始的Thief.prototype被省略):

enter image description here

蓝色线条代表原型链,其他颜色的线条代表其他关系:
  • 对象与其构造函数之间的关系
  • 构造函数与用于构造对象的原型对象之间的关系

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接