JavaScript面向对象系列:五、继承

原型对象链和 Object.prototype

js內建的继承方法被称为原型对象链,又可以称为原型对象继承。原型对象的属性可以经由对象实例访问,这就是继承的一种形式。对象实例继承了原型对象的属性。因为原型对象也是一个对象,他也有自己的原型对象并继承其属性。这就是原型对象链,而原型对象继承它的原型对象,以此类推。

所有对象,包括哪些你自己定义的对象都自动继承自 Object,除非另有指定。更确切的说,所有对象都继承自 Object.prototype。任何以队形字面量形式定义的对象,其 [[Prototype]] 的值都被设为 Object.prototype,这意味着它继承 Object.prototype 的属性。

var book = {
title:"javascript"
};

var prototype = Object.getPrototypeOf(book);
console.log(prototype === Object.prototype); //true

继承自 Object.peototype 的方法

前面说到的很多方法其实都是定义在 Object.prototype 上面的。因此可以被其他对象继承。

方法 作用
hasOwnProperty() 检查是否存在一个给定名字的自有属性
propertyIsEnumerable() 检查一个自有属性是否可枚举
isPrototypeof() 检查一个对象是否是另一个对象的原型对象
valueOf() 返回一个对象的值表达
toString() 返回一个对象的字符串表达

valueOf,每当一个操作符被用于一个对象时就会调用 valueOf() 方法。 valueOf() 默认返回对象实例本身。原始封装类型重写了 valueOf() 以使得它对 String 返回一个字符串,对 Boolean 返回一个布尔值,对 Number 返回一个数字。类似的,Date 对象返回一个 epoch 时间。如果你的对象也要这样使用操作符,也可以自定义 valueOf() 方法.定义的时候并没有改变操作符的行为,仅仅定了操作符默认行为所使用的值。

toString(),一旦 valueOf() 方法返回的是一个引用而不是原始值的时候,就会回退调用 toString() 方法。另外,当js期望一个字符串时,也会对原始值隐式调用 toString() .例如,当加号操作符的一边是一个字符串时,另一边会被自动转换成字符串。如果另一边是一个原始值,会自动被转换成一个字符串表达(例如,true 转换成 “true”)。如果另一边是一个引用值,则会调用 valueOf()。如果 valueOf() 返回一个引用值,则调用 toString()

例如

var book = {
title:"javascript"
};

var message = "Book = " + book;
console.log(message); //"Book = [object Object]"

这段以 “Book =” 和book来构造字符串。因为book是一个对象,此时调用它的 toString() 方法。该方法继承自 Object.prototype,大部分js引擎返回默认值 “[object Object]”。如果对这个值满意,就不需要改变对象的 toString() 方法。定义自己的 toString() 方法有时候可以为此类字符串转换提供更过信息的值。

var book = {
title:"javascript",
toString:function(){
return "[Book " + this.title + "]";
}
};

var message = "Book = " + book;
console.log(message); //"Book = [Book javascript]"

修改 Object.pototype

所有的对象都默认继承自 Object.prototype,所以改变 Object.prototype 会影响所有的对象,是非常危险的。

例如

Object.prototype.add = function(value){
return this + value;
}

var book = {
title:"javascript"
};

console.log(book.add(5)); //"[object Object]5";
console.log("title".add("end")); //"titleend"

//在浏览器中
console.log(document.add(true)); //"[object HTMLDocument]true"
console.log(window.add(5)); //"[object Window]6"

上面给 Object.prototype 添加方法可能会带来不可预知的结果

例如

var empty = {};

for(var prpperty in empty){
console.log(property);
}
// add

空对象依然会输出一个”add”属性。考虑到js中 for-in 使用频繁,为 Object.prototype 添加可枚举属性会影响大量代码。所以可以在 for-in 中使用 hasOwnProperty().

例如

var empty = {};

for(var prpperty in empty){
if(empty.hasOwnProperty(property)){
console.log(property);
}
}
//

这样循环只会输出对象自有属性,不会输出原型属性。

对象继承

对象继承是最简单的继承类型。唯一需要做的就是制定那个对象是新对象的 [[Prototype]] 。对象字面量形式会隐式指定 Object.prototype 为其 [[Prototype]],也可以使用 Object.create() 方法显式指定。

例如

var book = {
title:"javascript"
};

//和下面是一样的

var book = Object.create(Object.prototype,{
title:{
configurable:true,
enumerable:true,
value:"javascript",
writable:true
}
});

继承其他对象

var person1 = {
name:"laowang",
sayName:function(){
console.log(this.name);
}
};

var person2 = Object.create(person1,{
name:{
configurable:true,
enumerable:true,
value:"xiaowang",
writable:true
}
});

person1.sayName(); //"laowang"
person2.sayName(); //"xiaowang"

console.log(person1.hasOwnProperty("sayName")); //true
console.log(person1.isPrototypeOf(person2)); //true
console.log(person2.hasOwnProperty("sayName")); //false

另外,也可以通过 Object.create() 创建 [[Prototype]] 为null的对象,这样的对象是没有原型对象链的对象。意味着 toString()valueOf() 等內建方法都不存在该对象上面。实际上,这种对象完全就是一个没有任何预定义属性的白板,也是一个完美的哈希容器,因为不会有自由属性和原型属性的冲突。

例如

var nakedObject = object.create(null);

console.log("toString" in nakedObject); //false
console.log("valueOf" in nakedObject); //false

构造函数继承

js中的对象继承也是构造函数继承的基础。几乎所有的函数都有 prototype 属性,它可以被修改或者换。该 prototype 属性被自动设置为一个新的继承自 Object.prototype 的泛用对象,该对象有一个自有属性 constructor ,实际上js引擎自动做了下面的事情

function YourConstructor(){

}

//js引擎自动为你做了下面的事情
YourConstructor.prototype = Object.create(Object.prototype,{
constructor:{
configurable:true,
enumerable:true,
value:YourConstructor,
writable:true
}
});

创建出来的对象都继承自 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 + " ]";
};

//继承Reactangle
function Square(size){
this.length = size;
this.width = size;
}

Square.prototype = new Rectangle();
Square.prototype.constuctor = 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

此时不需要给Rectangle的调用提供参数,因为他们不需要被使用,而且如果提供了,那所有Square的对象实例都会共享同样的维度。用这种方式改变原型对象链时,需要确保构造函数不会再参数缺失时抛出错误(很多构造函数包含的初始化逻辑会需要参数)且构造函数不会改变任何全局状态,比如追踪有多少实例被创建等。

rect作为Rectangle的实例被创建,而square则是作为Square的实例被创建。两个对象都有getArea方法,那是因为继承自Reatangle.prototype。instanceof操作符认为变量square同时是Square、Rectangle、Object的实例,因为instanceof是使用原型对象链检查对象类型。

使用 Object.create() 方法可以简化并且不会导致参数缺失而报错。

例如

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 + " ]";
};

//继承Reactangle
function Square(size){
this.length = size;
this.width = size;
}

Square.prototype = Object.create(Rectangle.prototype.{
constructor:{
configurable:true,
enumerable:true,
value:Square,
writable:true
}
});
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
````

## 构造函数窃取

由于js中的继承是通过原型对象链来实现的,因此不需要调用对象的父类构造函数。如果需要在子类构造函数中调用父类构造函数,就需要用 ___call()___ 或者 ___apply()___。

例如
```javascript
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 + " ]";
};

//继承Reactangle
function Square(size){
Retangle.call(this,size,size);
}

Square.prototype = Object.create(Rectangle.prototype.{
constructor:{
configurable:true,
enumerable:true,
value:Square,
writable:true
}
});
Square.prototype.toString = function(){
return "[Square " + this.length + "X" + this.width + " ]";
};

 

var square = new Square(6);

console.log(square.length); //6
console.log(square.width); //6
console.log(square.getArea()); //36

由于这种做法模仿了那些基于类语言的类继承,通常被称为伪类继承。

访问父类方法

子类提供的新功能覆盖父类方法很常见,但是如果还想访问父类方法,只能通过 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 + " ]";
};

//继承Reactangle
function Square(size){
Retangle.call(this,size,size);
}

Square.prototype = Object.create(Rectangle.prototype.{
constructor:{
configurable:true,
enumerable:true,
value:Square,
writable:true
}
});
Square.prototype.toString = function(){
var text = Rectangle.prototype.toString().call(this);
return text.replace("Rectangle","Square");
};

总结

js通过原型对象链支持继承。当将一个对象的 [[Prototype]] 设置为另一个对象时,就在这两个对象之间创建了一条原型对象链。所有的泛用对象都自动继承自 Object.prototype 。如果你想创建一个继承自其它对象的对象,你可以用 Object.create() 指定 [[Prototype]] 为一个新对象。

 

可以在构造函数中创建原型对象链来完成自定义类型之间的继承。通过将构造函数的 prototype 属性设置为某一个对象那个,就建立了自定义类型对象和该对象的继承关系。构造函数的所有对象、实例共享同一个原型对象,所以他们都继承自该对象。这个技术在继承其他对象的方式时工作得很好。但是不能用原型继承自有属性。

为了正确继承自有属性,可以使用构造函数窃取。只需要以 call() 或者 apply() 调用父类的构造函数,就可以在子类里面完成各种初始化。结合构造函数窃取和原型对象链是js中最常见的继承手段。由于和基于类的继承相似,这个组合经常被称为伪类继承。

可以通过直接访问父类原型对象的方式访问父类方法。必须以 call() 或者 apply() 执行父类方法并传入一个子类的对象。

发表评论

电子邮件地址不会被公开。 必填项已用*标注