【问题标题】:Clarifying javascript prototype nomenclature and mechanism阐明 javascript 原型命名法和机制
【发布时间】:2013-06-11 17:18:03
【问题描述】:

认识到 JavaScript 本身没有类的概念,并且所有对象的“类型”都是“对象”,我试图弄清楚“原型”由什么组成,并且,特别是它的“名称”如何与它相关联。例如,在下面:

function Foo(){};
console.log(Foo.prototype);                // => "Foo {}"

console.log 是如何知道在大括号前输出Foo 的?该名称指的是什么?

(注意:我知道在上面,我指的是函数的原型属性,而不是原型本身(即不是 __proto__ 可访问的东西),但同样的问题适用于实际的原型对象。我只是使用原型属性来简化我的示例。)

更新:根据评论线程,这个问题真正集中在 Chrome 正在做什么,特别是在以下方面对其行为进行合理化:

function Foo(){};
Foo.prototype.constructor = function Bar(){};
f = new Foo();
console.log(f);              // => Foo{} (remembering that f created by Foo, ignoring constructor)
console.log(Foo.prototype)   // => Bar{} (reporting constructor value)

更多讨论请见https://gist.github.com/getify/5793213

【问题讨论】:

  • 我猜函数的prototype 属性和对象的__proto__ 是一回事。这就是为什么你会看到你看到的输出。
  • 基本上,无论何时执行new Foo(),结果对象的__proto__ 都会设置为构造函数的prototype。我怀疑除了“这就是 Javascript OO 的工作方式”之外还有更深层次的解释。 (尽管有 ECMAScript 规范的链接。)或者:无论何时定义一个函数,Javascript 也会在内部为使用该函数作为构造函数创建的对象定义一个原型对象。
  • 您是否阅读了问题末尾的要点?关于 Chrome 行为的官方/统一解释的问题仍未解决。下面的主要反应是“不要担心”、“不要惊慌”等。我什么都没做,但这不是重点。我赞成你的回答,因为它提供了一个很好的原型概述,但它并没有真正解决 Chrome 行为问题。

标签: javascript


【解决方案1】:

JavaScript 有一种非常扭曲的原型继承形式。我喜欢称它为constructor pattern of prototypal inheritance。还有另一种原型继承模式 - the prototypal pattern of prototypal inheritance。我先解释一下后者。

在 JavaScript 中,对象继承自对象。不需要上课。这是一件好事。它使生活更轻松。例如,假设我们有一个用于线条的类:

class Line {
    int x1, y1, x2, y2;

    public:

    Line(int x1, int y1, int x2, int y2) {
        this.x1 = x1;
        this.y1 = y1;
        this.x2 = x2;
        this.y2 = y2;
    }

    int length() {
        int dx = x2 - x1;
        int dy = y2 - y1;
        return sqrt(dx * dx + dy * dy);
    }
}

是的,这是 C++。现在我们创建了一个类,我们现在可以创建对象了:

Line line1(0, 0, 0, 100);
Line line2(0, 100, 100, 100);
Line line3(100, 100, 100, 0);
Line line4(100, 0, 0, 0);

这四条线组成一个正方形。

JavaScript 没有任何类。它具有原型继承。如果您想使用原型模式做同样的事情,您可以这样做:

var line = {
    create: function (x1, y1, x2, y2) {
        var line = Object.create(this);
        line.x1 = x1;
        line.y1 = y1;
        line.x2 = x2;
        line.y2 = y2;
        return line;
    },
    length: function () {
        var dx = this.x2 - this.x1;
        var dy = this.y2 - this.y1;
        return Math.sqrt(dx * dx + dy * dy);
    }
};

然后你创建对象line的实例如下:

var line1 = line.create(0, 0, 0, 100);
var line2 = line.create(0, 100, 100, 100);
var line3 = line.create(100, 100, 100, 0);
var line4 = line.create(100, 0, 0, 0);

仅此而已。没有带有 prototype 属性的令人困惑的构造函数。继承所需的唯一函数是Object.create。这个函数接受一个对象(原型)并返回另一个继承自原型的对象。

不幸的是,与 Lua 不同,JavaScript 支持原型继承的构造函数模式,这使得原型继承更难理解。构造器模式是原型模式的逆。

  1. 在原型模式中,对象被赋予了最重要的地位。因此很容易看出对象继承自其他对象。
  2. 在构造函数模式中,函数被赋予了最重要的地位。因此人们倾向于认为构造函数继承自其他构造函数。这是错误的。

使用构造函数模式编写的上述程序如下所示:

function Line(x1, y1, x2, y2) {
    this.x1 = x1;
    this.y1 = y1;
    this.x2 = x2;
    this.y2 = y2;
}

Line.prototype.length = function () {
    var dx = this.x2 - this.x1;
    var dy = this.y2 - this.y1;
    return Math.sqrt(dx * dx + dy * dy);
};

您现在可以创建Line.prototype 的实例,如下所示:

var line1 = new Line(0, 0, 0, 100);
var line2 = new Line(0, 100, 100, 100);
var line3 = new Line(100, 100, 100, 0);
var line4 = new Line(100, 0, 0, 0);

注意到构造器模式和原型模式的相似之处了吗?

  1. 在原型模式中,我们只需创建一个具有create 方法的对象。在构造函数模式中,我们创建一个函数,JavaScript 自动为我们创建一个prototype 对象。
  2. 在原型模式中,我们有两种方法 - createlength。在构造函数模式中,我们也有两种方法 - constructorlength

构造函数模式与原型模式相反,因为当你创建一个函数时,JavaScript 会自动为函数创建一个prototype 对象。 prototype 对象有一个名为constructor 的属性,其中points back to the function itself

正如 Eric 所说,console.log 知道输出Foo 的原因是因为当您将Foo.prototype 传递给console.log 时:

  1. 它找到Foo.prototype.constructor,它本身就是Foo
  2. JavaScript 中的每个命名函数都有一个名为 name 的属性。
  3. 因此Foo.name"Foo"。所以它会在Foo.prototype.constructor.name 上找到字符串"Foo"

编辑:好的,我了解到您在 JavaScript 中重新定义 prototype.constructor 属性时遇到问题。要理解这个问题,我们首先要了解new 运算符的工作原理。

  1. 首先,我希望您仔细看看我在上面向您展示的图表。
  2. 在上图中,我们有一个构造函数、一个原型对象和一个实例。
  3. 当我们在构造函数 JS 创建新对象之前使用 new 关键字创建实例时。
  4. 这个新对象的内部[[proto]] 属性被设置为指向在对象创建时指向的constructor.prototype

这意味着什么?考虑以下程序:

function Foo() {}
function Bar() {}

var foo = new Foo;

Foo.prototype = Bar.prototype;

var bar = new Foo;

alert(foo.constructor.name); // Foo
alert(bar.constructor.name); // Bar

在此处查看输出:http://jsfiddle.net/z6b8w/

  1. 实例foo 继承自Foo.prototype
  2. 因此foo.constructor.name 显示"Foo"
  3. 然后我们将Foo.prototype 设置为Bar.prototype
  4. 因此bar 继承自Bar.prototype,尽管它是由new Foo 创建的。
  5. 因此bar.constructor.name"Bar"

在您提供的JS fiddle 中,您创建了一个函数Foo,然后将Foo.prototype.constructor 设置为function Bar() {}

function Foo() {}
Foo.prototype.constructor = function Bar() {};
var f = new Foo;
console.log(f.hasOwnProperty("constructor"));
console.log(f.constructor);
console.log(f);

因为您修改了Foo.prototype 的属性,所以Foo.prototype 的每个实例都会反映此更改。因此f.constructorfunction Bar() {}。因此f.constructor.name"Bar",而不是"Foo"

自己看看 - f.constructor.name"Bar"


众所周知,Chrome 会做类似的奇怪事情。重要的是要了解 Chrome 是一个调试实用程序,console.log 主要用于调试目的。

因此,当您创建一个新实例时,Chrome 可能会将原始构造函数记录在console.log 访问的内部属性中。因此它显示Foo,而不是Bar

这不是实际的 JavaScript 行为。根据规范,当您覆盖 prototype.constructor 属性时,实例和原始构造函数之间没有链接。

其他 JavaScript 实现(如 Opera 控制台、node.js 和 RingoJS)做正确的事情并显示 Bar。因此 Chrome 的行为是非标准的并且是特定于浏览器的,所以不要惊慌。

需要了解的重要一点是,即使 Chrome 显示的是 Foo 而不是 Bar,对象的 constructor 属性仍然是 function Bar() {},与其他实现一样:

【讨论】:

  • @DaveNewton:我同意。原型 OOP 非常简单。然而,大多数人会因为在 JavaScript 中实现原型 OOP 的方式(即构造函数模式)而感到困惑。由于 JS 是最知名的支持原型 OOP 的语言,这使人们认为原型 OOP 很困难或不合标准。 JavaScript 是我近 8 年的首选语言,但现在我对 Lua 大放异彩。我喜欢 Lua 中的冒号运算符和元表的概念,而且 Lua 的原型 OOP 是正确的。您可以使用__index 方法以任何您喜欢的方式实现原型继承。
  • @PeterAlfvin - 当您重新定义prototype.constructor 时,从该原型继承的所有对象都会反映更改(请记住,对象从 JavaScript 中的其他对象继承,而不是从构造函数继承)。因此instance.constructor 将指向新构造函数,instance.constructor.name 将是新构造函数的名称,而不是旧构造函数的名称。我已经更新了我的答案。阅读它。
  • @AaditMShah,我真的仔细阅读了你的帖子,相信我理解它们。但是,我的问题不是关于创建对象的构造函数属性的值。我的问题是关于 console.log(f) 的输出,至少在 Chrome 浏览器上。 (当我在节点下进行测试或在 JS Fiddle 中生成警报时,我会得到不同的结果)。在构造函数被重新定义为 Bar 的情况下,“f”仍然输出为“Foo {}”,而不是“Bar {}”。那个时候字符串“Foo”是从哪里来的?请参阅 jsfiddle.net/at4zQ/4 以了解此情况的简化情况。
  • @PeterAlfvin - 不要担心太多。这是浏览器特定的行为。它与 JavaScript 语言本身无关。 Chrome 可能会附加一个指向每个对象的原始构造函数的内部属性以用于调试目的。但是,据我所知,没有其他 JavaScript 实现可以做到这一点,所以不要惊慌。我已经编辑了我的答案。阅读它。
  • @datacarl 在构造函数内部this 指的是由new 自动创建的实例。在create 函数中,我们使用Object.create(this) 手动创建一个实例(这里this 指的是原型对象,而不是实例)。您可以将Object.create(this) 创建的实例保存在您希望允许您在嵌套函数中访问它的任何变量中。例如:var self = Object.create(this);。现在self 可以被任何嵌套函数访问。您甚至可以使用applybindcall 将函数的this 指针设置为self 对象。
【解决方案2】:

constructor 属性(它指的是一个函数最初用作相应对象的生成器)用于为控制台日志中的prototype 对象命名。考虑以下几点:

function Foo() { 
  this.x = 1; 
}
console.log(Foo.prototype);  // Foo {}
Foo.prototype.constructor = function Bar() {  
  this.y = 2 
}
console.log(Foo.prototype);  // Bar {}        
var f = new Foo();
console.log(f.constructor);  // function Bar() { this.y = 2}

console.log(f.x);            // 1
console.log(f.y);            // undefined
console.log(f);              // Foo {x:1}

在这里,我们将constructor 切换到另一个函数,为prototype 对象提供了一个新名称。请注意,当直接从 object 查询 constructor 属性时,将返回相同的函数,该对象是使用 Foo() 函数创建的(随着我们沿继承链上升)。

不过,这并不意味着实际上使用了另一个函数(Bar())来创建相应的对象;它仍然是Foo(),您可以通过查询属性来查看它——也可以直接查看f。基本上,对象会记住用于创建它们的函数,即使 prototypeconstructor 属性被“重定向”。

【讨论】:

  • 谢谢,@raina77ow。关于你的最后一句话:“基本上,对象记住用于创建它们的函数,即使 prototypeconstructor 属性被“重定向”,我的后续问题是:1)这个概念是否有一个公认的名称“用于创建它们的功能”,以及 2)为了“记忆”而将这些信息存储在哪里?
【解决方案3】:
function Foo(){};

沿着链条工作:

console.log(Foo.prototype);
console.log(Foo.prototype.constructor);
console.log(Foo.prototype.constructor.name);

【讨论】:

  • raina77ow 的回答表明这是不正确的。如果重新定义了构造函数的prototype.constructor 属性,则由该构造函数创建的对象仍以构造函数的名称显示。你有不同的看法吗?
  • 抱歉,我意识到我的问题集中在为函数的原型属性显示的名称上,并且似乎确实跟踪了prototype.constructor。但是,即使构造函数的prototype.constructor 属性被重新定义,构造函数创建的对象仍保留构造函数的名称,所以我仍然对此感到疑惑。是否有一些内部的、不可访问的 constructor 属性保留了用于创建对象的构造函数?
【解决方案4】:

在网上进行了一些挖掘,但我发现这篇文章真正说明了原型和其他关键核心 javascript 功能的工作原理:

http://dmitrysoshnikov.com/ecmascript/javascript-the-core/

我特别喜欢原型链的示意图。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-10-31
    • 1970-01-01
    • 1970-01-01
    • 2014-07-20
    • 1970-01-01
    • 2011-06-03
    • 2013-08-18
    • 2020-01-25
    相关资源
    最近更新 更多