JavaScript .prototype 是如何工作的?

我不是那么喜欢动态编程语言,但我已经写了相当多的JavaScript代码。我从来没有真正了解过这个基于原型的编程,有人知道这是如何工作的吗?

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

我记得不久前我和人们讨论过很多(我不太确定我在做什么),但据我所知,没有课程的概念。它只是一个对象,这些对象的实例是原始对象的克隆,对吧?

但是,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

此外,这些幻灯片确实帮助很大。


答案 1

在实现经典继承(如 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());

虽然如前所述,我不能在Person上调用setAmountDue(),但getAmountDue()。

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

答案 2

每个 JavaScript 对象都有一个名为 的内部“槽”,其值为 或 .您可以将槽视为对象上的一个属性,该属性位于 JavaScript 引擎内部,隐藏在您编写的代码中。周围的方括号是经过深思熟虑的,是表示内部插槽的 ECMAScript 规范约定。[[Prototype]]nullobject[[Prototype]]

对象所指向的值,俗称“该对象的原型”。[[Prototype]]

如果通过点 () 或方括号 () 表示法访问某个属性,并且该对象不直接具有此类属性(即自己的属性,可通过 检查),则运行时会在 所引用的对象上查找具有该名称的属性。如果 也没有这样的属性,则依次检查它,依此类推。通过这种方式,原始对象的原型链被移动,直到找到匹配项,或者达到其终点。原型链的顶部是价值。obj.propNameobj['propName']obj.hasOwnProperty('propName')[[Prototype]][[Prototype]][[Prototype]]null

现代 JavaScript 实现允许通过以下方式对 进行读取和/或写入访问:[[Prototype]]

  1. 运算符(在从构造函数返回的默认对象上配置原型链),new
  2. 关键字(使用类语法时配置原型链),extends
  3. Object.create将提供的参数设置为结果对象的参数,[[Prototype]]
  4. Object.getPrototypeOf和(在对象创建获取/设置),以及Object.setPrototypeOf[[Prototype]]
  5. 命名的标准化访问器(即 getter/setter)属性(类似于 4)。__proto__

Object.getPrototypeOf并且优先于 ,部分原因是 当对象具有 的原型时,的行为是不寻常的Object.setPrototypeOf__proto__o.__proto__null

对象最初是在对象创建期间设置的。[[Prototype]]

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

因此,请注意,所有类和可以与 new 运算符一起使用的所有函数,除了它们自己的 [[Prototype]] 内部槽外,还具有名为 .prototype 的属性。“原型”这个词的这种双重使用是该语言新手无休止的混乱的根源。

与构造函数一起使用允许我们在JavaScript中模拟经典继承;尽管正如我们所看到的,JavaScript的继承系统是原型的,而不是基于类的。new

在将类语法引入 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 中引入的类语法简化了事情,它提供了配置原型链的“一种真正方法”,以便在 JavaScript 中模拟经典继承。extends

因此,与上面的代码类似,如果您使用类语法来创建一个新对象,如下所示:

class Parent { inheritedMethod() { return 'this is inherited' } }
class Child extends Parent {}

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

...生成的对象将设置为 的实例,其实例又为 。[[Prototype]]Parent[[Prototype]]Parent.prototype

最后,如果通过 创建新对象,则生成的对象将设置为 。Object.create(foo)[[Prototype]]foo