笔记:JavaScript继承
本文摘录及参考自:
1. Javascript继承机制的设计思想
2. Javascript 面向对象编程(一):封装
3. Javascript面向对象编程(二):构造函数的继承
4. Javascript面向对象编程(三):非构造函数的继承
5. 继承与原型链
6. JavaScript 中的继承
7. JavaScript继承方式详解
8. JavaScript深入之继承的多种方式和优缺点
9. JavaScript深入之从原型到原型链
10. JavaScript深入之call和apply的模拟实现
11. JavaScript深入之new的模拟实现
12. JavaScript深入之创建对象
13. 深入理解JavaScrip面向对象和原型继承
继承的由来
1. 原始模式
JavaScript的继承从本质上来说是通过“原型链”(prototype chain)实现的。
我们要创建Dog这一个对象,通过对Dog的封装,原始模式可能是这样的:
例:
var Dog ={
name:""
type: ""
}
var dogA = {}
dogA.name="测试1"
dogA.type="type"
var dogB ={}
dogB.name="测试2"
dogB.type="type"
可以看出,这样生成实例的方式非常麻烦,而且实例与原型之间没有联系
2. new
JavaScript在一开始的设计中,并没有引入“类”的概念(到ECMAScript6才引入class)。为了解决1中的问题,它引入了new关键字用来创建实例,new后面跟的不是类名,而是构造函数(方法)
例:
function Dog(name){
this.name = name
}
var dogA = new Dog("测试"); // Dog {name:测试}
通过这种方式创建的dogA实例,有一个__proto__属性(3会解释这个属性)指向原型。内部使用this,该变量会绑定到实例对象上。
一般情况下,我们使用这种方式创建实例即可。但是现在有一个新的需求: 需要给Dog构造函数添加一个公共的属性type,所有通过该构造函数创建的实例共享这个属性。
例:
function Dog(name){
this.name = name;
this.type = "Dog"
}
var dogA = new Dog("测试A");
var dogB = new Dog("测试B");
console.log(dogA.type); // Dog
console.log(dogB.type); // Dog
dogA.type ="chanag type";
console.log(dogA.type); // change type
console.log(dogB.type); // Dog 期望是 change type
因此对于new 构造函数的方法,存在无法共享属性和方法的问题。
3. prototype
为了解决2中存在的问题, JavaScript引入了prototype属性,这个属性包含一个对象,我们称这个对象为prototype对象。所有实例对象需要共享的属性和方法,都放在这个对象里面;那些不需要共享的属性和方法,就放在构造函数里面。实例对象一旦创建,将自动引用prototype对象的属性和方法。也就是说,实例对象的属性和方法,分成两种,一种是本地的,另一种是引用的。
例:
function Dog(name){
this.name = name;
}
Dog.prototype={ type:"Dog" }
var dogA = new Dog("测试A");
var dogB = new Dog("测试B");
console.log(dogA.type); // Dog
console.log(dogB.type); // Dog
//dogA.type="change type"; //这种方式只是在dogA的实例上添加了一个属性,并不是修改公共属性
Dog.prototype.type = "change type"
//dogA.__proto__.type ="chanag type"; 与上面的方式效果相同
console.log(dogA.type); // change type
console.log(dogB.type); // change type
继承的几种方式
在了解了new 和 prototype之后,我们来看看JavaScript中继承的几种方式。
1. 构造函数绑定
使用call或apply方法,将父对象的构造函数绑定到子对象上。
例:
function Animal(){
this.species = "动物";
}
function Cat(name,color){
Animal.apply(this,arguments); //Cat继承Animal
this.name = name;
this.color = color;
}
var cat1 = new Cat("测试1",'红色');
console.log(cat1);
输出结果如下图所示:
image.png注意构造函数绑定的方法,不会继承 Animal.prototype上的属性和方法
2. prototype模式(组合继承)
该方式比较常见,如果“猫”的prototype对象,指向一个Animal实例,那么所有“猫”的实例,就能继承Animal了。
例:
function Animal(){
this.species = "动物";
}
function Cat(name,color){
this.name = name;
this.color = color;
}
Cat.prototype = new Animal();
var cat1 = new Cat("测试1",'黄色');
console.log(cat1);
image.png
注意,该方式创建的Cat实例的constructor指向的是Animal而非Cat。原来,任何一个prototype对象都有一个constructor属性,指向它的构造函数,而我们让Cat的prototype直接指向了一个Animal实例,因此Cat的构造函数变成了Animal。这显然会导致继承链的紊乱(cat1明明是用构造函数Cat生成的)
为了解决该问题,我们需要手动将constructor属性指回Cat构造函数
例:
function Animal(){
this.species = "动物";
}
function Cat(name,color){
this.name = name;
this.color = color;
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
var cat1 = new Cat("测试1",'黄色');
console.log(cat1);
image.png
3. 直接继承prototype
第二种方法中,每次都要新建一个Animal对象,比较浪费内存。因此我们可以将不变的属性直接写入Animal.prototype,然后让Cat()跳过Animal(),直接继承Animal.prototype。
例:
function Animal(){
}
Animal.prototype.species = "动物"
function Cat(name,color){
this.name = name;
this.color = color;
}
Cat.prototype = Animal.prototype;
var cat1 = new Cat("测试1",'黄色');
console.log(cat1);
image.png
参照2. prototype模式 ,我们可以手动把Cat.prototype的constructor指回Cat
Cat.prototype.constructor = Cat;
image.png
这种方法虽然效率高,但是也有缺点 ,Cat.prototype和Animal.prototype现在指向了同一个对象,那么任何对Cat.prototype的修改,都会反映到Animal.prototype(注意,方法2也有同样的问题,所有的Cat实例,共享了一个Animal实例)。 因此上面的代码中,已经将Animal.prototype对象的constructor属性改成了Cat
Cat.prototype.constructor = Cat;
console.log(Animal.prototype.constructor); // Cat
4. 利用空对象作为中介(寄生组合式继承)
第三种方法“直接继承prototype”存在上述的缺点,因此就有了第四种方法,利用一个空对象作为中介
function Animal(){}
Animal.prototype.species = "动物"
function Cat(name,color){
this.name = name;
this.color = color;
}
var F = function(){};
F.prototype = Animal.prototype;
Cat.prototype = new F();
Cat.prototype.constructor = Cat;
var cat1 = new Cat("测试1","黄色");
console.log(cat1);
image.png
我们将上面的方法,封装成一个函数,便于使用。(这实际上是ES5 Object.create的模拟实现,具体参照6. Object.create创建)
function extend(Child, Parent) {
var F = function(){};
F.prototype = Parent.prototype;
Child.prototype = new F(); //空对象,消耗较少
Child.prototype.constructor = Child;
Child.uber = Parent.prototype;
}
5. 拷贝继承
我们也可以通过将父对象的所有属性和方法拷贝进子对象来实现继承
例:
function Animal(){
}
Animal.prototype.species = "动物";
function Cat(name,color){
this.name = name;
this.color = color;
}
function copyExtend(Child,Parent){
let p = Parent.prototype;
let c = Child.prototype;
// 注意constructor属性不会被遍历到
for(let i in p ){
c[i] = p[i];
}
}
copyExtend(Cat,Animal);
let cat1 = new Cat("测试1",'红色');
console.log(cat1);
image.png
6. Object.create创建
ECMAScript 5 中引入了一个新方法: Object.create()。可以调用这个方法来创建一个新对象。新对象的原型就是调用create方法时传入的第一个参数
例
function Animal(){}
Animal.prototype.species = "动物";
let animal = new Animal();
let cat1= Object.create(animal);
cat1.name = "测试1";
cat1.color = "红色";
console.log(cat1);
image.png
注意这种方式创建的对象,constructor是Animal。 而且,包含引用类型的属性值始终都会共享相应的值。
例:
function Animal(){
this.name = 'animal';
this.testArray=['test1','test2','test3']
}
Animal.prototype.species = "动物";
let animal = new Animal();
let cat1= Object.create(animal);
cat1.name = 'cat1'
cat1.testArray.push("cat1");
console.log(cat1.name) ; // cat1
console.log(cat1.testArray); // ["test1", "test2", "test3", "cat1"]
let cat2 = Object.create(animal);
console.log(cat2.name ); //animal
console.log(cat2.testArray); // ["test1", "test2", "test3", "cat1"]
注意:修改cat1.name的值,cat2.name的值并未发生改变,并不是因为cat1和cat2有独立的 name 值,而是因为cat1.name = 'cat1',给cat1添加了 name 值,并非修改了原型上的 name 值。
7. 使用Class关键字创建
ECMAScript6引入了一套新的关键字用来实现class。
例:
class Animal{
constructor(){
console.log("Animal Constructor");
}
}
Animal.prototype.species = "动物";
class Cat extends Animal{
constructor(name,color){
super();
this.name = name;
this.color = color;
}
}
let cat1 = new Cat("测试1","红色")
console.log(cat1);
image.png
总结
JavaScript中的继承分为两种:
- 原型链继承(对象间的继承): 借助已有的对象创建新的对象,将子类的原型指向父类,就相当于加入了父类这条原型链(例如,方法2,3,4,6,7)
- 类式继承(构造函数间的继承): 在子类构造函数的内部调用超类的构造函数(例如1)。严格的类式继承并不是很常见,一般都是组合着用