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 支持原型继承的构造函数模式,这使得原型继承更难理解。构造器模式是原型模式的逆。
- 在原型模式中,对象被赋予了最重要的地位。因此很容易看出对象继承自其他对象。
- 在构造函数模式中,函数被赋予了最重要的地位。因此人们倾向于认为构造函数继承自其他构造函数。这是错误的。
使用构造函数模式编写的上述程序如下所示:
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);
注意到构造器模式和原型模式的相似之处了吗?
- 在原型模式中,我们只需创建一个具有
create 方法的对象。在构造函数模式中,我们创建一个函数,JavaScript 自动为我们创建一个prototype 对象。
- 在原型模式中,我们有两种方法 -
create 和 length。在构造函数模式中,我们也有两种方法 - constructor 和 length。
构造函数模式与原型模式相反,因为当你创建一个函数时,JavaScript 会自动为函数创建一个prototype 对象。 prototype 对象有一个名为constructor 的属性,其中points back to the function itself:
正如 Eric 所说,console.log 知道输出Foo 的原因是因为当您将Foo.prototype 传递给console.log 时:
- 它找到
Foo.prototype.constructor,它本身就是Foo。
- JavaScript 中的每个命名函数都有一个名为
name 的属性。
- 因此
Foo.name 是"Foo"。所以它会在Foo.prototype.constructor.name 上找到字符串"Foo"。
编辑:好的,我了解到您在 JavaScript 中重新定义 prototype.constructor 属性时遇到问题。要理解这个问题,我们首先要了解new 运算符的工作原理。
- 首先,我希望您仔细看看我在上面向您展示的图表。
- 在上图中,我们有一个构造函数、一个原型对象和一个实例。
- 当我们在构造函数 JS 创建新对象之前使用
new 关键字创建实例时。
- 这个新对象的内部
[[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/
- 实例
foo 继承自Foo.prototype。
- 因此
foo.constructor.name 显示"Foo"。
- 然后我们将
Foo.prototype 设置为Bar.prototype。
- 因此
bar 继承自Bar.prototype,尽管它是由new Foo 创建的。
- 因此
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.constructor 是function 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() {},与其他实现一样: