一文理解JavaScript的继承

168 阅读4分钟

这是我参与2022首次更文挑战的第21天活动详情查看:2022首次更文挑战」。

1.原型

对于ES6出现了以class为基础的继承,但是底层ES5的继承也值得我们去探索。首先就是理解JavaScript原型的概念。我之前文章也写到过,对于原型目的是为了解决这么一个问题,解决多个对象都有同一个方法但是都要新开辟一个内存空间,使用原型就开辟一个内存空间,所有实例的共有方法都集放置在这么一个原型对象上面。

 function Person(name, age) {
    this.name = "name";
    this.age = 23;
    this.name = name;
    this.age = age;
  }
  Person.prototype.doEat = function (a, b) {
    console.log("a", "b", a, b);
  };

2.原型和构造函数,实例

在真正做继承之前,我们要先理解一下三个定义,原型是上面说到放相同方法在一个内存空间的地方,这个堆空间就叫原型对象。构造函数指只要通过new关键字创建对象的函数叫做构造函数,实例指的是通过new 构造函数所创建的对象就是实例,他们三者关系如下图所示

image.png

3.原型链继承

我们想到最简单的继承就是,将子类的原型指向父类的实例化对象,这样就可以使用父类所有的方法了,再将原型的构造函数指向子类,这样形成新的闭环。这样父类不管是构造函数的方法,还是父类原型的方法都在其实例中能够找到。这也是JavaScript最基本的继承思路。

function Parent(name, age) {
  this.name = name;
  this.age = age;
}
function Son(favor, sex) {
  this.favor = favor; // 兴趣爱好
  this.sex = sex;
}

Son.prototype.playSex = function () {
  console.log("我的性别是");
};
Son.prototype = new Parent("好好的", 23); // 98

Son.prototype.constructor = Son;
let sonObj = new Son("篮球", "男");

4.构造继承

通过3提到的原型链继承我们可以发现存在两个问题,第一是子类原型上的方法不能使用了,这可以通过子类继承中重新赋值原型方法解决。第二个问题是无法在子类向父类传递参数,都只能只有在外面实例化父类传递参数,显然是有些许问题的。下面代码所示是构造继承,使用call方法借调实现父类的构造方法,可以理解为将子类调用父类的构造函数,并且可以传递参数给父类构造方法,在TS中的super实现

function Parent(name, age) {
  this.name = name;
  this.age = age;

  this.playAge = function () {
    console.log("年龄是", this.age);
  };
}

Parent.prototype.playName = function () {
  console.log("姓名是", this.name);
};

function Son(weight, height, name, age) {
  this.weight = weight; // 兴趣爱好
  this.height = height;

  Parent.call(this, name, age); //TS中使用super
}
const p = new Parent("140", "180");
p.playName();

const son = new Son("140", "180", "张三", 24);
son.playAge();

5.原型+构造继承

构造继承的弊端就是父类的原型方法完全没法使用,因为只是相当于拷贝了父类构造方法的属性和方法。我们可以集合原型+构造函数继承的方法,互相弥补两种方式的弊端。

function Parent(name, age) {
  this.name = name;
  this.age = age;
  this.playName = function () {
    console.log("我的姓名是", this.name);
  };
}
Parent.prototype.playAge = function () {
  console.log("我的年龄是", this.age);
};
function Son(weight, height, name, age) {
  Parent.call(this, name, age);
  this.weight = weight; // 兴趣爱好
  this.height = height;
  this.playWeight = function () {
    console.log("我的体重是", this.weight);
  };
}
Son.prototype.playHeight = function () {
  console.log("我的身高是", this.height);
};
Son.prototype = new Parent("外面姓名", 24); // 98
Son.prototype.constructor = Son;
let son = new Son("120", "175", "里面姓名", 22);

6.完美继承

通过上面的说到的原型+构造函数继承已经是比较完美的方法了,但是还是会有些许瑕疵,比如父类构造方法其实执行了两次,第一次是父类实例化的时候执行,第二次是构造继承的时候借调执行,浪费了性能。而是父类实例化传递的参数完全无用,被子类借调的时候覆盖了,这也容易造成误解。然后可能说可以采用继承父类原型+构造,这个时候很多地方都是原型就会都被改到,这个时候要采用拷贝一份原型代码出来继承,如下代码所示

function Parent(name, age) {
  this.name = name;
  this.age = age;
  this.playName = function () {
    console.log("我的姓名是", this.name);
  };
}
Parent.prototype.playAge = function () {
  console.log("我的年龄是", this.age);
};
function Son(weight, height, name, age) {
  this.weight = weight; // 兴趣爱好
  this.height = height;
  Parent.call(this, name, age);
  this.playWeight = function () {
    console.log("我的体重是", this.weight);
  };
}
Son.prototype.playHeight = function () {
  console.log("我的身高是", this.height);
};
function Temp() {}
Temp.prototype = Parent.prototype;
tempobj = new Temp();
Son.prototype = tempobj;
tempobj.constructor = Son;
let son = new Son("120", "175", "里面姓名", 22);
console.log(son);
//   son.playHeight()
son.playAge();
son.playWeight();
son.playName();