其实在许多编程语言中,都有继承的身影,继承可以提高代码的复用性,并且可以扩展父类,但在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的默认原型对象。
但是这种方式其实会有很多弊端
//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 }
但是这种方式虽然解决了原型链继承的一些缺陷,但也有一些新的缺陷
// 借用构造函数也是有弊端:
// 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()
虽然这种继承方式解决了借用构造函数多次调用Person函数,但是也引出了一些新的问题.
/*
1.Student和Person共用一个原型对象,就会导致原本想绑定在Student的prototype上的方法会绑定到Student的prototype上,
如果有多个类都继承自Person,会导致原型对象非常混乱,甚至会覆盖原型对象上的方法
*/