1.原型链继承
-
套路
- 定义父类型构造函数
- 给父类型的原型添加方法
- 定义子类型的构造函数
- 创建父类型的对象赋值给子类型的原型
将子类型原型的构造属性设置为子类型
给子类型原型添加方法- 创建子类型的对象: 可以调用父类型的方法
-
关键
子类型的原型为父类型的一个实例对象
//父类型
function Supper() {
this.supProp = '父亲的原型链'
}
//给父类型的原型上增加一个[showSupperProp]方法,打印自身subProp
Supper.prototype.showSupperProp = function () {
console.log(this.supProp)
}
//子类型
function Sub() {
this.subProp = '儿子的原型链'
}
// 子类型的原型为父类型的一个实例对象
Sub.prototype = new Supper()
// 让子类型的原型的constructor指向子类型
// 如果不加,其构造函数找的[`new Supper()`]时从顶层Object继承来的构造函数,指向[`Supper()`]
Sub.prototype.constructor = Sub
//给子类型的原型上增加一个[showSubProp]方法,打印自身subProp
Sub.prototype.showSubProp = function () {
console.log(this.subProp)
}
var sub = new Sub()
sub.showSupperProp() //父亲的原型链
sub.showSubProp() //儿子的原型链
console.log(sub)
/**
Sub {subProp: "儿子的原型链"}
subProp: "儿子的原型链"
__proto__: Supper
constructor: ƒ Sub()
showSubProp: ƒ ()
supProp: "父亲的原型链"
__proto__: Object
*/
问题:
1.引用类型的属性被所有实例共享,包含引用类型的属性值始终都会共享相应的值
因为两个实例使用的是同一个原型对象,内存空间是共享的
举例:
function Parent () {
this.names = ['kevin', 'daisy'];
}
function Child () {
}
Child.prototype = new Parent();
var child1 = new Child();
child1.names.push('xiaofeixia');
console.log(child1.names); // ["kevin", "daisy", "xiaofeixia"]
var child2 = new Child();
console.log(child2.names); // ["kevin", "daisy", "xiaofeixia"]
- 某些属性其实是保存在父类型的实例对象上的;我们通过直接打印对象是看不到这个属性的;
3.无法传递参数
2.借用构造函数(经典继承)
套路:
- 定义父类型构造函数
- 定义子类型构造函数
- 在子类型构造函数中调用父类型构造
- 关键:
在子类型构造函数中通用call()调用父类型构造函数
- 作用:
能借用父类中的构造方法,但是不灵活
3.缺点:方法都在构造函数中定义,每次创建实例都会创建一遍方法。
function Person(name, age) {
this.name = name
this.age = age
}
function Student(name, age, price) {
//此处利用call(),将 [Student]的this传递给Person构造函数
Person.call(this, name, age)
// 相当于: this.Person(name, age)
/*this.name = name
this.age = age*/
this.price = price
}
var s = new Student('Tom', 20, 14000)
console.log(s.name, s.age, s.price)
//在[`Student`]中利用[`Person.call(this, name, age)`]改变了其this指向,所以可以实现此效果
父类原型对象中一旦存在父类之前自己定义的方法,那么子类将无法继承这些方法
相比原型链继承方式,父类的引用属性不会被共享,优化了第一种继承方式的弊端,但是只能继承父类的实例属性和方法,不能继承原型属性或者方法
所有的子类实例事实上会拥有两份父类的属性
一份在当前的实例自己里面(也就是person本身的),另一份在子类对应的原型对象中(也就是
person.__proto__里面);
当然,这两份属性我们无需担心访问出现问题,因为默认一定是访问实例本身这一部分的;
3.组合继承
优点:融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。
- 利用原型链实现对父类型对象的方法继承
- 利用super()借用父类型构建函数初始化相同属性
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.setName = function (name) {
this.name = name
}
function Student(name, age, price) {
Person.call(this, name, age) // 为了得到属性
this.price = price
}
Student.prototype = new Person() // 为了能看到父类型的方法
Student.prototype.constructor = Student //修正constructor属性
Student.prototype.setPrice = function (price) {
this.price = price
}
var s = new Student('Tom', 24, 15000)
s.setName('Bob')
s.setPrice(16000)
console.log(s.name, s.age, s.price)
4.原型式继承
function createObj(o) {
function F(){}
F.prototype = o;
return new F();
}
就是 ES5 Object.create 的模拟实现,将传入的对象作为创建的对象的原型。
也可以直接借助Object.create
方法实现普通对象的继承
利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型。
缺点:
包含引用类型的属性值始终都会共享相应的值,
var person = {
name: 'kevin',
friends: ['daisy', 'kelly']
}
var person1 = createObj(person);
var person2 = createObj(person);
person1.name = 'person1';
console.log(person2.name); // kevin
person1.firends.push('taylor');
console.log(person2.friends); // ["daisy", "kelly", "taylor"]
最终的结果:person1,person2 对象的原型指向了person对象;
注意:修改person1.name
的值,person2.name
的值并未发生改变,并不是因为person1
和person2
有独立的 name 值,而是因为person1.name = 'person1'
,给person1
添加了 name 值,并非修改了原型上的 name 值。
代码2:
let parent4 = {
name: "parent4",
friends: ["p1", "p2", "p3"],
getName: function() {
return this.name;
}
};
let person4 = Object.create(parent4);//借助Object.create方法实现普通对象的继承
person4.name = "tom";
person4.friends.push("jerry");
let person5 = Object.create(parent4);
person5.friends.push("lucy");
console.log(person4.name); // tom
console.log(person4.name === person4.getName()); // true
console.log(person5.name); // parent4
console.log(person4.friends); // ["p1", "p2", "p3","jerry","lucy"]
console.log(person5.friends); // ["p1", "p2", "p3","jerry","lucy"]
这里主要借助Object.create
方法实现普通对象的继承
5.寄生式继承
创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回对象。
function createObj (o) {
var clone = object.create(o);
clone.sayName = function () {
console.log('hi');
}
return clone;
}
缺点:跟借用构造函数模式一样,每次创建对象都会创建一遍方法。
代码2:
let parent5 = {
name: "parent5",
friends: ["p1", "p2", "p3"],
getName: function() {
return this.name;
}
};
function clone(original) {
let clone = Object.create(original);
clone.getFriends = function() {
return this.friends;
};
return clone;
}
let person5 = clone(parent5);
console.log(person5.getName()); // parent5
console.log(person5.getFriends()); // ["p1", "p2", "p3"]
寄生式继承在上面继承基础上进行优化,利用这个浅拷贝的能力再进行增强,添加一些方法
6.寄生组合式继承
现在我们来回顾一下之前提出的比较理想的组合继承
组合继承是比较理想的继承方式, 但是存在两个问题:
-
问题一: 构造函数会被调用两次: 一次在创建子类型原型对象的时候, 一次在创建子类型实例的时候.
-
问题二: 父类型中的属性会有两份: 一份在原型对象中, 一份在子类型实例中.
-
事实上, 我们现在可以利用寄生式继承将这两个问题给解决掉.
需要先明确一点: 当我们在子类型的构造函数中调用父类型.call(this, 参数)这个函数的时候, 就会将父类型中的属性和方法复制一份到了子类型中. 所以父类型本身里面的内容, 我们不再需要.
- 这个时候, 我们还需要获取到一份父类型的原型对象中的属性和方法.
- 能不能直接让子类型的原型对象= 父类型的原型对象呢?
- 不要这么做, 因为这么做意味着以后修改了子类型原型对象的某个引用类型的时候, 父类型原生对象的引用类型也会被修改.
- 我们使用前面的寄生式思想就可以了.
//定义寄生式核心函数
function inheritPrototype(subType, superType){
var prototype = Object.create(superType.prototype);
// 创建对象,创建父类原型的一个副本
prototype.constructor = subType;
// 增强对象,弥补因重写原型而失去的默认的constructor 属性
subType.prototype = prototype;
// 指定对象,将新创建的对象赋值给子类的原型
}
// 父类初始化实例属性和原型属性
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
};
// 借用构造函数传递增强子类实例属性(支持传参和避免篡改)
function SubType(name, age){
SuperType.call(this, name);
this.age = age;
}
// 将父类原型指向子类
inheritPrototype(SubType, SuperType);
// 新增子类原型属性
SubType.prototype.sayAge = function(){
alert(this.age);
}
var instance1 = new SubType("xyc", 23);
var instance2 = new SubType("lxy", 23);
instance1.colors.push("2"); // ["red", "blue", "green", "2"]
instance1.colors.push("3"); // ["red", "blue", "green", "3"]