最近在阅读红宝书,刚好重新过来一遍继承,拿来和大家分享下,继承是JS核心之一,面试必问。
在继承之前大家需要了解的就是js创建对象,常见的创建对象方式有工厂模式,构造模式,原型模式。
**理解原型对象**
1.只要创建了一个函数,就会为该函数创建一个prototype属性,指向函数的原型对象。
2.原型对象会自动获得一个constructor属性,指向所在的构造函数。
3.每个对象实例都包含一个内部属性,该属性指向原型对象
4.通过构造函数,还可以继续为原型对象添加其他属性和方法。
5.当给对象实例添加一个属性时,这个属性会屏蔽原型对象中保存的同名属性,
即添加这个属性只会阻止我们访问原型中的属性,而不会修改那个属性
最主要的创建对象的方法
组合使用构造函数模式和原型模式
创建自定义类型最常见的方式,定义引用类型的一种默认模式。
思想:
构造函数模式:用于定义实例属性
原型模式:用于定义方法和共享的属性
每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。支持向构造函数传递参数。
function Person(name,age){
this.name = name;
this.age = age;
this.friends = ["小红","小明"];
}
Person.prototype = {
constructor : Person,
getAge : function(){
console.log(this.age);
}
}
var zhangsan = new Person("zhangsan",23);
var lisi = new Person("lisi",30);
zhangsan.friends.push("小三");
console.log(zhangsan.friends); //["小红","小明","小三"]
console.log(lisi.friends); //["小红","小明"]
console.log(zhangsan.friends === lisi.friends); //false
console.log(zhangsan.getAge === lisi.getAge); //true
有了以上这些知识我们再来看继承
由于函数没有签名,在ECMAScript中无法实现接口继承,只支持实现继承,而实现继承主要是依靠原型链来实现。
继承:
原型链 借用构造函数 组合继承 原型式继承 寄生式继承 寄生组合式继承
JavaScript主要通过原型链实现继承。使用最多的是组合继承。
原型链:
通过将一个类型的实例赋值给另一个构造函数的原型实现。(问题对象实例共享所有继承的属性和方法,不适宜单独使用)
借用构造函数:
在子类型构造函数的内部调用超类型构造函数。(每个实例都具有自己的属性)
组合继承:
使用原型链继承共享的属性和方法,通过借用构造函数继承实例属性。
原型式继承:
可以不必预先定义构造函数的情况下继承(执行对给定对象的浅复制,复制得到的副本还可以进一步改造)
寄生式继承:
与原型式继承非常相似,也是基于某个对象或某些信息创建一个对象,然后增强对象,返回对象。(为了解决组合继承由于多次调用超类构造函数而导致低效率问题,可将这个模式与组合继承一起使用。)
寄生组合式继承:
集寄生式继承和组合继承的优点与一身,是实现基于类型继承的最有效方式。
原型链
每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数想指针(constructor),而实例对象都包含一个指向原型对象的内部指针(proto)。如果让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针(proto),另一个原型也包含着一个指向另一个构造函数的指针(constructor)。假如另一个原型又是另一个类型的实例……这就构成了实例与原型的链条。
function animal(){
this.type = "animal";
}
animal.prototype.getType = function(){
return this.type;
}
function dog(){
this.name = "dog";
}
dog.prototype = new animal();
dog.prototype.getName = function(){
return this.name;
}
var xiaohuang = new dog();
//原型链关系
xiaohuang.__proto__ === dog.prototype
dog.prototype.__proto__ === animal.prototype
animal.prototype.__proto__ === Object.prototype
Object.prototype.__proto__ === null
缺点:
包含引用类型值的原型。
在创建子类型的实例时,不能向超类型的构造函数中传递参数。(没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数)
借用构造函数
优点:
解决原型中包含引用类型值带来的问题。
可以在子类型构造函数中向超类型构造函数传递参数。
思想:
在子类构造函数内部调用超类构造函数。
通过apply()和call()方法可以在新建的对象上执行构造函数。
function Animal(){
this.colors = ["red","blue"];
}
function Dog(){
//继承Animal
Animal.call(this);
}
var xiaohuang = new Dog();
xiaohuang.colors.push("yellow");
console.log(xiaohuang.colors): // ["red","blue","yellow"]
var xiaohei = new Dog();
xiaohei .colors.push("black");
console.log(xiaohei .colors); // ["red","blue","black"]
//Dog的每个实例都会有自己的colors属性副本
//传递参数
//优点:在子类构造函数中向父类构造函数传递参数(相比于原型链的优点)
function Animal(name){
this.name = name;
}
function Dog(){
//继承Animal,同时还传递了参数
Animal.call(this,"xiaohuang");
this.age = 10; //实例属性
}
var xiaohuang = new Dog();
console.log(xiaohuang .name); //"xiaohuang”
console.log(xiaohuang .age); //10
缺点:
构造函数模式存在的问题—方法都在构造函数中定义,函数复用无从谈起。
在超类型的原型中定义的方法,在子类型是不可见的。
结果所有的类型都只能使用构造函数模式
借用构造函数很少单独使用
组合继承
组合继承/伪经典继承–最常用的继承模式
优点:
将原型链和借用构造函数组合到一起,发挥两者之长。
思想:
使用原型链实现对原型属性和方法的继承,(实现了函数的复用)
通过借用构造函数实现对实例属性的继承 (保证每个实例都有自己的属性)
function Animal(name){
this.name = name;
this.colors = ["red","blue"];
}
Animal.prototype.getName = function(){
console.log(this.name);
}
function Dog(name,age){
//继承属性
Animal.call(this,name);
this.age = age;
}
Dog.prototype = new Animal();
Dog.prototype.constructor = Dog;
Dog.prototype.getAge = function(){
console.log(this.age);
}
var xiaohuang = new Dog("xiaohuang",10);
xiaohuang.colors.push("yellow");
console.log(xiaohuang.colors); //"red","blue","yellow"
xiaohuang.getAge(); //10
xiaohuang.getName(); //"xiaohuang"
var xiaohei = new dog("xioahei",3);
console.log(xiaohei.colors); //"red","blue"
xiaohei.getAge(); //3
xiaohei.getName(); //"xioahei"
解释:
animal的构造函数定义了name和colors两个属性
dog的原型定义了getName()方法。
dog构造函数在调用animal构造函数时传入了name参数,
又定义了自己的属性age
将animal实例的赋值给dog的原型,又在新原型上添加方法getAge(),两个不同的实例可以分别拥有自己的属性name,colors,又可以使用相同的方法getName()
instanceof,isPrototypeOf()也能够识别基于组合继承创建的对象。
缺点:
无论什么情况下,都会调用两次超类型构造函数。
(1.创建子类原型的时候 2.子类型构造函数内部) 看 寄生组合式继承
有两组 name和colors属性,dog的原型中有,dog的实例中有,实例中会屏蔽原型的的两个同名属性。
原型式继承
思想:
没有严格意义上的构造函数。
借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。
必须要有一个对象可以作为另一个对象的基础。
function object(o){
function F(){};
F.prototype = o;
return new F();
};
//object对传入的对象进行了一次浅复制
var xiaoming = {
name : "xiaoming",
friends : ["f1","f2"]
};
var xiaohong = object(xiaoming);
xiaohong.__proto__ === xiaoming; //true
xiaohong.name = "xiaohong";
xiaohong.friends.push("f3");
var xiaohua = "xiaohua";
xiaohua.friends.push("f4");
console.log(xiaoming.friends);//["f1","f2","f3","f4"]
缺点 :
和原型模式一样,包含引用类型值的属性始终都会共享相应的值
寄生式继承
思路:
与寄生构造函数和工厂模式类似,
创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象。
function createAnother(o){
var clone = object(o); //通过调用函数创建一个新对象
clone.sayHi = function(){ //以某种方式来增强这个对象
alert("hi");
};
return clone; //返回这个对象
}
var person = {
name : "fangfang",
friends : ["f1","f2"]
};
var anotherperson = createAnother(person);
anotherperson.sayhi(); //"hi"
//anotherperson具有person的所有属性和方法,还有自己的sayhi方法
缺点:
适用场景:在主要考虑对象,而不是自定义类型和构造函数的情况
寄生组合式继承
最理想的继承方式
优点:
不必为了指定子类型的原型而调用父类型的构造函数(解决了组合继承至少两次调用超类构造函数)
(只调用了一次animal构造函数,避免了dog.prototype上面创建多余的属性)
(还能正常使用instanceof和isPrototypeOf())
思想:
通过借用构造函数来继承属性,
通过原型链的混成形式来继承方法
不必为了指定子类型的原型而调用父类型的构造函数,
我们所需的只是父类原型的一个副本。
就是使用寄生式继承来继承父类的原型,然后将结果指定给子类型的原型
function inheritPrototype(Dog,Animal){
var prototype = Object(Animal.prototype); //创建父类原型的一个副本
prototype.constructor = Dog; //副本添加constructor属性
Dog.prototype = prototype; //把副本赋值给子类原型
}
function Animal(name){
this.name = name;
this.colors = ["red","blue"];
}
Animal.prototype.getName = function(){
console.log(this.name);
}
function Dog(name,age){
Animal.call(this,name);
this.age = age;
}
inheritPrototype(Dog,Animal);
Dog.prototype.getAge = function(){
console.log(this.age);
}
//此时 Animal.prototype === Dog.prototype