图解Javascript对象的继承
在前面《图解Javascript对象的创建》 一文中, 我们简单描述了JavaScript
原型对象体系,
并简单介绍了原型链的相关概念。
和对象的创建一样, 开发者们根据自己的业务需求, 发明了形形色色的方法来模拟类的继承特性。 在JavaScript
基于原型的对象体系中,对象之间的继承关系主要依靠原型链体系来实现。
本文将对一些主流的方法进行总结和梳理。
基于原型链的继承
JavaScript
基于原型链实现继承关系的基本思想很简单:将子类构造函数的prototype
属性指向一个父类的实例对象。
如下例,我们创建一个类型Cat
继承了Animal
:
function Animal(name){
this.name=name;
this.favorFood=[];
};
Animal.prototype.sayHi= function (){
console.log(`Hi, I'm ${this.name}`);
};
function Cat(){
}
// 继承语句
Cat.prototype = new Animal("animal name");
Cat.prototype.catchMouse = function(){
console.log("catch a mouse..")
};
var cat1=new Cat();
var cat2=new Cat();
上面的例子里, 我们首先定义了Cat
的构造函数,并将一个Animal
的实例化对象作为它的原型对象;
随后又将一个catchMouse
方法挂载到该原型对象上。
另外, 由于所有引用类型都继承于Object
类型, 实质上也是由原型链实现的, 我们把整个链条串起来,
内存结构如下:
从图中可以看到, 我们在Animal
中定义的属性name
和favorFood
在实例化对象new Animal
的时候被创建,
而catchMouse
整个方法则是被我们手动挂载在Cat.prototype
上。
尽管我们在基类Animal
中定义了属性name
, 却无法在真正创建Cat
对象的时候动态传参并赋值给属性。
同样的问题, 如果我们试图通过Cat
的对象去修改的属性值favorFood
:
cat1.favorFood.push("red fish");
cat2.favorFood; //["red fish"]
由于favorFood
被挂载在Cat.prototype
上, 任一实例对象对于它的修改都会影响到其他属性。
为了规避上面提到的问题, 我们需要在原型链
的基础上对上述代码进行改进。
调用基类构造函数
首先, 从上图不难看出, 导致父类型属性被共享的原因在于相关属性被定义在了Cat
的原型对象上了。
如果将这些属性放在子类型对象上是否可以解决问题呢? 我们对代码进行如下修改:
function Animal(name){
this.name = name;
this.favorFood = [];
};
Animal.prototype.sayHi= function (){
console.log(`Hi, I'm ${this.name}`);
};
function Cat(name){
Animal.call(this, name)
}
// 继承语句
Cat.prototype = new Animal("animal name");
Cat.prototype.catchMouse = function(){
console.log("catch a mouse..")
};
var cat1=new Cat("cat1");
var cat2=new Cat("cat2");
我们在Cat
的构造函数中调用了Animal
函数, 这里的this
指向的将要创建的对象。
同时, 还给Cat
构造函数加上了入参, 这样就可以在创建对象的时候动态传参了。
Object.create
在上面的例子里, 由于不希望父类型属性被共享,我们在Cat
构造函数中回调了Animal
的构造函数, 保证name
和favorFood
属性被挂载到每个Cat
的对象中。
同时,为了将子类型Cat
的prototype
属性设置为父类型的对象, 调用了父类型的构造函数:
// 继承语句
Cat.prototype = new Animal("animal name");
这两个操作导致在子类型的原型对象上Cat.prototype
同样也挂载了name
和favorFood
属性, 而这完全没必要。
为了提高效率, 避免在子类型原型对象上创建不必要的属性, 可以利用Object.create
函数进行如下优化:
function Animal(name){
this.name = name;
this.favorFood = [];
};
Animal.prototype.sayHi= function (){
console.log(`Hi, I'm ${this.name}`);
};
function Cat(name){
Animal.call(this, name)
}
// 继承语句
// Cat.prototype = new Animal("animal name");
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;
Cat.prototype.catchMouse = function(){
console.log("catch a mouse..")
};
var cat1=new Cat("cat1");
var cat2=new Cat("cat2");
ES5
提供了方法
Object.create(proto, [propertiesObject])
用于创建一个新对象,并将传入的参数proto
作为新对象[[prototype]]
属性的值, 通过入参propertiesObject
可以定义新对象自身的属性。
利用Object.create
, 我们基于Animal.prototype
为Cat
创建了原型对象, 结构如下:
ES6 的 extend
在ES6之后引入了class
和extends
关键字, 继承的操作就方便了很多:
class Animal{
constructor(name){
this.name=name;
}
sayHi(){
console.log(`Hi, I'm ${this.name}`);
}
}
class Cat extends Animal{
constructor(name, age){
super(name);
this.age=age;
}
}
let c1 = new Cat("c1", 2);
let c2 = new Cat("c2", 1);
尽管最终的运行机时制仍然是基于原型体系的, 但语法上清爽了很多。