A今天总结一下JS中的继承。前面已经总结了原型和原型链,JS中的继承基于原型链,那么有必要顺着前文继续。
JS 中的继承
首先明确,JS 中的继承是原型继承。有了上面的前置知识,我们可以深入理解 JS 中的原型继承了。
继承不是改变原型的事
我们现在创建一个 User:
function User() {
this.name = "User";
}
let user = new User();
它可以表示为:
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 方法,那么结果就是谁在最后,这个方法就是谁。
它可以表示为:
从图中可以看到,模型已经抛弃了它自身的原型,直接指向了父级,也就是 User 的原型。所以,当模型需要单独修改原型属性/方法时,就会同时叠加到 User 的原型中,那么所有公用 User 原型的对象,都将受到影响,这就是原型的改变,它不是继承。
继承是原型的继承
修改父级引用
那么如何正确操作,不修改原型呢?其实前面在说 instanceof
时已经用到了:
// 接上例,将原型赋值改为如下
Admin.prototype.__proto__ = User.prototype;
Member.prototype.__proto__ = User.prototype;
Guest.prototype.__proto__ = User.prototype;
将它们原型的父级指向 User 的原型,它们可以表示为:
我们同样执行以下上面例子的代码:
// 接上例
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 对象都不会跟着改变。
它可以表示为:
新建原型中的构造函数
在新建的原型中,会发现没有构造函数,但是它仍然可以正常工作,因为它继承了父级的构造方法。
在创建新原型之后,不要忘记添加当前的构造函数,这一点是一定的,这会避免很多意想不到的问题。
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);
}
文章评论