最近在面试中被问到js的继承,当时回答的不太好,所以今天特别总结一下。
我们先来看一个基于原型链的继承例子
//父类 function Person(){} //子类 function Student(){} //继承 Student.prototype = new Person();
我们只要把子类的prototype设置为父类的实例,就完成了继承,也就是js里面的原型链继承。
接下来我们加上一点东西
//父类 function Person(name,age){ this.name = name || 'unknow' this.age = age || 0 } //子类 function Student(name){ this.name = name this.score = 80 } //继承 Student.prototype = new Person(); var stu = new Student('lucy'); console.log(stu.name); //lucy --子类覆盖父类的属性 console.log(stu.age); // 0 --父类的属性 console.log(stu.score); // 80 --子类自己的属性
基于上面的代码,我们再给父类和子类分别加一个方法
//父类 function Person(name,age){ this.name = name || 'unknow' this.age = age || 0 } Person.prototype.say = function(){ console.log('i am a person'); } //子类 function Student(name){ this.name = name this.score = 80 } //继承 Student.prototype = new Person(); Student.prototype.study = function(){ console.log('i am studing'); } var stu = new Student('lucy'); console.log(stu.name); //lucy --子类覆盖父类的属性 console.log(stu.age); // 0 --父类的属性 console.log(stu.score); // 80 --子类自己的属性 stu.say(); // i am a person --继承自父类的方法 stu.study(); // i am studing --子类自己的方法
到这里就完成了一个原型链继承,是不是觉得很简单。但是原型链继承有一个缺点,就是如果属性是引用类型的话,会共享引用类型,请看下面代码
//父类 function Person(){ this.hobbies = ['music','reading'] } //子类 function Student(){ } //继承 Student.prototype = new Person(); var stu1 = new Student(); var stu2 = new Student(); stu1.hobbies.push('basketball'); console.log(stu1.hobbies); // ["music", "reading", "basketball"] console.log(stu2.hobbies); // ["music", "reading", "basketball"]
我们可以看到,当我们改变stu1的引用类型的属性时,stu2对应的属性也会跟着更改,这就是原型链继承的缺点—引用属性会被所有实例共享。
那我们如何解决这个问题呢?就是下面我们要提到的借用构造函数继承,我们来看一下使用构造函数继承的最简单例子:
//父类 function Person(){ this.hobbies = ['music','reading'] } //子类 function Student(){ Person.call(this); } var stu1 = new Student(); var stu2 = new Student(); stu1.hobbies.push('basketball'); console.log(stu1.hobbies); // ["music", "reading", "basketball"] console.log(stu2.hobbies); // ["music", "reading"]
这样,我们就解决了引用类型被所有实例共享的问题了
注意:这里跟原型链继承有个比较明显的区别是并没有使用prototype继承,而是在子类里面执行父类的构造函数。相当于把父类的代码复制到子类里面执行一遍,这样做的另一个好处就是可以给父类传参。
//父类 function Person(name){ this.name = name; } //子类 function Student(name){ Person.call(this,name); } var stu1 = new Student('lucy'); var stu2 = new Student('lili'); console.log(stu1.name); // lucy console.log(stu2.name); // lili
构造函数解决了引用类型被所有实例共享的问题,但正是因为解决了这个问题,导致一个很矛盾的问题出现了—函数也是引用类型,也没办法共享了。也就是说,每个实例里面的函数,虽然功能一样,但是却不是一个函数,就相当于我们每实例化一个子类,就复制了一遍函数代码。
//父类 function Person(name){ this.say = function() {}; } //子类 function Student(name){ Person.call(this,name); } var stu1 = new Student('lucy'); var stu2 = new Student('lili'); console.log(stu1.say === stu2.say); // false
以上代码说明父类的构造函数,在子类的实例下是不共享的。
总结:
继承方式 继承的核心代码 优缺点
原型继承 Student.prototype = new Person() 实例的引用类型共享
构造函数继承 在子类(Student)里执行Person.call(this) 实例的引用类型不共享
从上表我们可以看出原型继承和构造函数继承这两种方式的优缺点刚好是互相矛盾的,那么我们有没有方法可以鱼和熊掌兼得呢?
接下来就是组合继承登场了,组合继承就是各取上面2种继承的长处,普通属性使用构造函数继承,函数使用原型链继承。接下来就看代码吧:
//父类 function Person(name){ this.hobbies = ['music','reading']; } Person.prototype.say = function(){ console.log('i am a person'); } //子类 function Student(name){ Person.call(this); //构造函数继承(继承属性) } Student.prototype = new Person(); //原型继承(继承方法) var stu1 = new Student('lucy'); var stu2 = new Student('lili'); stu1.hobbies.push('basketball'); console.log(stu1.hobbies); // ["music", "reading", "basketball"] console.log(stu2.hobbies); // ["music", "reading"] console.log(stu1.say === stu2.say); // true
这样我们就既能实现属性的独立,又能做到函数的共享。
至此,我们就把js里面的常用继承了解完了,总结一下:
原型链继承,会共享引用属性
构造函数继承,会独享所有属性,包括引用属性(重点是函数)
组合继承,利用原型链继承要共享的属性,利用构造函数继承要独享的属性,实现相对完美的继承
了解js继承的同学可能知道继承还有其他方式,比如原型式继承、寄生式继承、寄生组合继承等。今天在这里记录的是比较常用的3种继承方式,其他剩余的继承方式以后再学习。
————————————————
原文链接:https://blog.csdn.net/zhang070514/article/details/83933489
4.原型式继承:
原型式继承的基本思想:
1.一个基础对象
2.一个新对象。把基础对象作为原型对象
3.新对象实例
var person = { name: "蓝灯", arr: [1,2,3] } //Object.create()创建新对象,传入基础对象 var son1 = Object.create(person) son1.name = "AAA" son1.arr.push(4) console.log(son1.name) //AAA var son2 = Object.create(person) son2.name = "BBB" console.log(son2.name) //BBB console.log(son2.arr) //1,2,3,4,引用类型问题依然存在 //当然你也可以使用Object.create()的第二个参数传添加对象属性 var son3 = Object.create(person,{ name:{ value:'CCC' } }) console.log(son3.name) //CCC
-
原型式继承解决了原型链无法传参的问题,并且无需使用构造函数(避免了构造函数的问题)。因此在没必要使用构造函数时可以采用这种方法。
-
引用类型问题依旧存在
5. 寄生式继承
寄生式继承可以理解为是原型式继承的增强。在原型式继承中我们创建了一个新对象,寄生式继承便是在新对象中添加方法,以增强对象。
var person = { name: "蓝灯", arr: [1,2,3] } //增强对象 function increase(obj, prop) { var object = Object.create(obj,prop) object.getName = function() { console.log(this.name) } return object } var son1 = increase(person, { name: { value: "AAA" } }) son1.arr.push(4) console.log(son1.name) //AAA son1.getName() //AAA var son2 = increase(person, { name: { value: "BBB" } }) console.log(son2.name) //BBB console.log(son2.arr) //1,2,3,4 son2.getName() //BBB
寄生式继承类似于构造函数,每个实例对象都有一个副本——破坏了复用性。
5. 寄生组合式继承
其核心思想是:
- 组合式:子级的prototype继承父级的prototype——通过new Father()
- 寄生组合式:子级的prototype继承父级的prototype——通过赋值
function inheritPrototype(Son, Father) { //创建一个Father.prototype的副本 var prototype = Object.create(Father.prototype) /*下面这句代码的很多资料的描述感觉不太清晰,我的理解: *1、Father.prototype的作用是赋值给Son.prototype *2、如果没有下面这条语句:Son.prototype.constructor == Father构造函数 *3、因此需要更改Son.prototype.constructor,模拟new Father()的过程 */ prototype.constructor = Son //把Father.prototype赋值给 Son.prototype Son.prototype = prototype } function Father(name) { this.name = name this.arr = [1,2,3] } Father.prototype.getName = function() { console.log(this.name) } function Son(name, age) { Father.call(this, name) this.age = age } inheritPrototype(Son, Father) Son.prototype.getAge = function() { console.log(this.age) } var son1 = new Son("AAA", 23) son1.getName() //AAA son1.getAge() //23 son1.arr.push(4) console.log(son1.arr) //1,2,3,4 var son2 = new Son("BBB", 24) son2.getName() //BBB son2.getAge() //24 console.log(son2.arr) //1,2,3
-
寄生组合式继承对于引用类型的继承来说是最理想的继承方式:避免了应用类型问题,并且只调用一次父级构造函数。
-
要说其缺点就是比其他方式更为复杂一些
总结
JS共有6种方式实现继承:
原型链:最原始的继承方式(引用类型值相互影响、无法向父级构造函数传参)
借用构造函数:解决原型链的问题,但破坏了复用性
组合式:原型链+借用构造函数(取长避短),但调用了两次父级构造函数(创建的实例和原型上存在两份相同的属性)
原生式:解决原型链传参问题,并且无需使用构造函数,但也存在引用类型问题
寄生式:原生式的增强
寄生组合式:寄生式+组合式,解决了各种问题,只是代码稍微复杂