JavaScript继承

52 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第9天,点击查看活动详情

一、补充知识

声明在模块内部的变量和函数

声明在构造函数的变量和函数

挂载在原型上的方法或者属性

直接挂载在构造函数上的工具方法

  1. 定义在模块内部的变量或者函数
  2. 构造函数定义的属性和方法。定义的方法每次调用的时候,都会重新申请内存空间。比较浪费资源
  3. 定义在原型上的方法。原型就是共享方法或者属性
  4. 直接挂载在构造函数上的方法,工具方法。无法访问实例内部的属性和方法
 // 模块内部
 const a = 20;
 ​
 function Person(name, age) {
   this.name = name;
   this.age = age;
   // 构造函数方法,每声明一个实例,都会重新创建一次,属于实例独有
   this.getName = function() {
     return this.name;
   }
 }
 ​
 // 原型方法,仅在原型创建时声明一次,属于所有实例共享
 Person.prototype.getAge = function() {
   return this.age;
 }
 ​
 // 工具方法,直接挂载在构造函数名上,仅声明一次,无法直接访问实例内部属性与方法
 Person.each = function() {}

二、ES5继承

2.1 原型链继承

利用原型让一个引用类型继承另一个引用类型的属性和方法

 function SuperType() {
   this.prototype = true;
 }
 ​
 SuperType.prototype.getSuperValue = function() {
   return this.prototype;
 }
 ​
 function SubType() {
   this.subprototype = false;
 }
 ​
 // 继承SuperType
 SubType.prototype = new SuperType();
 ​
 SubType.prototype.getSubValue = function() {
   return this.subprototype;
 }
 ​
 var instance = new SubType();
 console.log(instance.getSuperValue()); // true
缺点
  1. 包含引用类型的原型属性会被所有实例属性共享,容易造成属性的修改混乱。
  2. 在创建子类型的实例时,不能向超类型的构造函数中传递参数。

2.2 借用构造函数

在子类型的构造函数中调用超类型构造函数

 function SuperType(){
     this.colors = ["red", "blue", "green"];
 }
 ​
 function SubType(){
     //继承了 SuperType
     SuperType.call(this);
 }
 ​
 var instance1 = new SubType();
 instance1.colors.push("black");
 console.log(instance1.colors);  //"red,blue,green,black"
 ​
 var instance2 = new SubType();
 console.log(instance2.colors);  //"red,blue,green"
优点

可以在子类型构造函数中向超类型构造函数添加参数。相当于复制了父类中的属性,在子类中可以访问。

缺点

所有的方法都在构造函数中定义,因此就无法做到函数的复用。而且超类型的原型中定义的方法,对于子类型而言也是不可见的。

2.3 组合继承

使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。

 function SuperType(name){
     this.name = name
     this.colors = ["red", "blue", "green"];
 }
 ​
 SuperType.prototype.sayName = function(){
     console.log(this.name);
 }
 ​
 function SubType(name, age){
    
     //继承属性
     SuperType.call(this,name);
 ​
     this.age = age;
 }
 ​
 //继承方法
 SubType.prototype = new SuperType();
 SubType.prototype.constructor = SubType;
 SubType.prototype.sayAge = function(){
     console.log(this.age);
 }
 ​
 var instance1 = new SubType("james",9);
 instance1.colors.push("black");
 console.log(instance1.colors);  //"red,blue,green,black"
 instance1.sayName(); // "james"
 instance1.sayAge(); // 9
 ​
 var instance2 = new SubType("kobe",10);
 console.log(instance2.colors);  //"red,blue,green"
 instance2.sayName(); // "kobe"
 instance2.sayAge(); // 10
优点

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式。而且,instanceof 和 isPropertyOf() 也能够用于识别基于组合继承创建的对象。

缺点

调用了两次超类的构造函数,导致基类的原型对象中增添了不必要的超类的实例对象中的所有属性。

2.4 原型式继承

基于已有的对象创建新的对象

 function object(o){
     function F(){};
     F.prototype = o;
     return new F();
 }
 // 或者
 Object.create()
优点

可以实现基于一个对象的简单继承,不必创建构造函数

缺点

与原型链中提到的缺点相同,一个是传参的问题,一个是属性共享的问题

2.5 寄生式继承

创建一个仅用于封装继承过程的函数,该函数在内部以某种方式增强对象,最后返回这个对象

 function createAnother(original){
    
     var clone = object(original); //通过调用函数创建一个新对象。  object上边的原型式继承
    
     clone.sayHi = function(){  // 某种方式增强这个对象
         console.log("hi");
     }
 ​
     return clone;  // 返回这个对象
 }
 ​
 var person = {
     name: "james"
 }
 ​
 var anotherPerson = createAnother(person);
 ​
 anotherPerson.sayHi(); // "hi"
优点

主要考虑对象而不是自定义类型和构造函数的情况下,实现简单的继承

缺点

使用该继承方式,在为对象添加函数的时候,没有办法做到函数的复用

2.6 寄生式组合继承

在继承原型时,我们继承的不是超类的实例对象,而是原型对象是超类原型对象的一个实例对象,通过call来继承父类的属性。

 function Person(name) {
   this.name = name;
 }
 ​
 Person.prototype.sayName = function() {
   console.log("My name is " + this.name + ".");
 };
 ​
 function Student(name, grade) {
   Person.call(this, name);
   this.grade = grade;
 }
 ​
 Student.prototype = Object.create(Person.prototype); // 原型式继承
 Student.prototype.constructor = Student;
 ​
 Student.prototype.sayMyGrade = function() {
   console.log("My grade is " + this.grade + ".");
 };
优点

效率高,避免了在 SubType.prototype 上创建不必要的属性。与此同时还能保持原型链不变,开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

三、ES6继承

ES6的语言需要借助翻译工具【babel】,转化成ES5语法。

3.1 类

Class语法,是ES6为我们创建对象提供了新的语法糖。

推荐阅读ES6入门

 // ES5
 // 构造函数
 function Person(name, age) {
   this.name = name;
   this.age = age;
 }
 ​
 // 原型方法
 Person.prototype.getName = function() {
   return this.name
 }
 ​
 // ES6
 class Person {
   constructor(name, age) {  // 构造函数
     this.name = name;
     this.age = age;
   }
 ​
   getName() {   // 这种写法表示将方法添加到原型中
     return this.name
   }
 ​
   static a = 20;  // 等同于 Person.a = 20
 ​
   c = 20;   // 表示在构造函数中添加属性 在构造函数中等同于 this.c = 20
 ​
   // 箭头函数的写法表示在构造函数中添加方法,在构造函数中等同于this.getAge = function() {}
   getAge = () => this.age   
 }

3.2 继承 extends

我们只需要一个extends关键字,就可以实现继承了,不用像ES5使用构造函数继承和原型继承

 class Point{
   constructor(name, age) {
     this.name = name;
     this.age = age;
   }
 ​
   toString() {
     return this.name
   }
 }
 ​
 Class ColorPoint extends Point { // 通过关键字extends关键字,实现继承
   constructor(x, y, color) {
     super(x, y); // 调用父类的constructor(x, y),super()内部的this指向的是子类。
     this.color = color;
   }
 ​
   toString() {
     return this.color + ' ' + super.toString(); // 调用父类的toString()
   }
 }

四、总结

从ES5的创建对象开始,再到继承,新兴的方法,不断的在改善之前方法的不足,这就是进步。最后到ES6的高效实现,只需要几个简单的关键字,就可以是完成操作。但是底层还是最基础的方法。

五、区别

ES5ES6
ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。