“类”
JavaScript本没有类的概念,但是为了模仿其他面向对象语言,于是人为的创造了JavaScript中的“类”。其实都是利用了函数的一种特殊特性:所有的函数默认都会拥有一个名为prototype的公有并且不可枚举的属性,它会指向另一个对象。
function Foo() { // ...
}
Foo.prototype; // { }
这个对象通常被称为Foo的原型。这个对象是在调用new Foo()(学习this关键字(二)说明了new的过程)时创建的。最后被关联到Foo.prototype对象上。
我们来测试一下:
function Foo() { // ...
}
var bar = new Foo();
Object.getPrototypeOf(bar) === Foo.prototype; //true
现在我们总结一下:
- 在面向类的语言中,类可以被复制(或者说实例化)多次,就像用模具制作东西一样。之所以会这样是因为实例化(或者继承)一个类就意味着“把类的行为复制到物理对象中”,对于每一个新实例来说都会重复这个过程。
- 但是在
JavaScript中,并没有类似的复制机制。你不能创建一个类的多个实例,只能创建多个对象,它们[[Prototype]]关联的是同一个对象。但是在默认情况下并不会进行复制, 因此这些对象之间并不会完全失去联系,它们是互相关联的。new Foo()会生成一个新对象(我们称之为bar),这个新对象的内部链接[[Prototype]]关联的是Foo.prototype对象。 - 最后我们得到了两个对象,它们之间互相关联,就是这样。我们并没有初始化一个类,实际上我们并没有从“类”中复制任何行为到一个对象中,只是让两个对象互相关联。
new Foo()这个函数调用实际上并没有直接创建关联,这个关联只是一个意外的副作用。new Foo()只是间接完成了我们的目标:一个关联到其他对象的新对象。
关于继承
继承意味着复制操作,JavaScript默认并不会复制对象属性。
在JavaScript中,我们并不会将一个对象(“类”)复制到另一个对象(“实例”),JavaScript会在两个对象之间创建一个关联,这样一个对象就可以通过委托访问另一个对象的属性和函数。这个机制通常被称为“原型继承”。[[Prototype]]机制如下图所示(图0):
“构造函数”
思考示例代码:
function Foo() { // ...
}
Foo.prototype.constructor === Foo; // true
var bar = new Foo();
bar.constructor === Foo; // true
Foo.prototype默认有一个公有并且不可枚举的属性constructor(图一),这个属性引用的是对象关联的函数(本例中是Foo)。此外,我们可以看到通过“构造函数”调用new Foo()创建的对象也有一个constructor 属性(图二),指向 “创建这个对象的函数”(本例中是Foo)。
我们很容易认为Foo是一个构造函数,因为我们使用new操作“构造”了一个对象。实际上Foo和我们写的其他函数没有任何区别。区别在于我们在调用函数前使用了new操作符。就会把它变成一个“构造函数”调用。
new会劫持函数并用构造对象的形式来调用它
例如下面的代码:
function normal(){
console.log("normal func");
}
var foo = new normal(); // "normal func"
console.log(foo); //{}
normal只是一个普通函数,但是它使用了new操作符,它就会构造一个对象并赋值给foo。这个调用是一个构造函数调用,但是normal本身并不是一个构造函数。
JavaScript中对于“构造函数”最准确的解释是,所有带new的函数调用。
函数不是构造函数,但是当且仅当使用
new时,函数调用会变成“构造函数调用”。
模仿“类”
不过我们还是会经常模仿“类”的行为:
function Foo(name, age) {
this.name = name;
this.age = age;
}
Foo.prototype.getName = function() {
return this.name;
}
Foo.prototype.getAge = function() {
return this.age;
}
var o1 = new Foo("tom",24);
var o2 = new Foo("cat",22);
o1.getName(); // "tom"
o2.getAge(); // 22
这段代码看起来似乎创建o1和o2时会把Foo.prototype对象复制到这两个对象中,然而事实并不是这样。因为通过之前的学习我们发现,在创建的过程中,o1和o2的内部[[Prototype]]都会关联到 Foo.prototype上。当o1和o2中无法找到getName时,它会在Foo.prototype上找到。
在“构造函数”我们讲过,看起来bar.constructor === Foo;意味着bar确实有一个指向Foo 的constructor属性。实际上,constructor属性同样被委托给了Foo.prototype,而Foo.prototype.constructor默认指向Foo。
根据以上分析,我们很容易就把这种模仿“类”的行为理解错误产生误导。
举例来说:
function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 创建一个新原型对象
var bar = new Foo();
bar.constructor === Foo; // false
bar.constructor === Object; // true
Object(..)并没有“构造”bar,看起来应该是Foo()“构造”了它。大部分情况下我们都认为是Foo() 执行了构造工作,bar.constructor应该是Foo,但是它并不是Foo!
bar并没有constructor属性,所以它会委托[[Prototype]]链上的Foo.prototype。但是Foo.prototype = { /* .. */ }对象也没有constructor属性(不过默认的Foo.prototype对象有这个属性),所以它会继续委托,这次会委托给委托链顶端的Object.prototype。这个对象有constructor属性,指向内置的Object(..)函数(图三)。
在看一段代码:
function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 创建一个新原型对象
Object.defineProperty( Foo.prototype, "constructor" , {
enumerable: false,
writable: true,
configurable: true,
value: Foo // 让 .constructor 指向 Foo
});
这段代码向我们展示了,我们可以在Foo.prototype上手动添加一个constructor属性,constructor并不是一个不可变属性,它只是不可枚举,但是它的值是可以写的。
结论:bar.constructor是一个非常不可靠并且不安全的引用。
原型“继承”
现在我们已经了解了JavaScript中的“类”行为和原型继承的机制。还记得这张图吗,那么我们现在来用JavaScript实现一下。
这段代码就是典型的原型“继承”:
function Foo(name){
this.name = name;
}
Foo.prototype.getName = function (){
return this.name;
}
function Bar(name, title){
Foo.call(this,name);
this.title = title;
}
// 我们创建了一个新的 Bar.prototype 对象并关联到 Foo.prototype
// 注意!现在没有 Bar.prototype.constructor 了
// 如果你需要这个属性的话可能需要手动修复一下它
Bar.prototype = Object.create(Foo.prototype);
Bar.prototype.getTitle = function() {
return this.title;
}
var obj = new Bar( "tom", "cat" );
obj.getName(); // "tom"
obj.getTitle(); //"cat"
这里核心的语句就是Bar.prototype = Object.create(Foo.prototype),调用Object.create(..)会创建一个新对象并且把新对象的[[Prototype]]关联到指定的对象(示例中的Foo.prototype)。
还有其他的方法也能实现例如:
Bar.prototype = Foo.prototype;
Bar.prototype = Foo.prototype并不会创建一个关联到Bar.prototype的新对象,它只是让Bar.prototype直接引用Foo.prototype对象。因此当你执行类似Bar.prototype.age = ...的赋值语句时会直接修改Foo.prototype对象本身。显然这不是我们想要的结果,否则你根本不需要Bar对象,直接使用Foo就可以了,这样代码也会更简单一些。
Bar.prototype = new Foo();
Bar.prototype.constructor = Bar;
Bar.prototype = new Foo()的确会创建一个关联到Bar.prototype的新对象。但是它使用了 Foo(..)的“构造函数调用”,如果函数Foo有一些副作用的话,就会影响到Bar()的“后代”,所以来还指定Bar.prototype.constructor = Bar这样的代码,过于复杂不利于理解。
因此比较好的方法是使用Object.create(..)而不是使用具有副作用的Foo(..)。这样做唯一的缺点就是需要创建一个新对象然后把旧对象抛弃掉,不能直接修改已有的默认对象。
检查对象和“类”的关系
我们在开发中经常会想这个对象在那一个函数(“类”)中。这时我们需要一个能帮助我们检查的方案。
- instanceof
function Foo() {
//...
}
var bar = new Foo();
bar instanceof Foo; //true
instanceof用于检测在bar的[[Prototype]]链中是否有指向Foo.prototype的对象。但是这个方法只能处理对象和函数之间的关系。如果我想判断两个对象之间的关系只用instanceof无法实现。(instanceof官网文档)
- isPrototypeOf
function Foo() {
//...
}
var bar = new Foo();
Foo.prototype.isPrototypeOf(bar);
isPrototypeOf用于检测对象Foo.prototype是否出现在对象bar的[[Prototype]]链中。可以用于两个对象的关系检查。(isPrototypeOf官网文档)
我们也可以直接获得一个对象的[[Prototype]]链。
Object.getPrototypeOf(bar);
Object.getPrototypeOf(bar) === Foo.prototype; //true
getPrototypeOf用于获取对象的[[Prototype]]。(getPrototypeOf官网文档)
小结
这一小节,完善了对原型,“构造函数”和“继承”。下一节将介绍具体的实践。