一、继承的概念
有两个构造函数,A和B。当我们说A继承自B时,那么A就拥有了B的所有属性与方法。继承者称为子构造函数,被继承者称为父构造函数。子构造函数可以拥有一些独有的属性和方法,子构造函数也可以重写父构造函数中的方法。
优点是可以简化代码,减少冗余,提高了代码的健壮性安全性和安全性。但是我们知道耦合与内聚是描述构造函数与构造函数之间的关系,耦合性越低、内聚性越高代码越好,而继承恰恰违背了该观点。

我们主要先介绍构造器的继承的6种方式,分别是原型链式继承、借用构造函数式继承、组合式继承、原型式继承、寄生式继承、寄生组合式基础。

二、原型链式继承
原型链式继承本质是父类的实例赋值给子类的原型,继承关系一般在扩展原型对象之前建立,否则后续新的内容可能会抹掉我们所继承来的东西。
定义Person父类:
1. function Person() {}
2. Person.prototype = {
3. description: "人类",
4. hobbys: ["金钱","权利","美女","帅哥"],
5. getDescription: function() {
6. console.log(this.description);
7. }
8. }
9. Person.prototype.constructor = Person;
定义Student子类并继承Person父类:
1. function Student(stuId){
2. // 子类独有属性
3. this.stuId = stuId;
4. }
5. // 继承父类【核心代码:父类的实例赋值给子类的原型】
6. Student.prototype = new Person();
7. Student.prototype.constructor = Student;
8. Student.prototype.description = "学生";
9. Student.prototype.learn = function(){
10. console.log("我去学习了");
11. }
实例化子类Student对象:
1. var stu = new Student(0);
2. console.log(stu.stuId);//0
3. stu.learn();//我去学习了
4. stu.getDescription();//学生
5.
6. console.log(stu instanceof Student)// true
7. console.log(stu instanceof Person)// true
这种继承方式存在一些问题:
第一个问题是无意的修改,伤害其他实例,子类通过其原型prototype对父类实例化,继承了父类。如果父类中的共有属性是引用类型,就会在子类中被所有实例公用,因此一个子类的实例更改子类原型从父类构造函数中继承来的共有属性就会直接影响其他子类。
1. var stu1 = new Student(1);
2. var stu2 = new Student(2);
3. stu1.hobbys.pop();
4. console.log(stu1.hobbys);//Array(3) [ "金钱", "权利", "美女" ]
5. console.log(stu2.hobbys);//Array(3) [ "金钱", "权利", "美女" ]
由于子对象与父对象指向的是同一个对象,所以一旦子对象对其原型进行了修改,父对象也会随之改变。
1. per = new Person();
2. console.log(per.hobbys);//Array(3) [ "金钱", "权利", "美女" ]
第二个问题是无法向父类传递参数,由于子类实现的继承是靠其原型prototype对父类的实例化实现的,因此,在实例化父类的时候也无法对父类构造函数内的属性进行初始化。
三、借用构造函数式继承
通过call()改变this指向,由于call可以更改函数的作用环境,因此在子类中,对Person调用这个方法就是将子类中的变量在父类中执行一遍,由于父类中是给this绑定属性的,因此子类自然也就是继承了父类的共有属性。
子类Student继承父类Person:
1. function Person(name, age) {
2. this.name = name;
3. this.age = age;
4. this.books = ["c","c++","python"];
5. }
6. Person.prototype.showBooks = function(){
7. console.log(this.books);
8. }
9.
10. function Student(name, age, stuId){
11. this.stuId = stuId;
12. Person.call(this, name, age);
13. }
实例化对象:
1. var stu1 = new Student("lilei", 18, 1);
2. console.log(stu1.name//lilei
3. console.log(stu1.age);//18
4. stu1.books.pop();
5. console.log(stu1.books);//Array [ "c", "c++" ]
6.
7. var stu2 = new Student("hanmeimei", 17, 1);
8. console.log(stu2.name);//hanmeimei
9. console.log(stu2.age);//17
10. console.log(stu2.books);//Array(3) [ "c", "c++", "python" ]
优点是避免了引用类型的属性被所有实例共享,也可以在子类中向父类传参。
但是也存在两个缺点:
第一个问题是只是子类的实例,不是父类的实例。
1. console.log(stu1 instanceof Student);//true
2. console.log(stu1 instanceof Person);//false
第二个问题是方法都在构造函数中定义,每次创建实例都会创建一遍方法。由于这种类型的继承没有涉及原型prototype,所以父类的原型方法自然不会被子类继承,而如果想被子类继承就必须放到构造函数中,但是如果这样,创建出来的每个实例都会单独拥有一份而不能共用,这样就违背了代码复用的原则。
1. stu1.showBooks();//会报错
四、组合式继承
组合式继承就是将原型链式继承与借用构造函数式继承这两种方式进行组合实现的继承,使用原型链实现对原型方法的继承,而通过借用构造函数来实现对实例属性的继承。
定义Person父类与Student子类:
1. function Person(name, age) {
2. this.name = name;
3. this.age = age;
4. this.books = ["c","c++","python"];
5. }
6. Person.prototype = {
7. description: "人类",
8. hobbys: ["金钱","权利","美女","帅哥"],
9. getDescription: function() {
10. console.log(this.description);
11. },
12. showBooks: function(){
13. console.log(this.books);
14. }
15. }
16. Person.prototype.constructor = Person;
17.
18.
19. function Student(name, age, stuId){
20. this.stuId = stuId;
21. Person.call(this, name, age);
22. }
实现继承:
1. Student.prototype = new Person();
2. Student.prototype.constructor = Student;
3. Student.prototype.description = "学生";
4. Student.prototype.learn = function(){
5. console.log("我去学习了");
6. }
实例化子类对象:
1. var stu = new Student("tom", 18, 0);
2. console.log(stu);//Object { stuId: 0, name: "tom", age: 18, books: (3) […] }
3. console.log(stu.name);//tom
4. console.log(stu.age);//18
5. console.log(stu.stuId);//0
6. stu.learn();//我去学习了
7. stu.getDescription();//学生
8. stu.showBooks();//Array(3) [ "c", "c++", "python" ]
9.
10. console.log(stu instanceof Student);//true
11. console.log(stu instanceof Person);//true
但是会调用两次父类构造函数(使用借用构造函数式继承时执行了一遍父类的构造函数,而在实现原型链式继承时又调用了一遍父类构造函数)所以这还不是最完美的方式。
五、原型式继承
不用严格意义上的构造函数,借助原型可以根据已有的对象创建新对象,还不必因此创建自定义类型。是对原型链式继承的封装,其中的过渡对象就相当于原型链式继承中的子类,只不过在原型式继承中作为一个过渡对象出现的,目的是为了创建要返回的新的实例化对象。
1. function object(obj){
2. function F(){};
3. F.prototype = obj;
4. return new F();
5. }
6.
7. var per = {
8. description: "人类",
9. hobbys: ["金钱","权利","美女","帅哥"],
10. getDescription: function() {
11. console.log(this.description);
12. }
13. }
14.
15. var stu1 = object(per);
16. stu1.name = "lilei";
17. var stu2 = object(per);
18. stu2.name = "hanmeimei";
19.
20. stu1.hobbys.pop()
21. console.log(stu1.hobbys);//Array(3) [ "金钱", "权利", "美女" ]
22. console.log(stu2.hobbys);//Array(3) [ "金钱", "权利", "美女" ]
23. console.log(per.hobbys);//Array(3) [ "金钱", "权利", "美女" ]
注意此时实际上相当于创建了per的两个副本,创造两个相似的对象,但是包含引用类型的值的属性始终会共享响应的值。
六、寄生式继承
寄生式继承是与原型式继承紧密相关的一种思路,它创造一个仅用于封装继承过程的函数,在函数内部以某种方式增强对象,最后再返回对象。所谓寄生,就是像寄生虫一样寄托于某个对象内部存在,而寄生继承这种增强新创建对象的继承思想也是寄托于原型继承模式。
1. function object(obj){
2. function F(){};
3. F.prototype = obj;
4. return new F();
5. }
6.
7. function extend(original){
8. // 通过调用函数创建一个新对象
9. var obj = object(original);
10. // 以某种方式来增强对象
11. obj.description = "学生";
12. obj.learn = function(){
13. console.log("我学习去了");
14. }
15.
16. return obj
17. }
18.
19. var per = {
20. description: "人类",
21. hobbys: ["金钱","权利","美女","帅哥"],
22. getDescription: function() {
23. console.log(this.description);
24. }
25. }
26.
27. var stu1 = extend(per);
28. var stu2 = extend(per);
29.
30. stu1.name = "lilei";
31. stu2.name = "hanmeimei";
32. console.log(stu1);//Object { description: "学生", learn: learn(), name: "lilei" }
33. console.log(stu2);//Object { description: "学生", learn: learn(), name: "hanmeimei" }
但是使用寄生式继承来为对象添加函数,会因为做不到函数复用而降低效率。
七、寄生组合式基础
通过借用构造函数来继承属性,通过原型链的混成形式来继承方法,不必为了指定子类型的原型而调用超类型的构造函数,只需要超类型的一个副本。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
这样既通过在原型上定义方法实现了函数复用,又能保证每个实例都有自己的属性,且只调用了一次父类构造函数,因此避免在父类构造函数.prototype上创建不必要的、多余的属性,与此同时,原型链还能保持不变,还能正常使用instanceof 和isPrototypeOf方法。
1. function object(obj){
2. function F(){};
3. F.prototype = obj;
4. return new F();
5. }
6.
7. function extend(Child, Parent){
8. var obj = object(Parent.prototype);
9. Child.prototype = obj;
10. Child.prototype.constructor = Child;
11. }
12.
13. function Person(name, age) {
14. this.name = name;
15. this.age = age;
16. this.books = ["c","c++","python"];
17. }
18. var base = {
19. description: "人类",
20. hobbys: ["金钱","权利","美女","帅哥"],
21. getDescription: function() {
22. console.log(this.description);
23. },
24. showBooks: function(){
25. console.log(this.books);
26. }
27. }
28. Person.prototype = base;
29. Person.prototype.constructor = Person;
30.
31. function Student(name, age, stuId){
32. this.stuId = stuId;
33. // 借用构造函数式继承
34. Person.call(this, name, age);
35. }
36. // 寄生式继承父类原型
37. extend(Student, Person);
38. Student.prototype.description = "学生";
39. Student.prototype.learn = function(){
40. console.log(this.name+"去学习了");
41. }
验证结果:
1. var stu1 = new Student("lilei", 18, 1);
2. console.log(stu1.stuId);//1
3. console.log(stu1.name);//lilei
4. console.log(stu1.age);//18
5. console.log(stu1.books);//Array(3) [ "c", "c++", "python" ]
6. console.log(stu1.hobbys);//Array(4) [ "金钱", "权利", "美女", "帅哥" ]
7. stu1.learn();//lilei去学习了
8. stu1.getDescription();//学生
9. stu1.showBooks();//Array(3) [ "c", "c++", "python" ]
1. var stu2 = new Student("hanmeimei", 17, 2);
2. console.log(stu2.stuId);2
3. console.log(stu2.name);//hanmeimei
4. console.log(stu2.age);17
5. console.log(stu2.books);//Array(3) [ "c", "c++", "python" ]
6. console.log(stu2.hobbys);//Array(4) [ "金钱", "权利", "美女", "帅哥" ]
7. stu2.learn();//hanmeimei去学习了
8. stu2.getDescription();//学生
9. stu2.showBooks();//Array(3) [ "c", "c++", "python" ]
1. stu1.hobbys.pop();
2. console.log(stu1.hobbys);//Array(3) [ "金钱", "权利", "美女" ]
3. console.log(stu2.hobbys);//Array(3) [ "金钱", "权利", "美女" ]
4. stu1.books.pop();
5. console.log(stu1.books);//Array [ "c", "c++" ]
6. console.log(stu2.books);//Array(3) [ "c", "c++", "python" ]
1. console.log(stu1 instanceof Student);//true
2. console.log(stu1 instanceof Person);//false
文章作者孙长凯:深信服大数据认证专家,产业教育中心资深讲师,曾就职于思特奇科技、天奕思益科技、千锋教育,分别担任分布式内存数据库研发工程师、大数据平台架构师、人工智能专家、学科主管以及多所高校特聘企业讲师;具有八年大数据、人工智能从业经验,在大数据人工智能领域、企业项目管理等方面有较强的实战经验,对大数据、人工智能相关技术课程具备丰富的课程交付经验。
本文详细介绍了JavaScript中继承的六种常见实现方式:原型链式继承、借用构造函数式继承、组合式继承、原型式继承、寄生式继承和寄生组合式继承。每种方式都有其优缺点,如原型链式继承可能导致属性共享,借用构造函数式继承无法正确识别实例类型。组合式继承虽解决了前两者的问题,但会调用两次父类构造函数。寄生式继承则通过创建父类副本避免共享,但效率较低。寄生组合式继承结合了两者优点,只调用一次父类构造函数,同时保持原型链。文章最后通过示例展示了每种方式的实现和应用场景。

被折叠的 条评论
为什么被折叠?



