深入理解继承
学习怎样创建对象是理解面向对象编程的第一步,第二步是理解继承。在传统的面向对象编程语言中,类继承其他类的属性。
然而,JS的继承方式与传统的面向对象编程语言不同,继承可以发生对象之间,这种继承的机制是我们已经熟悉的一种机制:原型。
1.原型链接和Object.prototype
js内置的继承方式被称为原型链接(prototype chaining)或原型继承(prototypal inheritance)。正如我们在前一天所学的,原型对象上定义的属性,在所有的对象实例中都是可用的,这就是继承的一种形式。对象实例继承了原型中的属性。而原型也是一个对象,所以它也有自己的原型,并且继承原型中的属性。这被称为原型链:对象继承自己原型对象中属性,而这个原型会继续向上继承自己的原型,依此类推。
所有对象,包括我们定义自己的对象,都自动继承自Object
,除非我们另有指定(本课后面讨论)。更具体地说,所有对象都继承Object.prototype
。任何通过对象字面量定义的对象都有一个__proto__
设置为object.prototype
,意味着它们都继承Object.prototype
对象中的属性,就像这个例子中的book
:
var book = {
title: "平凡的世界"
};
var prototype = Object.getPrototypeOf(book);
console.log(prototype === Object.prototype); // true
book
的原型等于Object.prototype
。不需要额外的代码来实现这一点,因为这是创建新对象时的默认行为。这种关系意味着book
自动接收来自Object.prototype
对象中的方法。
1.2.从Object.prototype中继承的方法
我们在前几天使用的一些方式实际上是定义在Object.prototype
原型对象中,因此所有其他对象也都继承了这些方法。这些方法是:
- hasOwnProperty():判断对象中有没有某个属性,接受一个字符串类型的属性名作为参数。
- propertyIsEnumerable():判断对象中的某个属性是否是可枚举的。
- isPrototypeOf():判断一个对象是否是另个对象的原型。
- valueOf:返回对象的值表示形式。
- toString:返回对象的字符串表示形式。
- toLocaleString: 返回对象的本地字符串表示形式。
这五种方法通过继承所有对象都拥有这6个方法。当我们需要使对象在JavaScript中一致工作时,最后两个是非常重要的,有时我们可能希望自己定义它们。
1.3:valueOf()
当我们操作对象时,valueof()
方法就会被调用时。默认情况下,valueof()
简单地返回对象实例。对于字符串,布尔值和数字类型的值,首先会使用原始包装类型包装成对象,然后再调用valueof()
方法。同样,Date
对象的valueof()
方法返回以毫秒为单位的纪元时间(就像Date.prototype.getTime()
一样)。这也是为什么我们可以对日期进行比较,例如:
var now = new Date();
var earlier = new Date(2010, 1, 1);
console.log(now > earlier); // true
1.4修改Object.prototype
默认情况下,所有对象都继承自Object.prototype
,因此改变Object.prototype
会影响到所有对象。这是非常危险的情况。
Object.prototype.add = function(value) {
return this + value;
};
var book = {
title: "平凡的世界"
};
console.log(book.add(5)); // "[object Object]5"
console.log("title".add("end")); // "titleend"
// in a web browser
console.log(document.add(true)); // "[object HTMLDocument]true"
console.log(window.add(5)); // "[object Window]true"
导致的另一个问题:
var empty = {};
for (var property in empty) {
console.log(property);
}
解决方法:
for(name in book){
if(book.hasOwnProperty(name)){
console.log(name);
}
}
虽然这个方法可以有效地过滤掉我们不需要的原型属性,但是它也限制了使用for-in
只能变量的属性,而不能遍历原型属性。建议不要修改原型对象。
2:对象继承
最简单的继承方式是对象之间的继承。我们所需要做的就是指定新创建对象的原型应该指向哪个对象。通过Object字面量的形式创建的对象默认将__proto__
属性指向了Object.prototype
,但是我们可以通过Object.create()
方法显示地将__proto__
属性指向其他对象。
Object.create()
方法接收两个参数。第一个参数用来指定新创建对象的__proto__
应该指向的对象。第二个参数是可选的,用来设置对象属性的描述符(特性),语法格式与Object.definedProperties()
方法参数个格式一样。如下所示:
var book = {
title: "人生"
};
// 等价于
var book = Object.create(Object.prototype, {
title: {
configurable: true,
enumerable: true,
value: "人生",
writable: true
}
});
代码中两个声明的效果是一样的。第一个声明使用对象字面量的方式定义一个带有单个属性:title
的对象。这个对象自动继承自Object.prototype
,并且属性默认被设置成可配置,可枚举,可写。第二个声明和第一个一样,但是显示使用了Object.create()
方法。但是你可能永远不会这样显示地直接继承Object.prototype
,没有必要这样做,因为默认就已经继承了Object.prototype
。继承自其他对象会比较有趣一点:
var person1 = {
name: '张三',
sayName: function(){
console.log(this.name);
}
};
var person2 = Object.create(person1, {
name: {
value: '李四',
configurable: true,
enumerable: true,
writable: true
}
});
person1.sayName(); // '张三'
person2.sayName(); // '李四'
console.log(person1.hasOwnProperty("sayName")); // true
console.log(person1.isPrototypeOf(person2)); // true
console.log(person2.hasOwnProperty("sayName")); // false
这段代码创建了一个对象person1
,该对象有一个name
属性和一个sayName()
方法。person2
对象继承了person1
,因此它也继承了name
属性和sayName()
方法。然而,person2
是通过Object.create()
方法定义的,它也定义了自己的name
属性。对象自己的属性遮挡了原型的中同名属性name
。因此,person1.sayName()
输出'张三'
,person2.sayName()
输出'李四'
。记住,person2.sayName()
只存在于person1
中,被person2
继承了下来。
当对象的属性被访问时,JavaScript会首先会在对象的属性中搜索,如果没有找到,则继续在__proto__
指向的原型对象中搜索。如果任然没有找到,则继续搜索原型对象的上个原型对象,直到到达原型链的末端。原型链的末端结束于Object.prototype
,Object.prototype
对象的__proto__
内部属性为null
。
3.构造函数继承
JavaScript中的对象继承也是构造函数继承的基础。回顾昨天的内容,几乎每一个函数都有一个可以修改或替换的prototype
属性。prototype
属性自动被赋值为一个新的对象,这个对象继承自Object.prototype
,并且对象中有一个自己的属性constructor
。实际上,JavaScript引擎为我们执行以下操作:
// 这是我们写的
function YourConstructor() {
// initialization
}
// JavaScript引擎在后台帮我们做的:
YourConstructor.prototype = Object.create(Object.prototype, {
constructor: {
configurable: true,
enumerable: true,
value: YourConstructor
writable: true
}
});
因此,不做任何额外的工作,这段代码给我们的构造函数的prototype
属性设置了一个对象,这个对象继承自Object.prototype
,这意味着通过构造函数YourConstructor()
创建的所有实例都继承自Object.prototype
。YourConstructor
是Object
的子类,Object
是YourConstructor
的超类。
由于prototype
属性是可写的,因此通过复写它我们可以改变原型链。例如:
function Rectangle(length, width) {
this.length = length;
this.width = width;
}
Rectangle.prototype.getArea = function() {
return this.length * this.width;
};
Rectangle.prototype.toString = function() {
return "[Rectangle " + this.length + "x" + this.width + "]";
};
// 继承 Rectangle
function Square(size) {
this.length = size;
this.width = size;
}
Square.prototype = new Rectangle();
Square.prototype.constructor = Square;
Square.prototype.toString = function() {
return "[Square " + this.length + "x" + this.width + "]";
};
var rect = new Rectangle(5, 10);
var square = new Square(6);
console.log(rect.getArea()); // 50
console.log(square.getArea()); // 36
console.log(rect.toString()); // "[Rectangle 5x10]"
console.log(square.toString()); // "[Square 6x6]"
console.log(rect instanceof Rectangle); // true
console.log(rect instanceof Object); // true
console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true
console.log(square instanceof Object); // true
这段代码中有两个构造函数:Reactangle
和Square
。Square
构造函数的原型对象被重新赋值为Reactangle
的对象实例。在创建Reactangle
对象实例的时候没有传递参数,因为它们没有用,如果传递参数了,所有的Square
对象实例都会共享相同的尺寸。以这种方式改变原型链之后,要确保constructor
属性的指向正确的构造函数。
4.使用父类的构造函数
如果你想要在子类的构造函数中调用父类的构造函数,那么我们就需要利用call()
方法或apply()
方法。
function Rectangle(length,width){
this.length = length;
this.width = width;
}
Rectangle.prototype.getArea = function(){
return this.length * this.width;
};
Rectangle.prototype.toString = function(){
return '[Rectangle'+this.length+'x'+this.width+']';
};
function Square(size){
Rectangle.call(this,size,size);
}
Square.prototype = Object.create(Rectangle.prototype,{
constructor:{
configurable:true,
enumerable:true,
writable:true
}
});
Square.prototype.toString = function(){
return '[Square'+this.length+'x'+this.width+']';
};
var square = new Square(20);
console.log(square.getArea()); //400
console.log(square.toString()); //[Square20x20]
5.访问父类的方法
在上一个例子中,Square
类型有自己的toString()
方法,该方法遮挡了原型中的toString()
方法。但有时候我们仍然想要访问父类的方法该怎么办?我们可以直接访问原型对象中的属性,如果是访问方法的话,可以call()
或apply()
。例如:
function Rectangle(length,width){
this.length = length;
this.width = width;
}
Rectangle.prototype.getArea = function(){
return this.length * this.width;
};
Rectangle.prototype.toString = function(){
return '[Rectangle'+this.length+'x'+this.width+']';
};
function Square(size){
this.length = size;
this.width = size;
};
Square.prototype = Object.create(Rectangle.prototype,{
constructor:{
value:Square,
configurable:true,
enumerable:true,
writable:true
}
});
Square.prototype.toString =function(){
var text = Rectangle.prototype.toString.call(this);
return text.replace('Rectangle','Square');
}
Square.prototype.getBorder = function(){
return '边数' + Rectangle.prototype.getBorder();
};
var square = new Square(20);
console.log(square);
console.log(square.getArea());
console.log(square.toString());
console.log(square.getBorder());
在这个版本的代码,使用Square.prototype.toString()
和call()
方法一起调用Rectangle.prototype.toString()
。这个方法只需要在返回结果之前将Rectangle
替换成Square
就可以了。对于这样简单的操作来所,这种方式可能有点繁琐,但这却是访问父类方法的唯一途径。