3

认识到 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

4

4 回答 4

17

JavaScript 有一种非常扭曲的原型继承形式。我喜欢称它为原型继承的构造函数模式。还有另一种原型继承模式——原型继承的原型模式。我先解释一下后者。

在 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,它指向函数本身

正如埃里克所说,console.log知道输出的原因Foo是因为当你传递Foo.prototypeconsole.log

  1. 它发现Foo.prototype.constructor哪个是Foo它自己。
  2. JavaScript 中的每个命名函数都有一个名为name.
  3. 因此Foo.name"Foo"。所以它"Foo"Foo.prototype.constructor.name.

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

  1. 首先,我想让你好好看看我上面给你展示的图表。
  2. 在上图中,我们有一个构造函数、一个原型对象和一个实例。
  3. 当我们new在构造函数 JS 创建一个新对象之前使用关键字创建实例时。
  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.prototypeBar.prototype
  4. 因此bar虽然Bar.prototype它是由new Foo.
  5. 因此bar.constructor.name"Bar"

在您提供的JS 小提琴中,您创建了一个函数Foo,然后设置Foo.prototype.constructorfunction 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而不是对象Barconstructor属性仍然function Bar() {}与其他实现一样:

于 2013-06-15T01:13:02.657 回答
1

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对象一个新名称。请注意,当constructor直接从使用 Foo() 函数创建的对象中查询属性时,将返回相同的函数(因为我们沿着继承链向上)。

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

于 2013-06-15T00:13:25.700 回答
0

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

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

我特别喜欢原型链的外观图。

于 2014-10-27T23:59:14.483 回答
0
function Foo(){};

沿着链条工作:

console.log(Foo.prototype);
console.log(Foo.prototype.constructor);
console.log(Foo.prototype.constructor.name);
于 2013-06-15T00:13:32.360 回答