注意:在JS里面是没有子类和父类的概念的,在这里只是为了方便才简称为子类与父类。我们应该完整的将其描述为子类构造函数与父类构造函数。
原型链
构造函数、原型和实例的关系:每个构造函数都有一个原型对象(prototype),原型有一个属性(constructor)指回构造函数,而实例有一个内部指针(__proto__)指向原型。
如果原型是另一个类型的实例呢?那就意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。这样就在实例和原型之间构造了一条原型链。
实现继承是 ECMAScript 唯一支持的继承方式,而这主要是通过原型链实现的。
原型链实现代码如下:
// 创建Animal
function Animal(){
this.name = 'Animal';
}
// prototype
Animal.prototype.getAnimalName = function(){
console.log(this.name + 'getAnimalName');
}
// 创建Dog
function Dog(){
this.name = "dog";
}
// 这一句将Animal继承给Dog
// 将Animal的实例赋值给Dog的prototype对象,相对于将Animal的实例中的__proto__赋值给Dog的prototype对象
// 如此一来,就能通过Animal的[[prototype]](__proto__)来访问到Dog的原型对象中的属性与方法
Dog.prototype = new Animal()
// 不建议使用Animal.__proto__ === Dog.prototype
// 在使用原型链继承时,要先继承再在自己的原型对象里定义自己的属性和方法
Dog.prototype.getDogName = function(){
console.log(this.name + 'getDogName');
}
var dog1 = new Dog();
dog1.getAnimalName();
dog1.getDogName();
注意:getAnimalName() 是一个方法,在Animal.protptype对象上,而name是一个属性在Dog.prototype上,因为getAnimalName是一个原型方法,name是一个实例属性
在这里我们是定义了两个类型Animal与Dog
并将Animal的实例赋值给Dog,于是Dog.prototype实现了对Animal的继承
这样赋值重写了Dog最初的原型对象,使其变成了Animal的实例
这样写使得Animal可以访问的属性和方法都存在于Dog.prototype
在这样实现了继承之后我们又向 Dog.prototype 里面添加了一个 getDogName 的方法
最后创建Dog的实例 dog1 并调用其继承的 getAnimalName 方法
dog1通过内部的[[prototype]]属性指向Dog.prototype,
Dog又通过内部的[[prototype]]属性指向Animal.prototype
由于将Dog.prototype的constructor属性指向了Animal,因此dog1的constructor也指向了Animal
可以通过修改 Dog.prototype.constructor 来改变指向,使其重新指回Dog
当我们想要读取实例上的属性时,会首先在实例上进行搜索,
如果没有找到,就往上搜索实例的原型
如果还没有找到就继续往上搜索原型的原型(前提是完成了原型链的继承)
dog1.getAnimalName()总共进行了三步搜索:dog1--Dog.prototype--Animal.prototype
对属性和方法的搜索会一直持续到原型链的末端
默认原型
在原型链中还有一环,就是在默认情况下所有的引用类型都继承自Object
任何函数的默认原型都是Object的一个实例,意味着实例内部还有一个内部指针指向Object.prototype
这也就是为什么自定义类型能够继承包括 toString()、valueOf()在内的所有默认方法的原因。
原型与继承关系
在上面的案例中,我们可以用instanceof运算符用于检测构造函数的protptype属性是否在某个实例的原型链上:
// instanceof运算符用于检测构造函数的protptype属性是否在某个实例的原型链上
console.log(dog1 instanceof Object); // true
console.log(dog1 instanceof Animal); // true
console.log(dog1 instanceof Dog); // true
// isPrototypeOf() 原型链中每个原型都可以调用这个方法,若原型链中包含这个原型,返回true
console.log(Object.prototype.isPrototypeOf(dog1));
console.log(Animal.prototype.isPrototypeOf(dog1));
console.log(Dog.prototype.isPrototypeOf(dog1));
让子类上的方法覆盖父类上的方法,或者在子类里添加新的方法:
// 若要让子类上的方法覆盖父类上的方法,或者在子类添加新方法,则需要先给在原型赋值之后再添加到原型上
function Animal(){
this.name = "animal";
}
Animal.prototype.getAnimalName = function(){
console.log(this.name + "getAnimalName");
}
var test = new Animal();
test.getAnimalName(); // animalgetAnimalName
function Dog(){
this.name = "dog";
}
Dog.prototype = new Animal();
Dog.prototype.getDogName = function(){
console.log(this.name + "getDogName");
}
Dog.prototype.getAnimalName = function(){
console.log(this.name + "我被覆盖了");
}
var dog2 = new Dog();
dog2.getAnimalName(); // dog我被覆盖了
dog2.getDogName(); // doggetDogName
getDogName()方法是 Dog 的新方法
而后一个 getAnimalName()是原型链上已经存在但在这里被遮蔽的方法
后面在Dog实例上用的就是后面这个被改写过的方法
而第一次Animal的实例执行的仍然是最初的原型里的方法
重点在于上述两个方法都是在把原型赋值为 Animal 的实例之后定义的。
原型链的破坏:
以对象字面量形式创建原型方法就可以破坏原型链:
// 只要以对象字面量的形式创建原型方法就可以破坏原型链,因为这相当于重写了原型链
function Animal() {
this.name = "animal";
}
Animal.prototype.getAnimalName = function () {
console.log(this.name + "getAnimalName");
};
function Dog(){
this.name = "dog";
}
// 继承
Dog.prototype = new Animal();
// 以对象字面量形式创建原型方法
Dog.prototype = {
getDogName(){
console.log(this.name + "getDogName");
},
someOtherMethod() {
return false;
}
};
var dog = new Dog();
dog.getDogName(); // doggetDogName
dog.getAnimalName(); // 报错!
在这里,Dog被赋值为一个Animal的实例后,又被一个对象字面量覆盖了
覆盖后的原型是一个Object的·实例,而不是Animal的实例
因此原先的原型链就断了,Dog和Animal也就没了联系
原型链的问题:
- 类似于原型模式的问题, 原型中包含的引用值会在所有实例间共享
function Animal(){
this.category = ['cat', 'rabbit']
}
function Dog(){};
// 继承
Dog.prototype = new Animal();
var dog1 = new Dog();
dog1.category.push('piggy');
console.log(dog1.category); // [ 'cat', 'rabbit', 'piggy' ]
var dog2 = new Dog();
console.log(dog2.category); // [ 'cat', 'rabbit', 'piggy' ]
在这里我们在Animal构造函数里面定义了一个category属性,并传入一个数组(引用值)
因此每个Animal的实例里都会有一个category属性,包括其中的数组
当我们在后面完成了Dog原型对Animal的继承时,Dog.prototype也成为了一个Animal的实例,获得了category属性
也就相当于创建了一个Dog.prototype.category属性
结果Dog的每个实例都会共享这个category属性,因此我们在修改dog1.category时也会修改dog2.category
原型链的第二个问题是:
- 无法在不影响所有对象实例的情况下将参数传入父类的构造函数里
盗用构造函数/对象伪装/经典继承
这是为了解决原型的包含引用值的问题。
思路:在子类构造函数中调用父类构造函数。
因为函数就是在特定上下文执行代码的简单对象,因此我们可以使用call或者apply方法以新创建的对象为上下文执行代码
function Animal(){
this.category = ['cat', 'rabbit']
}
function Dog(){
// 继承
Animal.call(this)
};
var dog1 = new Dog();
dog1.category.push('piggy');
console.log(dog1.category); // [ 'cat', 'rabbit', 'piggy' ]
var dog2 = new Dog();
console.log(dog2.category); // [ 'cat', 'rabbit' ]
var dog1 = new Dog();是dog1调用Dog构造函数,因此其内部的this指向dog1,
Animal.call(this)相当于是Animal.call(dog1),即dog1.Animal()
当dog1调用Animal里的方法时,Animal内部的this就指向了dog1
因此Animal上所有的属性和方法都被拷贝到了dog1上面
所以每个实例都会有自己的category属性副本,互不影响
经典继承还可以向父类构造函数内传递参数:
// 经典继承还可以向父类构造函数内传递参数
function Animal(name){
this.name = name;
}
function Dog(){
Animal.call(this,'wangwang');
this.age = 3;
}
var dog = new Dog();
console.log(dog.name); // wangwang
console.log(dog.age); // 3
在这里我们向Animal里传递了一个参数name,然后将它赋值给一个属性
在Dog构造函数调用Animal构造函数时同时将这个参数传入,实际上也会在Dog构造函数里定义一个name属性
为了不让父类构造函数将子类构造函数上的属性覆盖,我们可以先调用父类构造函数再给子类构造函数添加别的属性
经典继承的问题
经典继承的问题也就是构造函数模式的问题,即必须在构造函数中定义方法,函数不能复用,
子类也不能访问父类原型上定义的方法,所以经典继承也不能单独使用。
总结:
- 创建的实例只是子类的实例,不是父类的实例
- 没有拼接原型链,不能使用instanceof(),因为子类只继承了父类里的属性和方法,没有继承父类原型对象里的属性和方法
- 每个子类实例都保存了父类的实例方法副本,浪费内存影响性能,而且不能实现父类实例方法的复用
组合继承(伪经典继承)
结合了原型链和经典继承
思路:使用原型链继承原型上的属性和方法,使用经典继承函数继承实例属性。
这样做既可以将方法定义在原型上以实现复用,又可以使每个实例拥有自己的属性
组合继承代码如下:
function Animal(name){
this.name = name;
this.category = ['cat', 'rabbit']
}
Animal.prototype.sayName = function(){
console.log(this.name);
}
function Dog(name, age){
// 继承属性
Animal.call(this,name);
this.age = age;
}
Dog.prototype = new Animal();
Dog.prototype.sayAge = function(){
console.log(this.age);
}
// 继承方法
var dog = new Dog("love", 11);
dog.category.push('pig');
console.log(dog.category);
dog.sayName();
dog.sayAge();
var doggy = new Dog("sick", 9);
console.log(doggy.category);
doggy.sayName();
doggy.sayAge();
在这个例子里,我们在Animal的构造函数里定义了两个属性name和category
在Animal的原型对象里定义了方法sayName()
Animal.call(this,name);使Dog构造函数调用了Animal构造函数,并传入name属性
然后又额外定义了一个age属性
然后,Dog.prototype 也被赋值成为Animal的实例,原型实例被赋值后又添加了一个叫sayAge()的方法
这样,我们就可以创建两个Dog实例,让这两个实例都有自己的属性,同时共享相同的方法
组合继承弥补了原型链和经典继承函数的不足,是 JavaScript 中使用最多的继承模式。
而且组合继承也保留了 instanceof 操作符和 isPrototypeOf()方法识别合成对象的能力。