js继承

732 阅读7分钟

写在前面

  • 概念:如果你能通过某种方式,可以让某个对象访问到其他对象中的属性、方法,那么我们就把这种方式称之为继承。
  • 背景:有些对象会有方法,而这些方法都是函数(函数也是对象),如果把这些方法都放在构造函数中声明,则会产生内存浪费

// 等价关系
Person === Person.prototype.constructor
person1.__proto__ === person2.__proto__ === Person.prototype

Person.prototype === Person.prototype.constructor.prototype

// person1.constructor因为person1实例的原型对象是Person.prototype,所有person1继承了constructor,所有可以访问
person1.__proto__ === person1.constructor.prototype


注意js的继承都是建立在:方法在原型上创建、属性在实例上创建的前提下

1. 原型链继承

实现方式:让子类对象的原型指向父类的实例对象;也就是使用父类的实例对象重写子类的原型对象

原型链继承图解:

注意:由于SubType.prototype的constructor属性被重写为指向SuperType,所以instance.constructor也指向SuperType


// 示例:
function Parent() {
   this.isShow = true
   this.info = {
       name: "yhd",
       age: 18,
   };
}

Parent.prototype.getInfo = function() {
   console.log(this.info);
   console.log(this.isShow); // true
}

function Child() {};
Child.prototype = new Parent();

let Child1 = new Child();
Child1.info.gender = "男";
Child1.getInfo();  // {name: "yhd", age: 18, gender: "男"}

let child2 = new Child();
child2.getInfo();  // {name: "yhd", age: 18, gender: "男"}
child2.isShow = false

console.log(child2.isShow); // false
console.log(child1.isShow); // true 因为实例赋值与原型同名的非引用属性时,实例会自己创建一个自己的同名属性
1. 优点:父类的方法可以复用
2. 缺点:
    - 父类的所有引用属性(info)会被所有子类共享,更改一个子类的引用属性,其他子类也会受影响
    - 子类型实例不能给父类型构造函数传参

2. 构造函数继承

// 示例:
function Parent(name) {
    this.info = { name: name };
}
function Child(name) {
    //继承自Parent,并传参
    Parent.call(this, name);
    
     //实例属性
    this.age = 18
}

let child1 = new Child("yhd");
console.log(child1.info.name); // "yhd"
console.log(child1.age); // 18

let child2 = new Child("wxb");
console.log(child2.info.name); // "wxb"
console.log(child2.age); // 18
1. 优点:(修复了原型链继承的缺点)
    - 父类的引用属性不会被子类实例共享了
    - 子类可以向父类传参
2. 缺点:
    - 子类实例不能再访问父类原型上的方法了(因为构造函数继承模式下,父类和子类没有原型上的联系);因此所有方法属性都写在构造函数中,每次创建实例都会初始化,造成每份实例都有一份方法,内存浪费。

3. 组合继承(组合原型链继承和借用构造函数继承)(常用)

组合继承综合了原型链继承和盗用构造函数继承(构造函数继承),将两者的优点结合了起来

基本的思路就是使用原型链继承原型上的属性和方法,而通过构造函数继承实例属性,这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性

// 示例:
function Parent(age){
    this.colors = ['red', 'yellow']
    this.age = age
}

function Son(age, name){
    // 优点:子类可以给父类传参,并且保证父类中的引用属性子类不共享,因为call传入的this是每个子类自己的
    Parent.call(this, age)
    this.name = name
}

Parent.prototype.getAge = function(){
    console.log(this.age+'--'+this.name)
    console.log(this.colors)
}

// 这里可以不传参,这一步使得Son和Parent有原型联系,子类实例可以调用父类原型的方法
// 但是这一步也导致Son.prototype上也有一个colors
Son.prototype = new Parent()

let son1 = new Son(18, 'John')
let son2 = new Son(20, 'Bob')

son1.colors.push('pink')

son1.getAge() // 18--John ['red', 'yellow', 'pink']
son2.getAge() // 20--Bob ['red', 'yellow']

有个问题是: colors在原型上有一份,在子类的实例对象中也有一份

1. 优点:(弥补了原型链继承和构造函数继承的缺点,并采纳了两者的优点)
    - 父类原型上的方法可以复用
    - 子类可以向父类传参
    - 父类构造函数中的引用属性不会被共享
2. 缺点:
    - 调用了两次父类构造函数(耗内存):一次是在创建子类型原型的时候,另一次是在子类型构造函数内部
    - 子类对象有各自的引用属性colors外,原型对象上还多了一份colors属性

4. 原型式继承

背景:出发点是即使不自定义类型也可以通过原型实现对象之间的信息共享。所以才有了下面的object函数。节省了创建自定义类型这一步(感觉这样没啥意义)

使用场景:原型式继承非常适合只想让一个对象跟另一个对象建立继承这种关系,而不需要单独在外面再定义额外的构造函数。

// 示例:
function object(o){
    function F(){}
    F.prototype = o
    return new F()
}

let person = {
    name: '令狐冲',
    colors: ['red', 'yellow']
}
// 原型上的共享方法
person.say = function(){
    console.log(this.name)
}
let p1 = object(person)
let p2 = object(person)
p1.colors.push('pink')
p1.name = '任我行'
p2.colors.push('blue')

console.log(p1.name+' '+p1.colors)	// 任我行 red,yellow,pink,blue
console.log(p2.name+' '+p2.colors)	// 令狐冲 red,yellow,pink,blue
1. 优点:
    - 可以共用原型方法
2. 缺点:
    - 原型引用属性被共享
    - 子类不能向父类传参

ECMAScript 5通过增加Object.create()方法将原型式继承的概念规范化了。这个方法接收两个参数:作为新对象原型的对象,以及给新对象定义额外属性的对象(第二个可选)。在只有一个参数时,Object.create()与这里的object()方法效果相同。

Object.create()的第二个参数与Object.defineProperties()的第二个参数一样:每个新增属性都通过各自的描述符来描述。以这种方式添加的属性会遮蔽原型对象上的同名属性

// 示例:
let person = {
    name: '令狐冲',
    colors: ['red', 'yellow']
}

let p1 = Object.create(person, {
    name: {
        value: '鸠摩智'
    }
})

console.log(p1.name)	// 鸠摩智

5. 寄生式继承

寄生式继承是原型式继承的加强版。

function object(o){
    function F(){}
    F.prototype = o
    return new F()
}

function createAnother(origin){
  var clone=object(origin);
  // 这里增强对象,给对象绑定方法; 但是这个方法并不是绑定在原型上,而是绑定在实例,每个实例各一份say方法
  clone.say=function(){
    alert(this.name)
  }
  return clone;
}


let person = {
    name: '令狐冲',
    colors: ['red', 'yellow']
}

let p1 = createAnother(person)
let p2 = createAnother(person)
p1.colors.push('pink')
p1.name = '任我行'
p2.colors.push('blue')

console.log(p1.name+' '+p1.colors)	// 任我行 red,yellow,pink,blue
console.log(p2.name+' '+p2.colors)	// 令狐冲 red,yellow,pink,blue
console.log(p1.say())	// 任我行
console.log(p2.say())	// 令狐冲
// 优缺点
1. 优点:
    - 没有额外的创建自定义类型代码,而是将自定义类型代码`function F(){}`进行了整体的封装
2. 缺点:
    - 每个对象都有各自的say方法,造成内存浪费
    - 原型的引用类型被共享

6. 寄生组合式继承(常用)

寄生式组合继承可以算是引用类型继承的最佳模式;寄生组合式继承 == 寄生模式+组合模式

前面的组合继承缺点,使用Child.prototype = new Parent(),借助父类的实例对象作为中间值实现继承,但是导致的问题是,调用了两次父类的构造函数,并且子类原型对象上也有一份引用类型。

如果我们不使用 Child.prototype = new Parent() ,而是间接的让 Child.prototype 访问到 Parent.prototype 呢?

// 组合模式+3行代码改编
function Parent (name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function () {
    console.log(this.name)
}

function Child (name, age) {
    Parent.call(this, name);
    this.age = age;
}

// 注意下面这3行代码,代替了 Child.prototype = new Parent() 
// 借助了 `空对象 F` 作为父类和子类的中间连接点,这样就不会出现组合模式的弊端(在Child.prototype上增加不必要的多余属性,并且减少了Parent()的调用次数)
var F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();
///

var child1 = new Child('kevin', '18');

console.log(child1);

// 寄生组合式继承

function object(obj) {
  function Fun() { };
  Fun.prototype = obj;
  return new Fun();
}
// 寄生组合式继承的核心逻辑:在子类和父类之间创建一个空对象,形成一条原型链条
function inheritPrototype(child, parent) {
  let tempObj = object(parent.prototype);
  // 由于下面重写原型,所以手动设置constructor
  tempObj.constructor = child;
  child.prototype = tempObj;
}

function Parent(name) {
  this.name = name;
  this.friends = ["rose", "lily", "tom"]
}

Parent.prototype.sayName = function () {
  console.log(this.name);
}

function Child(name, age) {
  Parent.call(this, name);
  this.age = age;
}

inheritPrototype(Child, Parent);
Child.prototype.sayAge = function () {
  console.log(this.age);
}

let child1 = new Child("yhd", 23);
child1.sayAge(); // 23
child1.sayName(); // yhd
child1.friends.push("jack");
console.log(child1.friends); // ["rose", "lily", "tom", "jack"]

let child2 = new Child("yl", 22)
child2.sayAge(); // 22
child2.sayName(); // yl
console.log(child2.friends); // ["rose", "lily", "tom"]
1. 优点:
    - 只调用一次父类构造函数
    - Child可以向Parent传参
    - 父类方法可以复用
    - 父类的引用属性不会被共享

参考系列

juejin.cn/post/691421…

www.cnblogs.com/ranyonsue/p…

github.com/mqyqingfeng…