Skip to content

继承

js中继承是原型的继承,而不是改变构造函数的原型


改变构造函数的原型不是继承

js
function User(){}
// User有一个show方法,把方法定义到原型当中
User.prototype.name = function(){
    console.log('show')
}

const hd = new User()
// 在构造的对象中是没有show方法的,但是在其原型是有这个方法的
hd.show() //show

function Admin(){}
// 改变构造函数的原型,但是不推荐,推荐使用继承
Admin.prototype = User.prototype
const admin = new Admin()
admin.name()  // show

改变构造函数的原型的方法(让几个构造函数公用一个构造函数的原型)一般不去使用,因为Admin的原型继承了User的原型,后面我们需要给Admin增加其新的特有的方法,我们会同时将这个方法添加到User的原型中去,这样显然是不合理的,如果后续的其他对象添加了新的同名称的方法,也会将前面的方法进行覆盖,这也是不行的

继承是指某人继承了财产后,自己的财产还是保留的,而不是自己的财产会消失,改变构造函数的原型的方法将本身的原型全部舍弃,就没有办法给其本身加上特有的方法了

image-20241007110223333


继承是原型的继承

原型是一个对象,我们需要让原型进行继承,既要实现继承,达到对象复用的目的;又要实现重载,让不同的对象有不同的方法,共性、个性一个都不能少

js
function User(){}
// User有一个show方法,把方法定义到原型当中
User.prototype.name = function(){
    console.log('show')
}

const hd = new User()
// 在构造的对象中是没有show方法的,但是在其原型是有这个方法的
hd.show() //show

function Admin(){}
// 通过原型进行继承
Admin.prototype.__proto__ = User.prototype
Admin.prototype.role = function(){
    console.log('role')
}
const admin = new Admin()
admin.name()  // show
admin.role()  // role

image-20241007111251244

使用Admin.prototype.__proto__ = User.prototype进行原型的继承,进行原型继承和进行自定义对象方法的顺序是任意的,哪一个在前都可以

原型的继承还有一种方式:Admin.prototype = Object.create(User.prototype),这样的方式进行继承,进行原型继承和进行自定义对象方法的顺序是有要求的,必须先继承在进行自定义对象方法,最后在实例化新的对象,不然就会报错

js
function Admin(){}
// 通过原型进行继承
Admin.prototype = Object.create(User.prototype)
Admin.prototype.role = function(){
    console.log('role')
}
const admin = new Admin()

image-20241007111840502

因此推荐使用第一种方式进行原型的继承

原型是继承的,Admin.prototype对象的原型被设置成成了 User.prototype ,目的是使用 User.prototype原型中的属性,这就是继承。 至于对 Admin.prototype添加的属性方法,只是对Admin 类实例化出的所有对象的复用。其实就是一句话,改变原型(prototype)就是为了继承


继承对constructor属性的影响

对于定义的hd的构造函数:function Hd() {},这个构造函数中会有一个原型prototype,这个原型中有constructor属性,该属性记录着这个构造函数:f Hd()

image-20241008221330064

当我们使用这个构造函数创建一个新对象的时候:let obj = new Hd;,就会自动绑定到构造函数的原型:Hd.prototype,即:obj.__proto__ == Hd.prototype或者obj.__proto__.constructor == Hd

因此给我们一个对象,我们就可以通过这个对象实例化出来一个新的对象

js
function Hd() {}
let obj = new Hd();
let obj2 = obj.__proto__.constructor;

如果将原型发生改变时,创建一个新的对象来作为构造函数的原型对象,使用Object.create()实现继承会使constructor属性丢失

js
function User() {}
function Admin() {}
// 通过Object.create()来改变Admin的原型
Admin.prototype = Object.create(User.prototype)
Admin.prototype.role = function() {
    console.log("admin.role");
}
console.dir(Admin)

image-20241009202335026

会发现使用Object.create()实现继承会使constructor属性丢失

注意:虽然继承后constructor属性是丢失了,但是我们进行打印还是有值的,因为原型对象是继承User.prototype的,其User.prototype中是有constructor属性的

js
console.log(Admin.prototype.constructor);// f User()

因此,如果通过一个对象来直接改变了我们的原型,我们需要将constructor属性给添加上去:

js
Admin.prototype.constructor = Admin;

但是我们知道原型Admin.prototype也是一个对象,上述代码我们给对象压入constructor这个属性特征,压进去的属性特征是可以被遍历的,其enumerable的值为true

image-20241009203528907

遍历对象的属性方法:

js
let a = new Admin();
for (const key in a) {
    console.log(key)
}

在对象中,自定义的属性往往是允许被遍历的,但是constructor属性一般是不允许被遍历的,因此我们不采用Admin.prototype.constructor = Admin;方式进行定义,我们在定义的时候需要对其设置不可遍历:

js
Object.defineProperty(Admin.prototype, 'constructor', {
    value: Admin,
    enumerable: false
})

方法的重写

在有的时候,如果父类的方法对于子类来说不太适用,子类可以进行方法的重写,我们一般在子类本身的原型上重写这个方法

js
function User() {}
User.prototype.show = function() {
    console.log("user.show")
}
User.prototype.site = function() {
    return "jlc";
}

function Admin() {}
// 通过Object.create()来改变Admin的原型
Admin.prototype = Object.create(User.prototype)
Object.defineProperty(Admin.prototype, 'constructor', {
    value: Admin,
    enumerable: false
})
Admin.prototype.show = function() {
    console.log(User.prototype.site()+"admin.show");
}

let hd = new Admin();
hd.show();  // jlcadmin.show

image-20241009205356356


面向对象的多态

多态是指对象面对不同的内容时,其体现出不同的状态(在不同的形态响应出不同的结果)多态实现可以使我们的代码更加优雅

js
function User() {}
User.prototype.show = function() {
    console.log(this.description());
}

function Admin() {};
Admin.prototype = Object.create(User.prototype);
Admin.prototype.description = function () {
    return '我是管理员'
};

function Member() {};
Member.prototype = Object.create(User.prototype);
Member.prototype.description = function () {
    return '我是会员'
};

function Enterprise() {};
Enterprise.prototype = Object.create(User.prototype);
Enterprise.prototype.description = function () {
    return '我是企业账户'
};

for (const obj of [new Admin(), new Member(), new Enterprise()]) {
    obj.show();
}
// 我是管理员
// 我是会员
// 我是企业账户

多态特性实现的基础还是依赖于子级原型对父级原型方法的重写。其实现方式是,父级原型的指针,在实际调用中,指向实际调用者


使用父类构造函数

对于比较公用的方法,我们没有必要在自己的构造函数中进行编写,我们可以在父类的构造函数中去编写方法,对于实例化的对象直接去调用父类的构造函数即可

调用上一级,实现对象初始属性的构建:

js
function User(name, age) {
    this.name = name;
    this.age = age;
}
User.prototype.show = function() {
    console.log(this.name, this.age);
}

function Admin(...args) {
    // 使用call或者apply来进行改变User函数的this,将新增对象的this传递过去
    User.apply(this, args);
}
// 使用原型的继承方式,使Admin继承User
Admin.prototype = Object.create(User.prototype);
Admin.prototype.constructor = Admin;

let hd = new Admin('jlc', 24);
hd.show()  // jlc 24

我们希望传参之后的过程可以在User中进行完成,因为继承User的构造函数可能不止Admin,后面可能有别的构造函数

如果在Admin构造函数中编写相关的传参过程后,那么后面别的继承于User的构造函数就不能进行使用,为了减少重复工作,我们可以直接在父类的构造函数中编写进行传参后的方法


通过原型工厂封装继承

我们知道原型继承的步骤和重复工作比较多,当进行一个继承时,都需要进行以下操作的重复工作:

js
Admin.prototype = Object.create(User.prototype);
Admin.prototype.constructor = Admin;
Object.defineProperty(Admin.prototype, 'constructor', {
    value: Admin,
    enumerable: false
})

对于这样的重复工作,我们可以进行封装:

js
// 继承封装方法1
// 第一个参数是我们的基类,第二个参数是我们要继承的父类
function extend(sub, sup) {
    sub.prototype = Object.create(sup.prototype);
    sub.prototype.constructor = sub;
    Object.defineProperty(sub.prototype, 'constructor', {
        value: sub,
        enumerable: false
    })
}

// 继承封装方法2
function extend(sub, sup) { 		              Object.setPrototypeOf(sub.prototype,sup.prototype)
}


function User(name, age) {
    this.name = name;
    this.age = age;
}
User.prototype.show = function() {
    console.log(this.name, this.age);
}

function Admin(...args) {
    // 使用call或者apply来进行改变User函数的this,将新增对象的this传递过去
    User.apply(this, args);
}
// 使用封装好的原型工厂进行继承
extend(Admin, User);
let hd = new Admin('jlc', 24);
hd.show()  // jlc 24

通过对象工厂实现继承

之前的对象都是通过new一个构造函数进行创建的,但是创建一个对象还可以通过其他的方式进行创建:

js
let hd1 = {};
let hd2 = Object.create({});

对于上述两种方式进行对象的创建,创建的的对象直接指向Object的原型:hd1.__proto__ == Object.prototype,我们可以通过对象工厂实现对于其他原型的继承:

js
// 父级构造函数
function User(name, age) {
    this.name = name;
    this.age = age;
}
User.prototype.show = function() {
    console.log(this.name, this.age);
}

// 对象工厂,仅仅只是一个函数,不是构造函数
function admin() {
    // 声明出一个对象,使其继承User的原型
    const instance = Object.create(User.prototype);
    // 传递参数调用User的方法
    User.call(instance, name, age);
    // 添加自定义的方法
    instance.role = function() {
        console.log('role');
    }
    return instance;
}

let lisi = admin('lisi', 24);  
lisi.show();  // lisi 24
lisi.role();  // role

对于继承写法:

js
const instance = Object.create(User.prototype);

我们还可以使用以下的写法进行编写:

js
const instance = {};
instance.__proto__ = User.prototype;

多继承

Js中是没有多继承的,是没有办法使一个原型在继承另一个原型的基础上,又继承其他的原型,由于Js中没有办法实现多继承,在一些必要的情况下会进行逐级的继承,来调用该父级中的方法:

js
// 原型工厂
function extend(sub, sup) { 		              Object.setPrototypeOf(sub.prototype,sup.prototype)
}

function Request() {}
Request.prototype.ajax = function() {
    console.log('后台请求');
}

function User(name, age) {
    this.name = name;
    this.age = age;
}
extend(User, Request);
User.prototype.show = function() {
    console.log(this.name, this.age);
}

function Admin(...args) {
    User.apply(this, args);
}
extend(Admin, User);
let admin = new Admin('jlc', 24);
admin.ajax();  // 后台请求

这样的多级继承是比较繁琐的,也不是最佳的方法

mixin实现多继承

我们一般是使用mixin来实现多继承,mixin可以理解为一个实现多继承的混合功能,将其他对象的属性直接压到最开始继承的原型里面:

js
// 原型工厂
function extend(sub, sup) { 		              Object.setPrototypeOf(sub.prototype,sup.prototype)
}

const Request = {
    ajax() {
        console.log('后台请求');
    }
}

function User(name, age) {
    this.name = name;
    this.age = age;
}
extend(User, Request);
User.prototype.show = function() {
    console.log(this.name, this.age);
}

function Admin(...args) {
    User.apply(this, args);
}
extend(Admin, User);
// 只压入其他对象的一个方法
Admin.prototype.ajax = Request.ajax;
// 一次性压入其他对象的所有属性,推荐使用
Admin.prototype = Object.assign(Admin.prototype, Request)
let admin = new Admin('jlc', 24);
admin.ajax();  // 后台请求

mixin的内部继承和super关键字

对于功能类的对象,如之前的Request,我们也可以实现继承,去继承别的功能类对象,使用其他功能类对象里面的方法:

js
const Request, = {
    ajax() {
        return '后台请求';
    }
}

const Credit = {
    __proto__: Request,
    total() {
        console.log(this.__proto__.ajax() + '积分统计');
    }
}

function User(name, age) {
    this.name = name;
    this.age = age;
}
extend(User, Request);
User.prototype.show = function() {
    console.log(this.name, this.age);
}

function Admin(...args) {
    User.apply(this, args);
}
extend(Admin, User);
Admin.prototype = Object.assign(Admin.prototype, Credit)
let admin = new Admin('jlc', 24);
admin.total();  // 后台请求积分统计

在调用其他功能类中的方法时,其调用语句比较长:this.__proto__.ajax(),因此,系统提供了一个简写的形式,需要使用super关键字:super.ajax()super是指当前这个类的原型,不是调用者(admin)构造函数的原型

总之,功能类不是为了继承而使用的,而是合并到其他原型中,类似于多继承来解决问题,同时功能类之间也是可以相互继承的

Released under the MIT License.