首先放上让我收益匪浅的文章非常感谢
虽然很长但是很详细收益
原文已经讲的很清楚了,这里只记录通过这次被解答的疑惑。
为什么要继承
有个构造函数A,又有个构造函数B,B想用A里面的方法,我们可以CV,但我们是程序员,所以选择让代码来解决,那就是利用继承。
原型链就是和继承相辅相成的。
继承的原理就是利用原型链。当你新建了个A构造函数,但又想用B构造函数里面的方法的时候,我们可以通过原型链原理来实现,原型链原理就是当你访问A对象中没有的属性的时候,js引擎就会访问A对象的_proto_属性,看那有没有你访问的属性(没有就继续往上走),而_proto_属性本来指向的是A对象的构造函数functon A的原型A.prototype,我们可以通过修改A.prototype,使其A.prototype = new B()来继承B中的方法和属性。
1.原型链继承
原型链继承很好懂啊,核心就是Child.prototype = new Parent(),
但既然是原型链继承,那为什么不能是Child.prototype = Parent.prototype呢
这两个方式有什么区别吗?怎么理解呢?
首先Child.prototype = Parent.prototype
我们来看看Child.prototype = Parent.prototype会发生什么:
举例说明:
function Parent () {
this.name = 'Parent'
this.sex = 'boy'
}
Parent.prototype.getSex = function () {
console.log(this.sex)
}
function Child () {
this.name = 'child'
}
Child.prototype = Parent.prototype
var child1 = new Child()
console.log(child1.getSex) //能获取得到prototype上的方法
console.log(child1) //Child {name: "child"}
child1.getSex() //获取不到,undefined,因为没有继承parent里面的sex,而自己有没有sex所以获取不到
//改成这样才能获取到 Child.prototype = new Parent()
这样只能拿到父类原型链上的属性和方法那也太废了吧,只能访问到Parent.prototype上的东西,我可不止这样,我还想拿到父类构造函数上的属性。
那为什么Child.prototype = new Parent()就能拿到呢?
这是Child.prototype = Parent.prototype,打印的child1
这是Child.prototype = new Parent(),打印的child1
从两种方法拿到的Child实例来看,就很好懂了,Child.prototype = new Parent()之所以能拿到实例中的属性,是因为new Parent()这一步实例化了Parent:
console.log(new Parent())
然后Child.prototype指向的是实例化的Parent,要访问Parent实例上没有的属性,那才继续到Parent.prototype上找,比如说child1.getSex().
这是思维导图。
原型链继承的优缺点
直接上例子来展示
function Parent (name) {
this.name = name
this.sex = 'boy'
this.colors = ['white', 'black']
}
function Child () {
this.feature = ['cute']
}
var parent = new Parent('parent')
Child.prototype = parent
var child1 = new Child('child1') //传不了参 name还是parent
child1.sex = 'girl'
child1.colors.push('yellow') //parent实例中的colors被修改了
child1.cute = false //这样修改不了父例prototype上的属性,直接在child1实例上创建
var child2 = new Child('child2') //共享同一个parent,上面修改了color影响了这里的child2,传不了参 name还是parent
console.log(child1)
console.log(child2)
console.log(child1.name)
console.log(child2.colors)
console.log(parent)
答案
Child{ feature: ['cute'], sex: 'girl',cute: false } //点开发现他们的原型上的parent的属性都被push了yellow
Child{ feature: ['cute'] } //点开发现他们的原型上的parent的属性都被push了yellow
'parent'
['white', 'black', 'yellow']
Parent {name: "parent", sex: 'boy', colors: ['white', 'black', 'yellow'] }
解析:其实这里需要关注的点就是new Child传不了参,还有就是原型对象的属性被共享了,如果不小心修改了原型对象中的引用类型,那么所有子类创建的实例对象都会受影响(从child1.color.push后,再构造的child2可以看出来所有子例都受影响了)。
优点:
- 继承了父类的模板,又继承了父类的原型对象
缺点:
- 如果要给子类的原型上新增属性和方法,就必须放在Child.prototype = new Parent()后面
- 无法实现多继承(已经指定了原型对象了)
- 所有来自原型对象的所有属性都被共享了,如果不小心修改了原型对象中的引用类型,那么所有子类创建的实例对象都会受影响
- 创建子类时,无法向父类构造函数传参数(这点从child1.name可以看出来)
构造函数继承
这里我们也知道核心原理就是Parent.call(this, 'child'),在子类构造函数内部使用call或apply来调用父类构造函数
tips:这里提一下,call,apply函数是会立即执行的,bind返回的是函数,要手动执行。
function Parent (name) {
this.name = name
}
function Child () {
this.sex = 'boy'
Parent.call(this, 'child')
}
//相当于
function Child () {
this.sex = 'boy'
// 伪代码
this.name = 'child'
}
优点:
function Parent (name, sex) {
this.name = name
this.sex = sex
this.colors = ['white', 'black']
}
function Child (name, sex) {
Parent.call(this, name, sex)
}
var child1 = new Child('child1', 'boy')
child1.colors.push('yellow')
var child2 = new Child('child2', 'girl')
console.log(child1)
console.log(child2)
在原型链继承中我们知道,子类构造函数创建的实例是会查找到原型链上的colors的,而且改动它会影响到其它的实例,这是原型链继承的一大缺点。
而构造继承弥补了这一缺点:
解决了原型链继承中子类实例共享父类引用对象的问题,实现多继承,创建子类实例时,可以向父类传递参数
但是缺点也很明确,那就是构造继承只能继承父类的实例属性和方法,不能继承父类原型的属性和方法(不能获取Parent.prototype上的方法,比如说什么Parent.prototype.speak = () => {})
组合继承
- 使用原型链继承来保证子类能继承到父类原型中的属性和方法
- 使用构造继承来保证子类能继承到父类的实例中的属性和方法
var Parent = function (name, age) {
this.name = name
this.age = age
}
Parent.prototype.speak = function () {
console.log('speak')
}
var Child = function (name, age, sex) {
Parent.call(this, name, age)
this.sex = sex
}
Child.prototype = new Parent()
constructor
function Parent (name) {
this.name = name
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name) {
this.sex = 'boy'
Parent.call(this, name)
}
Child.prototype = new Parent()
Child.prototype.getSex = function () {
console.log(this.sex)
}
var child1 = new Child('child1')
var parent1 = new Parent('parent1')
console.log(child1.constructor) //f Parent () {}
console.log(parent1.constructor) //f Parent () {}
为什么?
parent1.constructor是Parent函数这个还好理解,结合上面👆的图片来看,只要通过原型链查找,我parent1实例自身没有constructor属性,那我就拿原型上的constructor,发现它指向的是构造函数Parent,因此第二个打印出Parent函数。
但是对于child1.constructor为什么指向了parent呢?
也很好理解啦。首先我们要明白一个前提,那就是Child.prototype = new Parent()。好了可以开始了,寻找constructor肯定会向上查找,通过_proto_找到它的构造函数的原型对象,这个原型对象的constructor就是我们要找的那个。
当我们想要获取child1.constructor,肯定是向上查找,通过__proto__找它构造函数的原型对象匿名实例。但是匿名实例它自身是没有constructor属性的呀,它只是Parent构造函数创建出来的一个对象而已,所以它也会继续向上查找,然后就找到了Parent原型对象上的constructor,也就是Parent了。
construcotr它不过是给我们一个提示,用来标示实例对象是由哪个构造函数创建的
为了纠正这个,就要加上这句:
Child.prototype.constructor = Child
组合继承优点:
将前两个继承方式结合起来:
- 可以继承父类实例属性和方法,
- 也能够继承父类原型属性和方法
- 弥补了原型链继承中引用属性共享的问题可传参,可复用
缺点:
function Parent (name) {
console.log(name) // 这里有个console.log()
this.name = name
}
function Child (name) {
Parent.call(this, name)
}
Child.prototype = new Parent()
var child1 = new Child('child1')
console.log(child1)
console.log(Child.prototype)
执行结果:
undefined
'child1'
Child{ name: 'child1' }
Parent{ name: undefined }
看到没有,执行了两次parent。
- 第一次是在原型链继承的时候,new Parent()
- 第二次是在构造继承的时候,Parent.call(),记得吧,call会立即调用的
也就是说在使用组合继承的时候,会凭空多调用一次父类构造函数。
另外,我们想要继承父类构造函数里的属性和方法采用的是构造继承,也就是复制一份到子类实例对象中,而此时由于调用了new Parent(),所以Child.prototype中也会有一份一模一样的属性,就例如这里的name: undefined,可是我子类实例对象自己已经有了一份了呀,所以我怎么也用不上Child.prototype上面的了,那你这凭空多出来的属性不就占了内存浪费了吗?
因此我们可以看出组合继承的缺点:
- 使用组合继承时,父类构造函数会被调用两次并且生成了两个实例,
- 子类实例中的属性和方法会覆盖子类原型(父类实例)上的属性和方法,所以增加了不必要的内存。
寄生组合继承
这是组合继承的更上一步。
首先我们直到组合继承的缺点是:
- 父类构造函数会被调用两次
- 生成了两个实例,在父类实例上产生了无用的属性
有没有一种方法,让我们直接跳过父类实例上的属性和方法,而让我直接就能继承父类原型链上的属性和方法呢?
也就是说,我们需要一个干净的实例对象,来作为子类的原型。并且这个干净的实例对象还得能继承父类原型对象里的属性。
Object.create()
用法:
Object.create(proto, propertiesObject)
这里着重看第一个参数proto(第二个没用到,在这里不重要),它的作用就是能指定你要新建的这个对象它的原型对象是谁。
有点抽象,举个例子吧:
就好像,我们用var parent1 = new Parent()创建了一个对象parent1,那么parent1.proto就是Parent.prototype,这是基础,很好理解吧。
使用var obj = new Object()创建了一个对象obj,那么obj的proto就是Object.prototype.
而Object.create(),现在就可以指定你新建对象的proto
Object.create(Parent.prototype)创建了一个空的对象,并且这个对象的__proto__属性是指向Parent.prototype的
tips:既然想不创建就能继承,为什么不用Child.prototype = Parent.prototype,
因为寄生组合继承中子类的 prototype 不能直接指向父类的 prototype 的原因是修改子类的 protoype 也会修改父类的 protypetype ,所以要使用Object.create(Parent.prototype) 创建一个新的。
function Parent (name) {
this.name = name
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name) {
this.sex = 'boy'
//这里保证了传参,并调用了一次
Parent.call(this, name)
}
// 与组合继承的区别,这里保证里继承,但又没有
Child.prototype = Object.create(Parent.prototype)
var child1 = new Child('child1')
console.log(child1)
child1.getName()
console.log(child1.__proto__)
console.log(Object.create(null))
console.log(new Object())
仅仅是Child.prototype = Object.create(Parent.prototype)一句的不同。
这是ES6以前的一种比较完美的继承方式,结合了上面所有继承的优点。
只调用了一次父类构造函数,只创建了一份父类属性子类可以用到父类原型链上的属性和方法能够正常的使用instanceOf和isPrototypeOf方法
原型式继承
原型式继承就是ES5以前没有Object.create()方法的替代方法。主要_proto_也是ES5后的东西。
ES6后直接使用object.create()来实现。
实现原理就是创建一个构造函数,构造函数的原型指向对象,然后调用new操作符创建实例并返回这个实例,本质是一个浅拷贝。
- 接受一个对象
- 返回一个新对象
- 新对象的原型链中必须能找到传进来的对象
代码:
function object(obj) {
function F () {}
F.prototype = obj
F.prototype.constructor = F
return new F();
}
混入方式继承多个对象
混入方式继承就是继承多个父类。
作用就是可以把多个对象的属性和方法拷贝到目标对象中,若是存在同名属性的话,后面会覆盖前面
function Child () {
Parent.call(this)
OtherParent.call(this)
}
Child.prototype = Object.create(Parent.prototype)
Object.assign(Child.prototype, OtherParent.prototype)
Child.prototype.constructor = Child