组团学

JavaScript-继承

阅读 (347)

一、继承的概念

继承:有两个构造函数,A和B。当我们说A继承自B时,那么A就拥有了B的所有属性与方法。继承者称为子构造函数,被继承者称为父构造函数。子构造函数可以拥有一些独有的属性和方法,子构造函数也可以重写父构造函数中的方法。

优点:

  • 简化代码,减少冗余
  • 提高了代码的健壮性
  • 提高了代码的安全性

缺点:耦合与内聚是描述构造函数与构造函数之间的关系,耦合性越低、内聚性越高代码越好,而继承恰恰违背了该观点

截屏2020030911.32.42.png

继承方式:

  • 构造器的继承

    • 原型链式继承
    • 借用构造函数式继承
    • 组合式继承
    • 原型式继承
    • 寄生式继承
    • 寄生组合式基础

截屏2020030911.48.14.png

  • 拷贝继承:抛开构造器,直接通过对象标识法来创建对象

    • 浅拷贝继承
    • 子拷贝基础

二、原型链式继承

本质:父类的实例赋值给子类的原型

注意:继承关系一般在扩展原型对象之前建立,否则后续新的内容可能会抹掉我们所继承来的东西

function Person() {} Person.prototype = { description: "人类", hobbys: ["金钱","权利","美女","帅哥"], getDescription: function() { console.log(this.description); } } Person.prototype.constructor = Person; function Student(stuId){ // 子类独有属性 this.stuId = stuId; } // //继承父类【核心代码:父类的实例赋值给子类的原型】 Student.prototype = new Person(); Student.prototype.constructor = Student; Student.prototype.description = "学生"; Student.prototype.learn = function(){ console.log("我去学习了"); }
var stu = new Student(0); console.log(stu.stuId); stu.learn(); stu.getDescription(); console.log(stu instanceof Student) console.log(stu instanceof Person)
  • 问题

    • 无意的修改,伤害其他实例

      子类通过其原型prototype对父类实例化,继承了父类。如果父类中的共有属性是引用类型,就会在子类中被所有实例公用,因此一个子类的实例更改子类原型从父类构造函数中继承来的共有属性就会直接影响其他子类

      console.log("----------------"); var stu1 = new Student(1); var stu2 = new Student(2); stu1.hobbys.pop(); console.log(stu1.hobbys); console.log(stu2.hobbys);

      由于子对象与父对象指向的是同一个对象,所以一旦子对象对其原型进行了修改,父对象也会随之改变

      console.log("-------------") per = new Person(); console.log(per.hobbys);
    • 无法向父类传递参数

      由于子类实现的继承是靠其原型prototype对父类的实例化实现的,因此,在实例化父类的时候也无法对父类构造函数内的属性进行初始化

三、借用构造函数式继承

原理:通过call()改变this指向,由于call可以更改函数的作用环境,因此在子类中,对Person调用这个方法就是将子类中的变量在父类中执行一遍,由于父类中是给this绑定属性的,因此子类自然也就是继承了父类的共有属性

function Person(name, age) { this.name = name; this.age = age; this.books = ["c","c++","python"]; } Person.prototype.showBooks = function(){ console.log(this.books); } function Student(name, age, stuId){ this.stuId = stuId; Person.call(this, name, age); }
var stu1 = new Student("lilei", 18, 1); console.log(stu1.name); console.log(stu1.age); stu1.books.pop(); console.log(stu1.books); console.log("------------------------"); var stu2 = new Student("hanmeimei", 17, 1); console.log(stu2.name); console.log(stu2.age); console.log(stu2.books);
  • 优点:

    • 避免了引用类型的属性被所有实例共享
    • 可以在子类中向父类传参
  • 缺点

    • 只是子类的实例,不是父类的实例

      console.log("------------------------"); console.log(stu1 instanceof Student);//true console.log(stu1 instanceof Person);//false
    • 方法都在构造函数中定义,每次创建实例都会创建一遍方法。由于这种类型的继承没有涉及原型prototype,所以父类的原型方法自然不会被子类继承,而如果想被子类继承就必须放到构造函数中,但是如果这样,创建出来的每个实例都会单独拥有一份而不能共用,这样就违背了代码复用的原则

      stu1.showBooks();//会报错

四、组合式继承

原理:组合式继承就是将原型链式继承与借用构造函数式继承这两种方式进行组合实现的继承,使用原型链实现对原型方法的继承,而通过借用构造函数来实现对实例属性的继承

function Person(name, age) { this.name = name; this.age = age; this.books = ["c","c++","python"]; } Person.prototype = { description: "人类", hobbys: ["金钱","权利","美女","帅哥"], getDescription: function() { console.log(this.description); }, showBooks: function(){ console.log(this.books); } } Person.prototype.constructor = Person; function Student(name, age, stuId){ this.stuId = stuId; Person.call(this, name, age); } Student.prototype = new Person(); Student.prototype.constructor = Student; Student.prototype.description = "学生"; Student.prototype.learn = function(){ console.log("我去学习了"); }
var stu = new Student("tom", 18, 0); console.log(stu); console.log(stu.name); console.log(stu.age); console.log(stu.stuId); stu.learn(); stu.getDescription(); stu.showBooks(); console.log(stu instanceof Student);//true console.log(stu instanceof Person);//true
  • 问题

    调用了两次父类构造函数(使用借用构造函数式继承时执行了一遍父类的构造函数,而在实现原型链式继承时又调用了一遍父类构造函数)所以这还不是最完美的方式

五、原型式继承

基本思想:不用严格意义上的构造函数,借助原型可以根据已有的对象创建新对象,还不必因此创建自定义类型

说明:是对原型链式继承的封装,其中的过渡对象就相当于原型链式继承中的子类,只不过在原型式继承中作为一个过渡对象出现的,目的是为了创建要返回的新的实例化对象

function object(obj){ function F(){}; F.prototype = obj; return new F(); }
var per = { description: "人类", hobbys: ["金钱","权利","美女","帅哥"], getDescription: function() { console.log(this.description); } } var stu1 = object(per); stu1.name = "lilei"; var stu2 = object(per); stu2.name = "hanmeimei"; stu1.hobbys.pop() console.log(stu1.hobbys); console.log(stu2.hobbys); console.log(per.hobbys);

本质:实际上相当于创建了per的两个副本

用处与问题:创造两个相似的对象,但是包含引用类型的值的属性始终会共享响应的值

六、寄生式继承

基本思想:寄生式继承是与原型式继承紧密相关的一种思路,它创造一个仅用于封装继承过程的函数,在函数内部以某种方式增强对象,最后再返回对象

说明:所谓寄生,就是像寄生虫一样寄托于某个对象内部存在,而寄生继承这种增强新创建对象的继承思想也是寄托于原型继承模式

function object(obj){ function F(){}; F.prototype = obj; return new F(); } function extend(original){ // 通过调用函数创建一个新对象 var obj = object(original); // 以某种方式来增强对象 obj.description = "学生"; obj.learn = function(){ console.log("我学习去了"); } return obj }
var per = { description: "人类", hobbys: ["金钱","权利","美女","帅哥"], getDescription: function() { console.log(this.description); } } var stu1 = extend(per); var stu2 = extend(per); stu1.name = "lilei"; stu2.name = "hanmeimei"; console.log(stu1); console.log(stu2);

缺点:使用寄生式继承来为对象添加函数,会因为做不到函数复用而降低效率

七、寄生组合式继承

截屏2020030911.48.14.png

原理:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法,不必为了指定子类型的原型而调用超类型的构造函数,只需要超类型的一个副本。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型

好处:这样既通过在原型上定义方法实现了函数复用,又能保证每个实例都有自己的属性,且只调用了一次父类构造函数,因此避免在父类构造函数.prototype上创建不必要的、多余的属性,与此同时,原型链还能保持不变,还能正常使用instanceof 和isPrototypeOf方法

说明:寄生组合式继承被认为是引用类型最理想的继承范式

function object(obj){ function F(){}; F.prototype = obj; return new F(); } function extend(Child, Parent){ var obj = object(Parent.prototype); Child.prototype = obj; Child.prototype.constructor = Child; }
function Person(name, age) { this.name = name; this.age = age; this.books = ["c","c++","python"]; } var base = { description: "人类", hobbys: ["金钱","权利","美女","帅哥"], getDescription: function() { console.log(this.description); }, showBooks: function(){ console.log(this.books); } } Person.prototype = base; Person.prototype.constructor = Person; function Student(name, age, stuId){ this.stuId = stuId; // 借用构造函数式继承 Person.call(this, name, age); } // 寄生式继承父类原型 extend(Student, Person); Student.prototype.description = "学生"; Student.prototype.learn = function(){ console.log(this.name+"去学习了"); }
var stu1 = new Student("lilei", 18, 1); console.log(stu1.stuId); console.log(stu1.name); console.log(stu1.age); console.log(stu1.books); console.log(stu1.hobbys); stu1.learn(); stu1.getDescription(); stu1.showBooks();
console.log("--------------------------"); var stu2 = new Student("hanmeimei", 17, 2); console.log(stu2.stuId); console.log(stu2.name); console.log(stu2.age); console.log(stu2.books); console.log(stu2.hobbys); stu2.learn(); stu2.getDescription(); stu2.showBooks();
console.log("--------------------------"); stu1.hobbys.pop(); console.log(stu1.hobbys); console.log(stu2.hobbys); stu1.books.pop(); console.log(stu1.books); console.log(stu2.books);
console.log("--------------------------"); console.log(stu1 instanceof Student);//true console.log(stu1 instanceof Person);//false

八、重写

概念:子构造函数继承父构造函数中的方法,可以在自构造函数中重新定义继承过来的方法,并在原有基础上增加新的功能

本质:子对象使用父对象中的方法

function object(obj){ function F(){}; F.prototype = obj; return new F(); } function extend(Child, Parent){ var obj = object(Parent.prototype); Child.prototype = obj; Child.prototype.constructor = Child; // 记住父对象 Child.uber = Parent.prototype }
function Person(name, age) { this.name = name; this.age = age; this.books = ["c","c++","python"]; } var base = { description: "人类", hobbys: ["金钱","权利","美女","帅哥"], getDescription: function() { console.log(this.description); }, showBooks: function(){ console.log(this.books); } } Person.prototype = base; Person.prototype.constructor = Person; function Student(name, age, stuId){ this.stuId = stuId; Person.call(this, name, age); } extend(Student, Person); Student.prototype.description = "学生"; Student.prototype.learn = function(){ console.log(this.name+"去学习了"); } // 在子构造函数中重写了getDescription()方法,在原有功能上增加一行星号的打印 Student.prototype.getDescription = function(){ this.constructor.uber.getDescription(); console.log("*****************"); }
var stu = new Student("lilei", 18, 1); stu.getDescription();

九、拷贝继承

抛开构造器,直接通过对象标识法来创建对象

  • 浅拷贝:指针拷贝,如果修改了拷贝对象,就等同于修改了原对象

    function extendCopy(parent, child) { child = child || {}; for (var i in parent) { child[i] = parent[i]; } child.uber = parent; return child; }
    var per = { description: "人类", hobbys: ["权利", "金钱", "美女", "帅哥"], getDescription: function() { console.log(this.description); } } var stu = {} // 实现继承 stu = extendCopy(per, stu); stu.description = "学生" stu.getDescription(); stu.hobbys.pop(); console.log(stu.hobbys); console.log(stu.uber.hobbys); console.log(per.hobbys);
  • 深拷贝:内存拷贝,开辟一片新内存用于存储要拷贝的对象,对于父对象没有影响

    /**注意 * 1、在拷贝每个属性之前,建议使用hasOwnPrototype()函数来确认不会误拷贝不需要继承的属性 * 2、区分数组与普通对象 * 3、子对象修改,不会影响父对象 */ function extendDeepCopy(parent, child) { child = child || {}; for (var i in parent) { if (parent.hasOwnProperty(i)) { if (typeof parent[i] === "object") { child[i] = Array.isArray(parent[i]) ? [] : {}; extendDeepCopy(parent[i], child[i]); } else { child[i] = parent[i]; } } } return child; }
    var per = { description: "人类", hobbys: ["权利", "金钱", "美女", "帅哥"], getDescription: function() { console.log(this.description); } } var stu = {} // 实现继承 stu = extendDeepCopy(per, stu); stu.description = "学生" stu.getDescription(); stu.hobbys.pop(); console.log(stu.hobbys); console.log(per.hobbys);
需要 登录 才可以提问哦