1. 原型链继承
- 基本思路:利用原型让一个引用类型继承另一个引用类型的属性和方法。
- 原型链继承的基本模式:
function Parent() { // 超类构造函数
this.name = "xiaoming";
this.age = 18;
}
// 在原型上添加方法
Parent.prototype.sayParentName = function () {
console.log(this.name);
console.log(this.age);
};
function Child() { // 子类构造函数
this.name = "xiaoliang";
}
//1.此时把Child的原型重写了,换成了Parent的实例
//2.即:原来存在Parent的实例中的属性和方法,现在也存在Child.prototype中了
Child.prototype = new Parent();
console.log(Child.prototype.__proto__ == Parent.prototype); // true
//3.在继承了Parent实例中的属性和方法的基础上,又添加了属于自己的一个新方法(这里两个名字一样会覆盖)
Child.prototype.sayChildName = function () {
console.log(this.name);
}
// 创建子类的一个实例对象,
var person = new Child();
//所以现在person指向Child的原型,Child的原型指向Parent的原型(因为Child的原型对象等于了Parent的实例,这个实例指向Parent的原型)
console.log(person); // Child
person.sayChildName(); // xiaoliang
person.sayParentName(); // xiaoliang 18 调用这个方法的时候,name的值是Child中name,访问不到超类中的name属性
构造函数,原型,实例对象之间的关系:
上述代码的打印结果就是:
使用这种方式实现继承需要注意constructor的指向问题,Person.constructor 现在指向的是 Parent,因为 Child.prototype 中的 constructor 被重写了, 所以需要改变 constructor 的指向:
Child.prototype.constructor = Child;
再打印就是这样的:
所以,使用原型链的方式实现继承,完整代码如下:
// 原型链继承
function Parent() { // 超类构造函数
this.name = "xiaoming";
}
// 在原型上添加方法
Parent.prototype.sayParentName = function () {
console.log(this.name);
};
function Child() { // 子类构造函数
this.name = "xiaoliang";
}
//1.此时把Child的原型重写了,换成了Parent的实例
//2.换句话说,原来存在Parent的实例中的属性和方法,现在也存在Child.prototype中了
Child.prototype = new Parent();
Child.prototype.constructor = Child; // 手动修改构造器的指向
console.log(Child.prototype.__proto__ == Parent.prototype); // true
//3.在继承了Parent实例中的属性和方法的基础上,又添加了属于自己的一个新方法(这里两个名字一样会覆盖)
Child.prototype.sayChildName = function () {
console.log(this.name);
}
// 创建子类的一个实例对象
var person = new Child();
//所以现在person指向Child的原型,Child的原型指向Parent的原型(因为Child的原型对象等于了Parent的实例,这个实例指向Parent的原型)
console.log(person); //Child
需要注意的是:在给 Child 添加新的方法的时候,一定要在原型继承父元素后添加,这样可以防止自己定义的方法与继承来的方法名字相同时, 导致 Child 自己定的方法被覆盖。
-
原型链继承的缺点:
- 最主要的问题是包含引用类型值的原型, 因为包含引用类型值的原型属性会被所有的实例共享, 而在通过原型来实现继承的时候, 原型实际变成了另外一个函数的实例(这里边就有可能存在引用类型)。
function Parent() { this.colorArr = ["red", "black"]; // 数组,是引用类型的 } function Child() { this.color = "green"; } Child.prototype = new Parent(); Child.constructor = Child; var person1 = new Child(); person1.colorArr.push("blue"); // 实例对象person1在colorArr属性中添加了一个颜色 console.log(person1.colorArr); // ["red", "black", "blue"] var person2 = new Child(); // 创建了另一个实例对象, 但是这个实例对象中的colorArr属性也被改变了。 console.log(person2.colorArr); // ["red", "black", "blue"]
- 在创建子类型的实例时,不能向超类型的构造函数中传递参数。
所以结论是, 一般不会单独使用原型链继承。
2. 借用构造函数继承
- 基本思路: 在子类型的构造函数中调用超类型构造函数。
- 借用构造函数实现继承的基本模板:
function SuperType() {
this.colors = ["red", "blue", "green"];
this.sayHello = function () {
console.log('hello!')
}
}
SuperType.prototype.sayHi = function () {
console.log('hi');
};
function SubType() {
//继承了 SuperType
SuperType.call(this);
}
var p1 = new SubType();
console.log(p1);
p1.colors.push("black");
console.log(p1.colors); // ["red,blue,green,black"]
p1.sayHello(); // hello!
p1.sayHi(); // 报错,它只可以继承构造函数上的属性和方法,无法继承原型上的方法
var p2 = new SubType(); // 创建一个新的实例对象, 它的colors属性不会受到p1实例的影响。
console.log(p2.colors); // ["red,blue,green"]
补充:这里是对call方法的应用:call()
方法是javaScript中每个函数原型链上的方法。
call()
方法调用一个函数, 其具有一个指定的this
值和分别地提供的参数( 参数的列表 )。
call() 方法作用和apply()作用类似,唯一区别,call()接受若干参数,apply()接收一个包含若干参数的数组
它的基本语法是:fun.call(thisArg, arg1, arg2, ...)
,
其中:thisArg
表示在fun函数运行时指定的this
值。
关于call
有3个基本用法: 1. 借用构造函数继承函数的属性和方法; 2. 调用匿名函数 ; 3. 调用函数,并且指定执行上下文的this。
上述代码运行后的结果是:
3. 借用构造函数的方式实现继承它的优点:能够在子类型构造函数中向超类型构造函数添加参数:
// 借用构造函数继承
function SuperType(name) {
this.name = name; //
this.colors = ["red", "blue", "green"];
this.sayHello = function () {
console.log('hello!')
}
}
// 原型上添加方法
SuperType.prototype.sayHi = function () {
console.log('hi');
};
function SubType(name) {
//继承了 SuperType
SuperType.call(this,name); // 可以向超类中传入name参数,就可以访问超类中的name属性了
}
var p1 = new SubType('Milly'); // 可以从子类传入一个参数,在调用超类的构造函数的时候就可以访问到超类中的name属性了。
console.log(p1);
p1.colors.push("black");
console.log(p1.colors); //"red,blue,green,black"
p1.sayHello(); // hello!
console.log(p1.name); // Milly
代码的运行结果如下:
4. 借用构造函数实现继承的缺点:子类只能继承到超类构造函数中的属性和方法,定义在超类构造函数原型上的函数是没有办法继承到的,所以无法实现函数的复用。
所以结论是, 一般也很少单独借用构造函数实现继承。
3. 组合继承
- 基本思路:将原型链和借用构造函数的技术组合到一块,使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。
- 优点:既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。
- 组合继承的基本模板:
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function () {
console.log(this.name);
};
function SubType(name, age) {
//继承属性
SuperType.call(this, name);
this.age = age;
}
//继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
// 给实例添加方法,就有了它自己的方法 --- 注意写在继承之后
SubType.prototype.sayAge = function () {
console.log(this.age);
};
var instance1 = new SubType("james", 9);
instance1.colors.push("black");
console.log(instance1.colors); //"red,blue,green,black"
instance1.sayName(); // "james"
instance1.sayAge(); // 9
var instance2 = new SubType("kobe", 10);
console.log(instance2.colors); //"red,blue,green"
instance2.sayName(); // "kobe"
instance2.sayAge(); // 10
- 组合继承的缺点:调用了两次超类的构造函数,导致基类的原型对象中增添了不必要的超类的实例对象中的所有属性。
4. 原型式继承
-
基本思路:基于已有的对象来创建新的对象,实现的原理是,向函数中传入
一个对象,然后返回一个以这个对象为原型的对象。function object(o){ //传入一个对象,返回一个原型对象为该对象的新对象 function F(){}; F.prototype = o; // F的原型是对象o return new F(); // F是要返回的那个对象,那这个对象是F的一个实例? }
ES5中新增了
Object.create()
方法规范了原型式继承。这个方法接收两个参数,一个是将被用作新对象原型的对象,一个是为新对象定义额外属性的对象(可选)。 -
原型式继承与原型链继承的对比:
首先是原型链继承:
function Parent() { // 超类构造函数
this.name = "xiaoming";
this.color = 'red';
}
// 在原型上添加方法
Parent.prototype.sayParentName = function () {
console.log(this.name);
};
function Child() { // 子类构造函数
this.name = "xiaoliang";
this.age = 18;
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
console.log(Child.prototype.__proto__ == Parent.prototype); // true
Child.prototype.sayChildName = function () {
console.log(this.name);
}
// 创建子类的一个实例对象
var person1 = new Child();
var person2 = new Child();
console.log(person1); //Child
console.log(person1.name); //
console.log(person1.color); //red
原型式继承:
function Parent() { // 超类构造函数
this.name = "xiaoming";
this.color = 'red';
}
// 在原型上添加方法
Parent.prototype.sayParentName = function () {
console.log(this.name);
};
function Child() { // 子类构造函数
this.name = "xiaoliang";
this.age = 18;
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
console.log(Child.prototype.__proto__ == Parent.prototype); // true
Child.prototype.sayChildName = function () {
console.log(this.name);
}
// 创建子类的一个实例对象
var person1 = new Child();
var person2 = new Child();
console.log(person1); //Child
console.log(person1.name); // xiaoliang
console.log(person1.color); // undefined
person1.sayChildName(); // xiaoliang
person1.sayParentName(); // xiaoliang
console.log(person2);
- 原型式继承的缺点:与原型链的方式一样,存在无法传参的问题和属性共享的问题。