澄清javascript原型命名和机制

时间:2013-06-15 00:07:23

标签: javascript

认识到JavaScript没有类本身的概念,并且所有对象的“类型”都是“对象”,我试图让我理解“原型”包含的内容,特别是它的“名称”如何与它相关联。例如,在以下内容中:

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

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

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

更新:根据评论主题,这个问题非常关注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 个答案:

答案 0 :(得分:17)

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. 在构造函数模式中,函数最重要。因此人们倾向于认为构造函数继承自其他构造函数。这是错误的。
  3. 使用构造函数模式编写时,上面的程序看起来像这样:

    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
    3. 构造函数模式是原型模式的反转,因为当您创建函数时,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"上找到字符串Foo.prototype.constructor.name

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

        1. 首先,我想让你好好看看我上面给你看的图表。
        2. 在上图中,我们有一个构造函数,一个原型对象和一个实例。
        3. 在构造函数JS创建新对象之前使用new关键字创建实例时。
        4. 此新对象的内部[[proto]]属性设置为指向创建对象时<{1>}指向的任何内容
        5. 这意味着什么?请考虑以下程序:

          constructor.prototype

          请参阅此处的输出:http://jsfiddle.net/z6b8w/

          1. 实例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 继承自foo
          2. 因此Foo.prototype会显示foo.constructor.name
          3. 然后我们将"Foo"设置为Foo.prototype
          4. 因此,Bar.prototype继承自bar,但它是由Bar.prototype创建的。
          5. 因此new Foobar.constructor.name
          6. 在您提供的JS fiddle中,您创建了一个功能"Bar",然后将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.prototypef.constructor。因此function Bar() {}f.constructor.name,而不是"Bar"

            自己查看 - "Foo"f.constructor.name


            众所周知,Chrome会做出类似奇怪的事情。重要的是要理解Chrome是一个调试工具,"Bar"主要用于调试目的。

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

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

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

            重要的是要理解,即使Chrome显示Bar而不是Foo,对象的Bar属性仍然是constructor,与其他实现一样:

答案 1 :(得分: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对象赋予一个新名称。请注意,直接从使用Foo()函数创建的对象查询constructor属性时会返回相同的函数(因为我们继承了继承链)。

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

答案 2 :(得分:0)

function Foo(){};

在链条上工作:

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

答案 3 :(得分:0)

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

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

我特别喜欢关于原型链如何看待的图表。