前言
在js中继承也是面试中常考的问题,也算是比较烦人的东西,因为它的方法有很多种,今天小编就给大家来总结一下实现js继承的一些方法·。
继承实现的效果就是一个函数 (或对象) 可以从别的函数 (或对象) 那里继承到属性,也就是让子类的实例能够继承父类的属性和方法,比如下面,我可以打印出Tom才能认为继承到了
function Parent() {
this.name = 'Tom'
}
function Child() {
this.age = 18
}
let child = new Child()
console.log(child.name); // undefined
正文
法一、原型链继承
child实例对象寻找这个name属性,会先去构造函数的显示具有的属性上去找,发现没有后会去自己的隐式原型找,实例对象的隐式原型就是构造函数的显示原型,因此就会去Child的prototype身上找,欧克,已经有想法了,直接把Parent的实例赋给Child的显示原型上去,如下
function Parent() {
this.name = 'Tom'
}
Child.prototype = new Parent()
function Child() {
this.age = 18
}
let child = new Child()
console.log(child.name); // Tom
让一个构造函数的原型是另一个类型的实例,那么这个构造函数new出来的实例就具有该实例的属性。
当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。
但是这种写法有个缺点,子类实例会继承同一个原型对象,内存共享,所以实例之间会相互影响
另外,还有个问题,子类不能给父类传参
function Parent() {
this.name = 'Tom'
}
Child.prototype = new Parent()
function Child(age) {
this.age = age
}
let child = new Child(18, 'John') // 实现不了
console.log(child);
可以看到,父类中的name属性并没有被修改。
优点:写法方便简洁,容易理解。
缺点:对象实例共享所有继承的属性和方法。不能传递参数,子类无法给父类传参,因为这个对象是一次性创建的。
法二、构造函数继承
这个方法稍微有点巧妙,既然想要子类需要继承到父类的name属性,也就是说把this.name = 'Tom'这段代码放到Child中来,也就是说,让父类的this指向子类,我们可以用call显示绑定this的指向,在子类型构造函数的内部调用父类型构造函数;使用 apply() 或 call() 方法将父对象的构造函数绑定在子对象上。如下代码:
function Parent() {
this.name = 'Tom'
}
function Child() {
Parent.call(this) // this.name = 'Tom'
this.age = 18
}
let s1 = new Child()
console.log(s1.name); // Tom
可以看到,子类实例确实继承到了父类的属性,那么这个方法有没有刚刚那两个缺点呢?首先,如果是多个实例对象的话,this都是各自指向各自的,已经没有他们共用的原型了,因此没有第一个缺点,第二个缺点也没有,可以让子类给父类传参,如下
function Parent(name) {
this.name = name
}
function Child(name) {
Parent.call(this, name) // this.name = 'Tom'
this.age = 18
}
let s1 = new Child('Tom222')
console.log(s1.name); // Tom
但是这个方法却还有另外一个致命的缺点,就是它无法继承到父类构造函数原型上的属性,咱们看下面这段代码:
Parent.prototype.getName = function () {
return this.name
}
function Parent(name) {
this.name = name
}
function Child(name) {
Parent.call(this, name) // this.name = 'Tom'
this.age = 18
}
let s1 = new Child('Tom')
console.log(s1.getName()); // TypeError: c1.getName is not a function
可以看到,报错了,因为child和parent的原型是没有什么关系的,因此不会继承到它原型上的属性和方法。
优点:解决了原型链实现继承的不能传参的问题和父类的原型共享的问题。
缺点:无法继承父类原型上的属性
借用构造函数的缺点是方法都在构造函数中定义,因此无法实现函数复用。在父类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。
法三、组合继承
原型链继承可以解决继承原型的问题,构造函数继承可以解决传参和原型共享的问题,二者结合就是组合继承
Parent.prototype.getName = function () {
return this.name
}
function Parent(name) {
this.name = name
}
Child.prototype = new Parent()
function Child(name) {
Parent.call(this, name) // this.name = 'Tom'
this.age = 18
}
let child = new Child('John')
console.log(child.getName()); // John
但是这个方法有个小小的细节需要我们注意一下,就是constructor属性混乱了,这个属性就是让你来访问该对象是由谁创建的,
咱们清楚,实例对象p的constructor属性就是构造函数Parent,谁创建我constructor就是谁
但是咱们现在来看下这个方法下子类的constructor指向谁
what??!child实例居然是由Parent创建的,而不是Child……
按道理来说,实例child的原型里应该有constructor,但是没了,其实这是因为Child.prototype = new Parent()直接将Child的原型修改了,全部赋值成了Parent,所以就会产生这样一个问题,可能会导致一些潜在的bug
当然,这个问题也可以解决,直接把constructor给人家加上
Parent.prototype.getName = function () {
return this.name
}
function Parent(name) {
this.name = name
}
Child.prototype = new Parent()
Child.prototype.constructor = Child
function Child(name) {
Parent.call(this, name) // this.name = 'Tom'
this.age = 18
}
let child = new Child('John')
console.log(child.getName()); // John
可以看到,加上 Child.prototype.constructor = Child这句话后就没有缺点了,但是还是会有人觉得这个继承有两次调用父类一个new Parent,还有个Parent.call 性能开销大
优点: 解决了原型链继承和借用构造函数继承造成的影响。
缺点: 无论在什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部,性能开销大
法四、原型式继承
继承其实也可以发生在对象身上,就是Object.create()
这个方法我们以前在拷贝中用到过,他拷贝的对象,没有属性,是一个空对象,其属性全部拷贝到了原型身上去了,也可以说继承到原型上去了
同样,因为是原型继承,多个子类同样可以修改父类的引用类型的值
优点是:不需要单独创建构造函数。
缺点是:属性中包含的引用值始终会在相关对象间共享,子类实例不能向父类传参
法五、寄生式继承
let parent = {
name: 'Tom',
age: 40,
like: [1, 2]
}
function clone(obj) {
let clone = Object.create(obj)
clone.getLike = function() {
return this.like
}
return clone
}
let child = clone(parent)
let child2 = clone(parent)
寄生式继承同原型式继承,但是可以让子对象默认具有自己的属性,但是缺点和原型式继承一样
法六、寄生组合式继承
其实寄生组合继承就是用来优化组合继承的两次调用父类这个缺点
既然要去掉一个调用父类,那么一定是去掉new Parent,但是这样父类的原型就无法给到子类了,那就直接利用对象的继承Object.create来实现,将父类的原型传入进去,赋值给子类,这样就实现了让子类继承到父类的原型,同时不用多次调用父类
Parent.prototype.getName = function() {
return this.name
}
function Parent() {
this.name = 'Tom'
this.like = [1, 2, 3]
}
function Child() {
Parent.call(this)
this.type = 'children'
}
Child.prototype = Object.create(Parent.prototype) // new Parent()
Child.prototype.constructor = Child
let s1 = new Child()
let s2 = new Child()
这样既解决了组合式继承里面的父类构造函数要调用两遍,就会让父类里面的属性被子类显示继承一遍,隐式继承一遍,继承两遍的浪费的情况,又可以解决掉我们动子类构造函数的原型会影响父类的原型的问题。nice!这个方法不错。可以说是目前最优雅处理继承的方法了。
法七、class继承
只要让子类也能打印出Tom,就代表继承成功
class Parent {
constructor(name) {
this.name = name
}
getName() {
return this.name
}
}
class Child {
constructor() {
this.age = 18
}
}
let child = new Child('Tom')
console.log(child.name); // undefined
语法跟Java一样的,extends和super一起使用,super用于子类向父类传参,不过super要写上面去
class Parent {
constructor(name) {
this.name = name
}
getName() {
return this.name
}
}
class Child extends Parent{
constructor(name) {
super(name) // super让子类传参给父类
this.age = 18
}
}
let child = new Child('Tom')
console.log(child.name); // Tom
其实es6的class继承就是用寄生组合继承来打造的,child的constructor依旧是指向Child,没有缺陷
优点:语法简单易懂,操作更方便。
缺点:并不是所有的浏览器都支持class关键字 lass Per
总结
今天咱们聊到了js继承的七种方法,其中第六种在es6之前是最优雅的,也是面试官经常会问到的,它希望的也是你能答的是这个点,希望这篇文章对你有所帮助,可以点个免费的赞赞嘛,谢谢!gitee.com/Luo-zhao-fa…