js构造函数继承实现原理

对象创建

首先,我们知道对象有两种创建方式:

  1. 字面量创建法
// 字面量
const p = { age: 23, weight: 60 }
  1. new创建法
// 构造函数
function Person(age, weight) {
    this.age = age
    this.weight = weight
}

const p = new Person(23, 60)
console.log(p) // {age: 23, weight: 60}

或者使用es6语法

// 类
class Person {
    constructor(age, weight) {
        this.age = age
        this.weight = weight
    }
}

const p = new Person(23, 60)
console.log(p) // {age: 23, weight: 60}

对象的行为

上面创建了一个简单的对象,只有两个属性,但是通常情况下,对象还应该有些行为(方法),
比如“说话”、“写字”、“唱歌
我们给对象增加一个“说话”的方法

const p = {
    age: 23,
    weight: 60,
    say() {
        console.log(`大家好!我已经${this.age}岁了`)
    }
}

我们思考,通过字面量给对象添加方法似乎很方便,
但是考虑一种情况,如果我想创建很多个p对象,就得写很多个字面量,这样是不是有点不优雅
于是我们采用构造函数方式添加实例方法

function Person(age, weight) {
    this.age = age
    this.weight = weight
    this.say = () => {
        console.log(`大家好!我已经${this.age}岁了`)
    }
}

const p1 = new Person(23, 60)
const p2 = new Person(12, 30)
// ...
const pN = new Person(40, 70)
console.log(p1) // {age: 23, weight: 60, say:Function}
  • 我们创建了n个p实例,每个实例都有了say方法,似乎我们的目标达成了,
  • 但是回过头发现,这个say方法做的事同一件事情(输出自己的年龄),
  • 给每一个p实例都加上这个方法好像没有必要,要是他们能共用一个say方法就好了,这样浏览器就不用创建那么那么多重复的say方法了。
  • 该怎么办呢?我们尝试把say方法放到所有实例都能访问到的地方,也就是构造函数Person的原型(prototype)上,这样大家就共用一个方法了。
  • 话说回来,为什么Person的prototype能被所有人访问到呢,这是因为在new Person()之后,都会默认把p的一个属性__proto__指向Person的原型(prototype)

改造代码:

function Person(age, weight) {
    this.age = age
    this.weight = weight
}
Person.prototype.say = () => {
    console.log(`大家好!我已经${this.age}岁了`)
}
const p = new Person(23, 60)
console.log(p) // {age: 23, weight: 60}

控制台打印
在这里插入图片描述
可以看到,p的原型上多了一个say方法

  • 接下来,我们考虑一个需求,我现在不单单想得到一个p实例,我想得到一个更强大的人,一个超人,并且他会飞。
  • 普通的做法是,还是新建一个p对象,给这个p添加一个飞行(fly)的方法。这样也不是不可以,那如果我要创建很多个超人呢?给每个p添加fly?这样显然不合理。
  • 又有同学说,直接在Person的原型上添加fly方法不就好了,我想说,如果你这么干了,就会影响到之前创建的那些p实例,他们莫名奇妙多了一个fly方法,搞得普通人都能飞了,显然不合理!
  • 那应该怎么做的?其实在js中,对象可以拥有多个原型,上面的例子中我们可以理解为p拥有一个原型,因此继承了Person的一些方法,那么可不可以有一个对象同时继承了Person的一些方法和SuperMan的一些方法?这样他又可以说话又可以飞了

原型继承

// 普通人构造函数
function Person(age, weight) {
    this.age = age
    this.weight = weight
}
Person.prototype.say = () => {
    console.log(`大家好!我已经${this.age}岁了`)
}

// 超人构造函数
function SuperMan(age, weight, height) {
    this.age = age
    this.weight = weight
    this.height = height
}
SuperMan.prototype = new Person()// 直接把Person实例当作SuperMan的原型
SuperMan.prototype.fly = () => {
    console.log(`大家好!我飞起来了,咻咻咻`)
}
const s = new SuperMan(23, 60, 2)

这里新建一个SuperMan构造函数,用来创建超人实例s
其中一步操作是把Person实例当作SuperMan的原型,这样会发生什么事?
在这里插入图片描述

  • 首先s是SuperMan的实例,于是s肯定能访问到fly方法,紧接着SuperMan.prototype作为Person的一个实例,意味着SuperMan.prototype拥有一个__proto__属性可以访问到say方法。
  • 事实确实如此,当执行s.say() 的时候,js首先在s身上查找say方法,发现没有找到,于是查找s._proto_:发现还是没找到,继续查找s._proto_._proto_:在这里终于找到的say方法。

我们来测试一下,看看s到底是个什么东西

if (p instanceof Person) {
    console.log('p 是个人,会说话')
}
if (p instanceof SuperMan) {
    console.log('p 是个超人,会飞')
}
if (s instanceof Person) {
    console.log('s 是个人,会说话')
}
if (s instanceof SuperMan) {
    console.log('s 是个超人,会飞')
}
console.log('p的构造器是:' + p.constructor)
console.log('s的构造器是:' + s.constructor)

输出结果:
在这里插入图片描述

  1. p是普通人,输出“p 是个人,会说话”符合我们的预期,他肯定不是超人所以没有输出"p 是个超人,会飞",也符合预期
  2. s即使超人也是普通人,这合情合理,但是instanceof是怎么知道这一点的呢?其实instanceof 不仅考虑当前对象的类型,还考虑它继承的所有的对象,s虽然是作为SuperMan被创建的,但是他继承自Person,因此他也是普通人
  3. 接下来,p的构造器是Person,这合情合理,因为他就是Person创建的
  4. s的构造器也是Person,这不合理,因为他是使用SuperMan创建的,看看这是为什么:查看属性s.constructor,由于s身上和s._proto__身上都没有constructor,于是在s._proto.__proto__的身上找到了constructor,于是表现为Person。

坦率讲这是一个漏洞,下面我们来修复这个问题

function SuperMan(age, weight, height) {
    this.age = age
    this.weight = weight
    this.height = height
}
SuperMan.prototype = new Person()// 直接把Person实例当作SuperMan的原型
SuperMan.prototype.constructor = SuperMan // 显式的声明SuperMan.prototype.constructor 

继续处理其他问题:重复代码

你可能没注意到SuperMan和Person做了一些重复的事情

function Person(age, weight) {
    this.age = age
    this.weight = weight
}

function SuperMan(age, weight, height) {
    this.age = age
    this.weight = weight
    this.height = height
}

他们都会去设置实例的age和weight这俩属性,显然,我们可以这么做

function SuperMan(age, weight, height) {
    Person.call(this, age, weight)// 这行代码重用了Person中处理age, weight的代码
    this.height = height
}

写到这里,就粗略的实现了原型链继承,s继承SuperMan,同时继承Person。如果继续深究,还继承了Object

继续处理下一个问题:原型污染

由于SuperMan.prototype = new Person(),你是否可以猜到,这句代码执行完成后,生成的对象有点"不干净",好像多了ageweight属性。我们希望SuperMan.prototype是个干净的空对象,而不要平白无故多了这两属性。虽然对代码运行没什么影响,我们还是可以做一点优化,把上面那句代码改写成

// SuperMan.prototype = new Person() 删除这句
SuperMan.prototype = Object.create(Person.prototype)// 改成这句

总结

实现一个构造函数继承另一个构造函数要做哪些事情

  1. 把父构造器的实例作为自己的原型
SuperMan.prototype = new Person()
// 或者 SuperMan.prototype = Object.create(Person.prototype)
  1. 声明自己的原型的constructor
SuperMan.prototype.constructor = SuperMan
  1. 父构造器在自己身上执行一下
function SuperMan(age, weight, height) {
    Person.call(this, age, weight)// 这行代码重用了Person中处理age, weight的代码
    this.height = height
}

最后,用es6的语法将会十分轻松的实现继承,上述代码可以改写成:

class Person {
    constructor(age, weight) {
        this.age = age
        this.weight = weight
    }
    say() { }
}

class SuperMan extends Person {
    constructor(age, weight, height) {
        super(age, weight)// super在这里指代父类构造器 等效于Person.call(this, age, weight)
        this.height = height
    }
    fly() { }
}
const s = new SuperMan(23, 60, 2)
console.log(s);
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值