码字,杂谈

真丶深入理解 JavaScript 原型和原型链(三):继承

A今天总结一下JS中的继承。前面已经总结了原型和原型链,JS中的继承基于原型链,那么有必要顺着前文继续。

JS 中的继承

首先明确,JS 中的继承是原型继承。有了上面的前置知识,我们可以深入理解 JS 中的原型继承了。

继承不是改变原型的事

我们现在创建一个 User:

function User() {
  this.name = "User";
}

let user = new User();

它可以表示为:

《真丶深入理解 JavaScript 原型和原型链(三):继承》

1、当声明一个 User 模型时,系统会自动给出这个模型和其对应的原型 prototype,并将原型的父级指向全局的 Object 原型。
2、实例化 user 的时候,系统会生成一个对象实例,同时将其父级指向 User 的原型。
3、此时,user 实例处理一个三层的原型链中:user -> User.prototype -> Object.prototype

user.__proto__.__proto__ === Object.prototype; // true

我们现在希望添加几个基于 User 的模型:

function Admin() {
  this.name = "Admin";
}

function Member() {
  this.name = "Member";
}

function Guest() {
  this.name = "Guest";
}

它们现在的父级都是 Object,如何将这些模型的父级指向 User 呢?

可能很容易想到,使用:

Admin.prototype = User.prototype;
Member.prototype = User.prototype;
Guest.prototype = User.prototype;

的方式,然后我们看一下继承的效果:

// 我们给 User 的原型添加一个方法
User.prototype.show = function () {
  console.log("show function");
};

// 看看实现继承没有
let admin = new Admin();
admin.show(); // show function

成功调用了 show 方法。

它看似很好,但是这样的操作其实也会导致问题:

// 比如我们现在需要在不同角色里面分别设置一个 role 的方法
Admin.prototype.role = function () {
  console.log("admin role");
};

Member.prototype.role = function () {
  console.log("member role");
};

Guest.prototype.role = function () {
  console.log("guest role");
};

// 再来执行一下
admin.role(); // guest role

很显然,这并不是我们期望的结果。原因在于这三个模型的原型都是 User,它们同时设置了 role 方法,那么结果就是谁在最后,这个方法就是谁。

它可以表示为:

《真丶深入理解 JavaScript 原型和原型链(三):继承》

从图中可以看到,模型已经抛弃了它自身的原型,直接指向了父级,也就是 User 的原型。所以,当模型需要单独修改原型属性/方法时,就会同时叠加到 User 的原型中,那么所有公用 User 原型的对象,都将受到影响,这就是原型的改变,它不是继承。

继承是原型的继承

修改父级引用

那么如何正确操作,不修改原型呢?其实前面在说 instanceof 时已经用到了:

// 接上例,将原型赋值改为如下
Admin.prototype.__proto__ = User.prototype;
Member.prototype.__proto__ = User.prototype;
Guest.prototype.__proto__ = User.prototype;

将它们原型的父级指向 User 的原型,它们可以表示为:

《真丶深入理解 JavaScript 原型和原型链(三):继承》

我们同样执行以下上面例子的代码:

// 接上例
admin.role(); // admin role

let member = new Member();
member.role(); // member role

let guest = new Guest();
guest.role(); // guest role

这样,它们各自的方式都属于它们自己,而不会修改 User 的原型。

创建新的原型

还有一种改变方式,通过 Object.create() 方法来创建一个新的原型对象,该方法可以使用第一个参数对象作为新对象的原型。

所以,我们还是以 Admin 为例,可以如下操作:

function User() {}
function Admin() {}

Admin.prototype = Object.create(User.prototype);
Admin.prototype.role = function () {}; // 需要在 Object.create 方法之后执行

这样也可以做到 Admin 继承自 User。

新建原型的语句顺序

像上例中的最后一句,因为新建原型等于给 Admin.prototype 重新赋值,所以其自有属性/方法都应该在此语句之后。如果把新建原型语句放在最后,那么所有其他方法都将找不到。

新建原型对已创建对象的影响

还是根据上例,假设我们现在作如下实现:

function User() {}
function Admin() {}

let admin = new Admin(); // 在修改之前创建实例

Admin.prototype = Object.create(User.prototype);
Admin.prototype.role = function () {};

admin.role(); // 报错,找不到 role

我们在新建原型语句之前创建一个实例对象,那么无论之后如何修改原型,admin 对象都不会跟着改变。

它可以表示为:

《真丶深入理解 JavaScript 原型和原型链(三):继承》

新建原型中的构造函数

在新建的原型中,会发现没有构造函数,但是它仍然可以正常工作,因为它继承了父级的构造方法。

在创建新原型之后,不要忘记添加当前的构造函数,这一点是一定的,这会避免很多意想不到的问题。

Admin.prototype = Object.create(User.prototype);
Admin.prototype.constructor = Admin; // 添加构造函数

添加上了构造函数就可以了么?并没有,你还需要为构造函数设置为不可遍历,那么就要用到 Object.defineProperty 方法:

Admin.prototype = Object.create(User.prototype);
Admin.prototype.constructor = Object.defineProperty(
  Admin.prototype,
  "constructor",
  {
    value: Admin,
    enumerable: false // 设置不可遍历
  }
);

基类的调用

既然是继承,那么肯定会有父类的方法调用。JS 中的调用方式如下:

// 定义基类
function User(name, age) {
  this.name = name;
  this.age = age;
}

// 定义一个基类方法
User.prototype.show = function () {
  console.log(this.name, this.age);
};

// 定义子类
function Admin(name, age) {
  // 不能使用这样的方式,在 JS 中会有 this 的指向问题
  // User(name, age);

  // 通过 call 方法传入指向
  User.call(this, name, age);
}
Admin.prototype.__proto__ = User.prototype;

let admin = new Admin("jermeyjone", 20);
admin.show(); // jeremyjone 20

因为 this 指向问题,需要用到 call 方法。当然,参数较多时,还可以使用 apply 方法。

function Admin(...args) {
  User.apply(this, args);
}
点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注