一文大白话讲清楚js中的继承(一)原型链继承+构造函数继承+组合式继承

一文大白话讲清楚js中的继承(一)原型链继承+构造函数继承+组合式继承

原型链继承

  • 就是我们说的第一种,直接吧子原型指向父实例
function Person(){
	this.name='tom'
}
Person.prototype.sayName=function(){
	console.log(`name is ${this.name}`)
}

function Student(){
	this.score=80
}
Student.prototype=new Person()
Student.prototype.constructor=Student
let stu=new Student()
console.log(stu)
console.log(stu.name)
console.log(stu.score)
stu.sayName()
console.log(stu.__proto__)
// Person { score: 80 }
// tom
// 80
// name is tom
// Person { name: 'tom' }
  • 我们打印stu为{ score: 80 },里面并没有name属性,但是我们打印console.log(stu.name)还是打印出来了tom,为什么,因为虽然stu里面有name,但是stu的原型是Person的实例,而这个实例里面是有name的,所以向上查找原型链的时候找到了,就打印了name
  • 也就是说tom其实是这么打印出来的console.log(stu.proto.name),我们测试一下
function Person(){
	this.name='tom'
}
Person.prototype.sayName=function(){
	console.log(`name is ${this.name}`)
}

function Student(){
	this.score=80
}
Student.prototype=new Person()
Student.prototype.constructor=Student
let stu=new Student()
console.log(stu)
console.log(stu.name)
console.log(stu.__proto__.name)
console.log(stu.score)
stu.sayName()
console.log(stu.__proto__)
// Person { score: 80 }
// tom
// tom
// 80
// name is tom
// Person { name: 'tom' }
  • 会发现打印了两个tom,说明我们的推测是正确的
  • 这就是原型链继承,但是这个继承有什么问题呢,在这呢,我们是先把Student的原型指向Person的实例,然后再实例化Student,这就导致所有的Student实例共享一个Person的实例
  • 上代码
function Person(){
	this.name='tom'
	this.score={
		math:80
	}
}

function Student(){
}
Student.prototype=new Person()
Student.prototype.constructor=Student
let stu1=new Student()
let stu2=new Student()
stu1.name='newTom'
stu1.score.math=90
console.log(stu1.name)
console.log(stu2.name)
console.log(stu1.score.math)
console.log(stu2.score.math)
// newTom
// tom
// 90
// 90
  • 发现没,因为stu1和stu2共享了父实例,所以对于父实例的引用类型就会造成数据污染,一改全改
  • 还有个问题,什么问题呢,假设我们的父构造函数是通过参数来给属性赋值的,我们上代码
function Person(name){
	this.name=name
}
function Student(){}

Student.prototype=new Person('tom')
Student.prototype.constructor=Student
let stu=new Student()
console.log(stu.name)
  • 看出来没,因为要先把父实例赋值给子原型,所以父实例化的时候就必须把参数给到,也就是说我们在实例化子构造函数的时候再给付构造函数传值,必须提前传,这就很不友好,因为我们的高频操作肯定是实例化子构造函数,其他操作做一个封装,但是因为这个限制,封装就很不好实现
  • 所以这个是个大问题,
  • 还有一个问题,啥问题,这里Student只继承了一个Person,如果还有其他的构造函数要继承呢,怎么继承呢,再赋值给prototype么,那肯定不行,把上一个就覆盖了,所以理论上只能继承一个
  • 没问题,这就是原型链继承的三大问题所在
    1. 父类的引用类型属性会被子类实例共享
    1. 无法实现多继承
    1. 无法在实例化子类的时候向父类传递参数

构造函数继承

  • 第二中继承,在构造函数里面执行构造函数,可以理解为借用构造函数,进一步可以理解为把两个构造函数合并为一个进行实例化
  • 怎么理解呢,两个构造函数,一个A,一个B,现在想在实例化B的时候把A也实例化了,怎么做呢,很简单,在A的构造函数里面执行B的构造函数就可以了,但是这样虽然两个构造函数都执行了,但是执行出来的两个结果没有关联起来
function Person(){}
function Student(){
	Person()
}
let stu=new Student()
  • 这里虽然两个构造函数都执行了,但是没有关联起来,也就是说stu里面还是没有Person的属性
  • 怎么让他不仅执行还能关联起来呢,聪明的你又想到了call/apply
  • 是的,我们只需要让Person执行的时候this指向当前对象就OK了
function Person(){}
function Student(){
	Person.call(this)
}
let stu=new Student()
  • 酱紫就对上号了,new Student的时候,Student和Person的this指向是同一个,这不最后实例化的就是一个么,这样stu就包含了两个构造函数的属性
  • 完整来个代码
function Person(name){
	this.name=name
	this.score={
		math:80
	}
}
function Student(name){
	Person.call(this,name)
	this.age=12
}

let stu=new Student('tom')
console.log(stu)
// Student { name: 'tom', score: { math: 80 }, age: 12 }
  • 你看,这时候stu是不是Person和Student的属性都有了,而且实例化Student的时候可以直接给Person传参了,不用提前给参了,挺好
  • 但别高兴地太早,我们是在子类里面直接调用父类的构造函数,而不是像原型链继承一样,把子类的原型指向父类的实例,这就导致,子类实例的原型链上没有父类的原型链,也就意味着,父类原型链上的所有东西,子类实例访问不到
  • 二话不说,上代码验证
function Person(name){
	this.name=name
	this.score={
		math:80
	}
}
//父类原型链上的方法
Person.prototype.sayName=function(){
	console.log(`name is ${this.name}`)
}
function Student(name){
	Person.call(this,name)
	this.age=12
}

let stu=new Student('tom')
console.log(stu)
stu.sayName()//stu.sayName is not a function
console.log(stu.__proto__)//{}

  • 看到了吧,stu的原型链上确实丢失了父类原型链,所以访问不到父类原型链上的方法
  • 但是,祸福相依,虽然我丢失了父类原型,但是发现没有,每次new Student,Person都会重新执行一次,说明什么,说明所有子类实例不在共享父类的属性,而是各自肚子拥有
function Person(name){
	this.name=name
	this.score={
		math:80
	}
}

function Student(name){
	Person.call(this,name)
	this.age=12
}

let stu1=new Student('tom1')
let stu2=new Student('tom2')
stu1.score.math=59
console.log(stu1)
console.log(stu2)
// Student { name: 'tom1', score: { math: 59 }, age: 12 }
// Student { name: 'tom2', score: { math: 80 }, age: 12 }
  • 看到了吧,这会stu1和stu2互不影响了
  • 所以总结
  • 优点
    1. 子类实例化的时候可以向父类传递参数了
    1. 子类实例不在共享父类的属性
  • 缺点
    1. 子类实例无法继承父类原型上的方法
    1. 每个子类实例都有父类方法的副本
  • 到这,你是不是有点想法了
  • 原型链的缺点是不可以传递参数,且共享了属性,但是继承了父类原型上的方法,这不正好跟构造函数对应上了吗
  • 于是你是不是在想,能不能把两个结合一下,优势互补一下,岂不妙哉
  • 是的,你很棒,这就叫组合继承,原型链继承+构造函数继承

组合式继承

  • 知道了原型链继承和构造函数继承 ,组合式继承就很好搞么,把两个融合在一起,也就是不仅执行构造函数,而且挂载原型
  • 话不多说,上代码
function Person(name){
	this.name=name
	this.score={
		math:80
	}
}
Person.prototype.sayName=function(){
	console.log(`name is ${this.name}`)
}

function Student(name){
	Person.call(this,name)
	this.age=12
}
Student.prototype=new Person()
Student.prototype.constructor=Student

let stu1=new Student('tom1')
let stu2=new Student('tom2')

console.log(stu1)
console.log(stu2)

stu1.sayName()
stu2.sayName()

stu1.score.math=12

console.log(stu1)
console.log(stu2)
// Student { name: 'tom1', score: { math: 80 }, age: 12 }
// Student { name: 'tom2', score: { math: 80 }, age: 12 }
// name is tom1
// name is tom2
// Student { name: 'tom1', score: { math: 12 }, age: 12 }
// Student { name: 'tom2', score: { math: 80 }, age: 12 }
  • 观察打印结果,你看几乎很完美的解决了我们需要的问题,
    1. 实例化子类的时候可以向父类传递参数
    1. 子类实例不再共享父类的引用类型
    1. 子类实例继承了父类原型的方法
  • 够完美了吧,很完美,但是瑕疵也还是有的 ,在哪里呢,眼尖的童鞋发现了,Person这个构造函数会执行两次,分别是挂载原型时候的new Person和子类里面调用Person.call
  • 这个瑕疵大么,说大不大,但是肯定会有性能开销,这个是一定的
  • 这三个算是一组
  • 下篇文章讲另外一组,原型式继承+寄生式继承+寄生组合式继承
<think>我们正在讨论C++中的前置递增运算符(++i)和后置递增运算符(i++)的区别。根据提供的引用,我们可以总结如下: 1. **前置递增运算符(++i)**: - 先对操作数进行递增操作,然后返回递增后的值。 - 在运算符重载中,前置递增运算符返回的是对象的引用(因为修改了当前对象,然后返回该对象本身)。 - 因此,前置递增通常效率更高,因为它不创建临时对象。 2. **后置递增运算符(i++)**: - 先返回操作数当前值的个副本(临时对象),然后再对操作数进行递增操作。 - 在运算符重载中,后置递增运算符有个额外的整型参数(通常为0,用于区分前置和后置),并且返回的是递增前的副本(即个临时对象,不是引用)。 - 因此,后置递增会涉及临时对象的创建和拷贝,效率相对较低。 引用[1]指出:后置递增运算符在C++中与前置递增运算符的差别在于它需要传递个额外的整型参数(通常使用默认值为0),以区分前置和后置操作。后置递增的意图是“返回当前对象的个副本,并在该副本被返回后,修改原对象以使其值增加1”。 引用[2]和[4]进步说明了运算符重载的实现方式: - 前置递增运算符重载函数原型:`ReturnType& operator++();` - 后置递增运算符重载函数原型:`ReturnType operator++(int);` (注意:这里的`int`是用于区分的哑元参数) **示例代码说明**: 假设我们有个自定义的整数类`MyInteger`,我们可以这样重载前置和后置递增运算符: ```cpp class MyInteger { private: int value; public: MyInteger(int val) : value(val) {} // 前置递增运算符重载 MyInteger& operator++() { ++value; return *this; // 返回当前对象的引用 } // 后置递增运算符重载 MyInteger operator++(int) { MyInteger temp = *this; // 保存当前对象的值(副本) ++value; // 当前对象递增 return temp; // 返回递增前的副本 } int getValue() const { return value; } }; ``` **使用示例**: ```cpp MyInteger a(5); MyInteger b = ++a; // 前置递增:a先递增到6,然后b得到a的引用(即6) // 此时a的值是6,b的值也是6 MyInteger c(5); MyInteger d = c++; // 后置递增:d得到c的副本(5),然后c递增到6 // 此时c的值是6,d的值是5 ``` **性能考虑**: 由于后置递增需要创建临时对象,因此在不需要原始值的场景下,使用前置递增更高效。特别是在循环和迭代器操作中,推荐使用前置递增。 **总结区别**: | 特性 | 前置递增(++i) | 后置递增(i++) | |--------------|----------------|----------------| | 操作顺序 | 先递增,后使用 | 先使用(返回原值副本),后递增 | | 返回值 | 对象的引用 | 递增前的副本(临时对象) | | 效率 | 较高(无临时对象) | 较低(有临时对象) | | 重载函数参数 | 无 | 个int类型的哑元参数(用于区分) | 希望这个解释能帮助你理解两者的区别。在实际编程中,除非需要后置递增的特性(即需要递增前的值),否则应优先使用前置递增。 §§相关问题§§ 1. 在C++中,如何重载个类的前置递减和后置递减运算符? 2. 为什么在STL迭代器中,推荐使用前置递增而不是后置递增? 3. 除了递增递减运算符,还有哪些运算符可以重载为前置和后置形式? 4. 在C++中,后置递增运算符的哑元参数具体是什么作用?能否省略?</think>### 前置递增(++i)与后置递增(i++)的区别详解 在编程中,前置递增运算符(`++i`)和后置递增运算符(`i++`)的核心区别在于**运算顺序和返回值的不同**: #### 1. **运算顺序** - **前置递增(++i)**: 先执行递增操作,再返回结果。 例如: ```cpp int i = 5; int j = ++i; // 先让 i 变为 6,再将 6 赋值给 j // 结果:i=6, j=6 ``` - **后置递增(i++)**: 先返回当前值,再执行递增操作。 例如: ```cpp int i = 5; int j = i++; // 先将 i 的原始值 5 赋值给 j,再让 i 变为 6 // 结果:i=6, j=5 ``` #### 2. **运算符重载实现(C++示例)** ```cpp class Counter { int value; public: // 前置递增:返回引用(修改自身) Counter& operator++() { ++value; return *this; // 返回自身引用 } // 后置递增:返回副本(需哑元参数 int 区分) Counter operator++(int) { Counter temp = *this; // 保存当前值 ++value; // 自身递增 return temp; // 返回递增前的副本 } }; ``` - **关键区别**: - 前置返回引用(`Counter&`),直接修改原对象[^2][^4]。 - 后置返回临时副本(`Counter`),需额外参数 `int` 标识[^1][^4]。 #### 3. **性能差异** - **前置递增(++i)** 效率更高: 直接修改对象并返回引用,无临时对象开销[^4]。 - **后置递增(i++)** 效率较低: 需创建副本存储原值,涉及拷贝构造和析构开销[^1][^4]。 *在循环或高性能场景中优先使用 `++i`*。 #### 4. **使用场景** - **前置递增**: 适用于不需要原始值的场景(如循环迭代器): ```cpp for (auto it = vec.begin(); it != vec.end(); ++it) // 推荐! ``` - **后置递增**: 仅当需要原始值时才使用: ```cpp int oldValue = counter++; // 需要递增前的值 ``` #### 总结 | 特性 | 前置递增(++i) | 后置递增(i++) | |--------------|----------------------|------------------------| | **运算顺序** | 先增后取值 | 先取值后增 | | **返回值** | 递增后的引用 | 递增前的副本 | | **性能** | 高效(无临时对象) | 较低(需创建副本) | | **重载语法** | `T& operator++()` | `T operator++(int)` | > 关键原则:**除非必须使用原始值,否则优先选择前置递增**。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

许先森森

爱我就打我

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值