JavaScript .prototype 如何工作?

IT技术 javascript dynamic-languages prototype-oriented
2020-12-24 01:26:14

我不喜欢动态编程语言,但我已经编写了相当多的 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

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

6个回答

在 Java、C# 或 C++ 等实现经典继承的语言中,您首先创建一个类——对象的蓝图——然后您可以从该类创建新对象,或者您可以扩展该类,定义一个新类来扩充原来的class。

在 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());

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

//The following statement generates an error.
john.setAmountDue(1000);
警告:这个答案忽略了父类构造函数不是在每个实例的基础上调用的事实。它起作用的唯一原因是因为他在子构造函数和父构造函数中做了完全相同的事情(设置名称)。有关在 JavaScript 中尝试继承时常见错误的更深入解释(以及最终解决方案),请参阅: this stack overflow post
2021-02-19 01:26:14
我认为 stackoverflow 上的答案不仅对原始海报很有趣,而且对潜伏或来自搜索的其他人的大社区也很有趣。我一直是其中之一,我从旧帖子中受益。我想我可以为其他答案添加一些代码示例。关于您的问题:如果您遗漏新的,则不起作用。当我调用 myCustomer.sayMyName() 时,它返回“myCustomer.sayMyName 不是函数”。最简单的方法是试验萤火虫,看看会发生什么。
2021-02-26 01:26:14
关于该Customer.prototype = new Person();行,MDN 显示了一个使用 的示例Customer.prototype = Object.create(Person.prototype),并指出'这里的一个常见错误是使用 "new Person()"'来源
2021-03-07 01:26:14
据我了解 var Person = function (name) {...}; 正在定义一个能够构建 Person 对象的构造函数。所以目前还没有Object,只有匿名构造函数被分配给Person。这是一个很好的解释: helephant.com/2008/08/how-javascript-objects-work
2021-03-08 01:26:14

每JavaScript对象具有一个内部“时隙”称为[[Prototype]]其值是任一null或一个object您可以将插槽视为对象上的一个属性,位于 JavaScript 引擎内部,对您编写的代码隐藏。周围的方括号[[Prototype]]是故意的,是 ECMAScript 规范约定,用于表示内部插槽。

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

如果您通过点 ( obj.propName) 或方括号 ( obj['propName']) 表示法访问属性,并且该对象不直接具有这样的属性(即自己的属性,可通过 进行检查obj.hasOwnProperty('propName')),则运行时会在引用的对象上查找具有该名称的属性由[[Prototype]]代替。如果[[Prototype]] 没有这样的属性,[[Prototype]]则依次检查,依此类推。通过这种方式,原始对象的原型链被遍历,直到找到匹配项,或者到达其末尾。原型链的顶端是nullvalue。

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

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

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

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

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

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

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

在 JavaScript 引入类语法之前,构造函数是模拟类的唯一方法。我们可以将构造函数的.prototype属性所引用的对象的属性视为共享成员;IE。每个实例都相同的成员。在基于类的系统中,每个实例的方法都以相同的方式实现,因此在概念上将方法添加到.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'

...生成的对象[[Prototype]]将被设置为 的一个实例Parent,而它的 实例[[Prototype]]又是Parent.prototype

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

这是一个非常简单的基于原型的对象模型,在解释过程中将被视为示例,尚无评论:

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

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

1- JavaScript 函数的实际工作方式:

为了迈出第一步,我们必须弄清楚 JavaScript 函数实际上是如何工作的,作为一个类,比如this在其中使用关键字的函数,或者只是作为一个带有参数的常规函数​​,它做什么和返回什么。

假设我们要创建一个Person对象模型。但在这一步中,我将尝试在不使用prototypeandnew关键字的情况下做同样的事情

因此,在这一步functionsobjectsthis关键字,都是我们。

第一个问题是不使用keyword 的情况下如何this使用newkeyword

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

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

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

现在不使用new关键字我们如何使用这些功能。所以 JavaScript 有 3 种不同的方法来做到这一点:

一种。第一种方法只是将该函数作为常规函数调用:

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

在这种情况下,这将是当前上下文对象,这通常是全球 window在浏览器或对象GLOBALNode.js这意味着我们将在浏览器中使用 window.name 或在 Node.js 中使用 GLOBAL.name,并将“George”作为其值。

我们可以它们附加到一个对象上,作为它的属性

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

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

这样我们就可以这样称呼它们:

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

现在person对象是这样的:

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

-将属性附加到对象的另一种方法是使用prototype可以在任何名称为 的 JavaScript 对象中找到的对象的__proto__,我试图在摘要部分对其进行一些解释。因此,我们可以通过执行以下操作来获得类似的结果:

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);

它所做的是创建一个新的 JavaScriptObject并将 附加propertiesObject__proto__属性。因此,为了确保您可以执行以下操作:

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

但这里的棘手之处在于您可以访问__proto__person对象的第一级中定义的所有属性(阅读摘要部分了解更多详细信息)。


如您所见,使用这两种方式中的任何一种this都将准确指向person对象。

C。JavaScript 有另一种为函数提供 的方法this,即使用callapply来调用函数。

apply() 方法使用给定的 this 值和作为数组(或类似数组的对象)提供的参数调用函数。

call() 方法使用给定的 this 值和单独提供的参数调用函数。

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

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); } };

在这一部分中,当您使用关键字,我将尝试执行 JavaScript 所采取的所有步骤,而不使用new关键字 and 所以当我们这样做的时候函数充当了一个构造函数,这些是 JavaScript 所做的,一一:prototypenewnew Person("George")Person

一种。首先,它创建一个空对象,基本上是一个空哈希,如:

var newObject = {};

JavaScript 采取的下一步是所有原型对象附加到新创建的对象

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

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

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


一种。& 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"

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

newObject.getName();

C。然后它将该对象提供给构造函数,

我们可以使用我们的示例来做到这一点,例如:

Person.call(newObject, "George");

或者

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

然后构造函数可以做任何它想做的事情,因为构造函数内部的this是刚刚创建的对象。

现在模拟其他步骤之前的最终结果:对象{名称:“乔治”}


概括:

基本上,当您在函数上使用new关键字时,您是在调用它,并且该函数充当构造函数,因此当您说:

new FunctionName()

JavaScript 在内部创建一个对象,一个空的散列,然后将该对象提供给构造函数,然后构造函数可以做任何它想做的事情,因为构造函数的内部是刚刚创建的对象,然后它当然会给你那个对象如果您没有在函数中使用 return 语句,或者您return undefined;在函数体的末尾放置了 a

因此,当 JavaScript 在对象上查找属性时,它所做的第一件事就是在该对象上查找它。然后有一个[[prototype]]我们通常拥有的秘密属性__proto__该属性是 JavaScript 下一步要查看的内容。当它查看 时__proto__,只要它又是另一个 JavaScript 对象,它就有自己的__proto__属性,它会一直向上,直到到达下一个__proto__为 null的点重点是 JavaScript 中唯一一个其__proto__属性为 null 的Object.prototype对象object:

console.log(Object.prototype.__proto__===null);//true

这就是继承在 JavaScript 中的工作方式。

原型链

换句话说,当你在一个函数上有一个原型属性并且你调用一个 new 时,在 JavaScript 完成查看新创建的属性对象后,它会查看函数的,.prototype并且这个对象也可能有它的自己的内部原型。等等。

原型的七个公案

深思熟虑后,西罗三下火狐山,心神清明。

然而他的手却是焦躁不安,自己抓起毛笔,记下了下面的笔记。


0)两种不同的东西可以称为“原型”:

  • 原型属性,如 obj.prototype

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

    它可以通过 ES5 检索Object.getPrototypeOf()

    Firefox 使它可以通过__proto__属性作为扩展来访问。ES6 现在提到了一些可选的__proto__.


1)存在这些概念来回答这个问题:

当我这样做时obj.property,JS 在哪里寻找.property

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


2)

  • __proto__用于点.属性查找,如obj.property.
  • .prototype用于直接查找,只是间接地因为它决定__proto__在对象创建与new

查找顺序为:

  • obj添加的属性obj.p = ...Object.defineProperty(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()
    

    然后new设置:

    f.__proto__ === F.prototype
    

    .prototype使用的地方。

  • Object.create

     f = Object.create(proto)
    

    套:

    f.__proto__ === proto
    

4)代码:

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

对应下图(部分Number内容省略):

(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)

此图显示了许多语言预定义的对象节点:

  • null
  • Object
  • Object.prototype
  • Function
  • Function.prototype
  • 1
  • Number.prototype(可以找到(1).__proto__,括号必须满足语法)

我们的两行代码只创建了以下新对象:

  • f
  • F
  • F.prototype

i现在是一个属性,f因为当你这样做时:

var f = new F(1)

它的计算结果Fthis被该值new然后把它分配给要回来,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,例如设置字段,很像经典的 OOP 语言。


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:
// http://stackoverflow.com/questions/22528967/es6-class-variable-alternatives
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.i1如“4)”中所述。

在第二行,当我们这样做时:

c.inc()
  • .inc通过[[Prototype]]找到c-> C-> C.prototype->inc
  • 当我们在 Javascript 中调用一个函数 as 时X.Y(),JavaScript 会函数调用内部自动设置this为 equal XY()

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

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

prototype允许您创建类。如果你不使用prototype那么它就会变成一个静态的。

这是一个简短的例子。

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

在上述情况下,您有静态函数调用测试。这个函数只能通过 obj.test 访问,你可以把 obj 想象成一个类。

在下面的代码中

function obj()
{
}

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

obj 已成为现在可以实例化的类。obj 的多个实例可以存在并且它们都具有该test功能。

以上是我的理解。我正在把它变成一个社区维基,所以如果我错了,人们可以纠正我。

-1:prototype是构造函数的属性,不是实例,即你的代码是错误的!也许你的意思__proto__是对象的非标准属性,但那是一个完全不同的野兽......
2021-02-19 01:26:14