在ES6之前js的常见的6种继承方案(上)

111 阅读3分钟

其实在许多编程语言中,都有继承的身影,继承可以提高代码的复用性,并且可以扩展父类,但在js早期并没有实现继承,于是js社区就出现了很多种继承方案,今天我们就来聊一聊常见的几种继承方案


首先我们要先知道new操作符在执行的过程中会发生些什么

function Person(){}

/*const p1 = new Person()
1.在内存中创建一个新对象
const obj = {};
    
2.把新对象的原型指针指向构造函数的原型属性
obj.__proto__ = Fn.prototype;
    
3.通过call,apply等改变this的指向,并传入参数
Fn.call(obj,...args)

4.return创建出来的对象
return obj */

第一种 原型链的继承方案

// 父类: 公共属性和方法
function Person() {
  this.name = "mooo"
  this.friends = []
}

Person.prototype.speaking = function() {
  console.log(this.name + " speaking~")
}

// 子类: 特有属性和方法
function Student() {
  this.sno = 114514
}

var p = new Person()
Student.prototype = p

Student.prototype.studying = function() {
  console.log(this.name + " studying~")
}

var stu = new Student()
stu.speaking() // mooo speaking~

其实就是利用new Person()产生出的p对象的__proto__指向Person的 prototype,然后将Student的prototype指向p对象,这样Person的函数new出来的sut对象的__proto__,这样stu对象在调用方法时如果发现自己没有就会顺着__proto__往上找,找到Person的默认原型对象。

image.png 但是这种方式其实会有很多弊端

//1.打印stu对象, 继承的属性是看不到的
console.log(stu) //Person { sno: 114514 }
console.log(stu.name) // mooo

// 2.第二个弊端: 创建出来两个stu的对象
    var stu1 = new Student()
var stu2 = new Student()

// 直接修改对象上的属性, 是给本对象添加了一个新属性
stu1.name = "jack"
console.log(stu1.name) //jack
console.log(stu2.name) //mooo

// 获取引用, 修改引用中的值, 会相互影响
stu1.friends.push("peter")
console.log(stu1.friends) //[ 'peter' ]
console.log(stu2.friends) //[ 'peter' ]

// 3.第三个弊端: 在前面实现类的过程中都没有传递参数
var stu3 = new Student("peter", 114514)

第二种 借用构造函数方案

// 父类: 公共属性和方法
function Person(name, age, friends) {
  this.name = name
  this.age = age
  this.friends = friends
}

Person.prototype.eating = function() {
  console.log(this.name + " eating~")
}

// 子类: 特有属性和方法
function Student(name, age, friends, sno) {
  Person.call(this, name, age, friends)
  this.sno = sno
}

var p = new Person()
Student.prototype = p

Student.prototype.studying = function() {
  console.log(this.name + " studying~")
}


var stu = new Student("mooo", 18, ["peter"], 111)
console.log(stu)//Person { name: 'mooo', age: 18, friends: [ 'peter' ], sno: 111 }

image.png 但是这种方式虽然解决了原型链继承的一些缺陷,但也有一些新的缺陷

// 借用构造函数也是有弊端:
// 1.第一个弊端: Person函数至少被调用了两次
     (1)一次在创建子类原型的时候;
     (2)另一次在子类构造函数内部(也就是每次创建子类实例的时候);
// 2.第二个弊端: stu的原型对象上会多出一些属性, 但是这些属性是没有存在的必要
console.log(p)
/*
Person {
  name: undefined,
  age: undefined,
  friends: undefined,
  studying: [Function (anonymous)]
}*/

第三种 父类原型赋值给子类方案

// 父类: 公共属性和方法
function Person(name, age, friends) {
  this.name = name
  this.age = age
  this.friends = friends
}

Person.prototype.eating = function() {
  console.log(this.name + " eating~")
}

// 子类: 特有属性和方法
function Student(name, age, friends, sno) {
  Person.call(this, name, age, friends)
  this.sno = sno
}

// 直接将父类的原型赋值给子类, 作为子类的原型
Student.prototype = Person.prototype

Student.prototype.studying = function() {
  console.log(this.name + " studying~")
}


var stu = new Student("mooo", 114514, ["peter"], 114514)
console.log(stu)
/*Person { name: 'mooo', age: 114514, friends: [ 'peter' ], sno: 
114514 }*/

stu.eating()

image.png 虽然这种继承方式解决了借用构造函数多次调用Person函数,但是也引出了一些新的问题.

/* 
1.Student和Person共用一个原型对象,就会导致原本想绑定在Student的prototype上的方法会绑定到Student的prototype上,
如果有多个类都继承自Person,会导致原型对象非常混乱,甚至会覆盖原型对象上的方法
 */