注意:在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属性
为了不让父类构造函数将子类构造函数上的属性覆盖,我们可以先调用父类构造函数再给子类构造函数添加别的属性
经典继承的问题也就是构造函数模式的问题,即必须在构造函数中定义方法,函数不能复用,
子类也不能访问父类原型上定义的方法,所以经典继承也不能单独使用。
总结:
结合了原型链和经典继承
思路:使用原型链继承原型上的属性和方法,使用经典继承函数继承实例属性。
这样做既可以将方法定义在原型上以实现复用,又可以使每个实例拥有自己的属性
组合继承代码如下:
- 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()方法识别合成对象的能力。