面向对象中必须要知道的继承
身为一个靠js吃饭的程序员.我们必须明白一点.
JavaScript是最纯正的面向对象的语言
JS中没有类这个玩意儿,我们在ES6看到的Class是基于原型来模拟的.也称呼为[[模拟类]]. 比爪哇,C什么加都要纯的多!!!🐶
我们为什么需要继承
const kitty = {
say: function() {
console.log('喵喵喵')
}
}
const tiger = {
say: function(value) {
console.log('呼呼呼')
}
}
const lion = {
say: function(value) {
console.log('嗷嗷嗷')
}
}
上面每个对象中都有一个say的属性, 写了三遍.这个时候,如果我们都需要在say中添加一些判断的话,就需要在每个对象中添加一遍.那就是三遍了. 所以说我们需要一个办法,把三个对象都能够引用同一样一个方法.实现代码的复用.
const Felidae = function({value}) {
this.value = value
this.say = function() {
console.log(this.value)
}
} const kitty = new Felidae({value: '喵喵喵' })
const tiget = new Felidae({value: '呼呼呼' })
const lion = new Felidae({value: '嗷嗷嗷' })
这样子我们就不用给每个对象都写一遍say的方法了.
这个时候我需要让Felidae也能够有一些其他科的共同的方法应该怎么做呢?
const Animal = function() {}
animal.prototype.run = function() {
console.log('I can run')
}
const Felidae = function() {}
Felidae.prototype = new Animal()
const Kitty = new Felidae()
conosle.log(kitty.run()) // 'I can run'
通过[[原型]] 的赋值就可以达到要求.
Kitty.run()的时候发生了什么
Kitty对象身上时不存在run 这个方法的.但是我们却能够直接调用它,这个过程中发生了什么呢?
要了解这个过程的话,我们需要一定的前置知识,[[对象的访问器属性]]和[[对象的属性描述符]]的相关知识.
几乎所有对象在创建之际,都会存在一个隐藏的属性,它就是原型[[prototype]]. 当我们在Kitty.run()的过程中. 就是默认的[[Get]]的过程,它会先寻找自身的属性,如果没有的话,就往[[prototype]]上面找.直到找到为止,或者说知道Object.prototye为止.
[[原型链]]的尽头就是Object.prototype
几乎这个词很准确,应该通过Object.create(null)创建的对象就很'干净'. 可以试着在控制器中打印出来看看.
[[Get]]的默认过程是这样,[[Put]]的过程呢?
Kitty.color = 'white' 发生的屏蔽属性概念
接前面的代码,给Kittiy对象赋值color.
Kitty.color = 'white'
console.log(Kitty.color) // 'white'
给Kittiy赋予color属性的过程中, 依旧会执行[[Get]]操作, 如果自身存在这个属性,就直接赋值就行.如果不存在,往[[原型链]]上面找.如果还是没有的话,会直接赋值color这个舒服给Kitty.但是如果原型链上存在color属性的话,默认情况下就会发生屏蔽属性.
所以屏蔽属性,就是最下层的属性名和[[原型链]]上面的属性有重名的情况下,底层的这个属性会把[[原型链]]上面给屏蔽了,双方都存在.打印的话,会直接打印出底层的这个属性的值.
但是这个是默认的情况下,如果出现如下代码中的情况的话,就有得说了.
const Felidae = function() {}
Object.defineProperties(Felidae .prototype, {
color: {
value: 'black',
}
})
const Kitty = new Felidae()
Kitty.color = 'white'
console.log(Kitty.color) // 'black'
显性的使用[[对象的属性描述符]]的话,除了value,其他不配置的话,默认都为false
上面的代码没有产生屏蔽属性,也没有改变color的值.意不意外. 还有下面这种,也不会产生屏蔽属性:
const Felidae = function() {}
Object.defineProperties(Felidae .prototype, {
color: {
set: function() {}
}
})
const Kitty = new Felidae()
Kitty.color = 'white'
console.log(Kitty.color) // undefined
由此我们可以总结出,当我们给一个对象赋值的时候,会出现的三种情况(出自[[你不知道的JavaScript上]]):
- 如果在[[prototype]]链上层存在名为color的普通数据访问属性,并且没有被标记为只读(writable: false),那就回直接给Kitty添加一个color属性,它是
屏蔽属性. - 如果在[[prototype]]链上存在color,但是它被标记为(writable: true,). 那么无法修改已有属性或者在Kitty上面创建屏蔽属性. 在ES5严格模式下甚至会直接报错.
- 如果[[prototype]]链上面存在color,且有set. 那就一定会调用set.color不会添加到Kitty中,也不会重新定义color这个setter.
依托于原型的继承
开头就说了JS不存在类, 而是基于原型的继承,也称之为原型继承. 在传统意义中的继承中,继承意味着复制的操作.所以很多人都称JS的对象继承就像现实中父与子的关系, DNA继承的关系等. 这样的说法不能说是完全不对,可以说是狗屁不通.
Js中的继承不存在复制的操作,而是对两个对象之间产生一定的关联,它们不是父与子的关系.在[[你不知道的JavaScript上]]中称呼它们之间的关系用了委托 一词. 更像是一用雇佣的关系,可以说是平级的关系.
改变this的指向来实现继承
function Animate() {
this.color = 'red'
}
function Kitty() {
Animate.apply(this)
}
const kitty = new Kitty()
console.log(kitty.color) // 'red'
Kitty拥有了Animate构造函数的属性. nice. 似乎是完成了继承.可是这样有一个致命的问题, Kitty没有办法继承Animate的原型.
function Animate() {
this.color = 'red'
}
Animate.prototype = {
brand: 'benz'
}
function Kitty() {
Animate.apply(this)
}
const kitty = new Kitty()
console.log(kitty.color) // 'red'
console.log(kitty.brand) // undefiend
原型关联new一个构造函数实现继承
我们通过下面的代码得出图中所示:
const Felidae = function() {}
Felidae.prototype = {
run: function() {
console.log('I can run')
}
}
const Kitty = new Felidae()
conosle.log(kitty.run()) // 'I can run'
const lion = new Felidae()
const FelineSubfamily = function() {}
FelineSubfamily.prototype = new Felidae()
const tiger = new FelineSubfamily()
kitty和lion以及tiger看似没有直接的关系,但是都间接的有指向Felidae.prototype. 和Java或者dart这些的继承不一样.它们在通过new之后,或者说是实例化之后,都是独立的个体.相互之间没有关系.
而JS的原型继承和Java背道而驰. JS中‘实例化’的实例甚至能够更改[[prototype]]上面的属性.
const Felidae = function() {
this.option = {
color: 'white'
}
}
const FelineSubfamily = function() {}
FelineSubfamily.prototype = new Felidae()
const Kitty = new FelineSubfamily()
Kitty.option.color = '妈咋'
const Lino = new FelineSubfamily()
console.log(Kitty.option.color) // ‘妈咋’
console.log(Lino.option.color) // '妈咋'
这个例子太典型了 , 所谓的'子级'改变了‘父级’的属性. 而且还连累了'兄弟'. 改变的是[[引用类型]].原始类型是否可以改变呢?
const Felidae = function() {
this.number = 1
}
const FelineSubfamily = function() {}
FelineSubfamily.prototype = new Felidae()
const Kitty = new FelineSubfamily()
Kitty.number++
const Lino = new FelineSubfamily()
console.log(Kitty.number) // 2
console.log(Lino.number) // 1
结果显示没有变化的. 这里面发生了什么呢?
解释这个过程呢,需要回到[[#Kitty run 的时候发生了什么]]中. 拆解一下Kitty.number++的过程:
kitty.number = Kitty.number + 1
这里直接给Kitty上赋予了一个number的属性.赋值操作是从右到左的.Kitty.number 在[[Get]]过程中,找到了原型链上面的number,值为1,所以1 + 1,返回2.而此时原型链上面number是默认的[[对象的属性描述符]].非只读状态. 所以是直接给Kitty上面赋予了 number的属性.
由此可以知道JS如此继承的隐患太大了. 也更加明确了对象的继承不是复制的操作. 为了减少这样的隐患.我们一般都将属性赋值到[[prototype]]上面:
const Felidae = function() {}
Felidae.prototype = {
color: 'red',
}
const FelineSubfamily = function() {}
FelineSubfamily.prototype = new Felidae()
const Kitty = new FelineSubfamily()
Kitty.color = 'white'
const Lino = new FelineSubfamily()
console.log(Kitty.color) // ‘white’
console.log(Lino.color) // 'red'
如此就可以消除,实例化对象直接通过访问成员属性来修改构造函数上面的属性. '兄弟级'也没有发生了意外的改变. 这样看起来,似乎对象的继承已经很安全了.其实不然, 这样依旧是不够安全;
functino Car() {
this.brand = 'benz'
}
Car.prototype = {
brand: 'Mazda',
intro: function() {
console.log(this.brand)
}
}
car.intro() // 'benz'
car.__proto__.intor() // 'Mazda'
这例子,同时也有对于[[This指向问题]]的解释
我们通过__proto__k 依旧能够访问到原型对象.
依旧可以通过__proto__去修改原型链上的东西.
function Test() {}
Test.prototype = {
color: 'red'
}
function A() {}
A.prototype = new Test()
const a = new A()
a.__proto__.color = 'green'
console.log(a.color) // 'green'
const b = new A()
console.log(b.color) // 'green'
创造一个中间构造函数来实现继承(企业级的做法)
function Test() {}
Test.prototype = {
color: 'red'
}
function Buffer() {}
Buffer.prototype = Test.prototype
const buffer = new Buffer()
function A() {}
A.prototype = buffer
A.prototype.color = 'green'
const a = new A()
console.log(a.color) // 'green'
const b = new Test()
console.log(b.color) // 'red'
通过一个中间的Buffer 就轻松的解决了来继承的对象改变Test原型上面的属性.
每次都创建一个Buffer 的话会很麻烦,可以封装一下
function inherit(Target, Origin) {
function Buffer(){}
Buffer.prototype = Origin.prototye
Target.prototype = new Buffer()
// 规范构造函数的数据
Target.prototype.constructor = Target
Target.prototype.super_class = Origin
}
自此.对象的原型继承相对稳定了. 其实就算加了一个缓冲的函数,子级依旧可以修改父级的属性.通过obj.__proto__.__proto__.xxx 还是能够把任何想要改的原型链上的东西.
还需要配合[[对象的属性描述符]]才可以更稳定.让原型链上的属性都配置上只读不可配置.
对象的原型继承如此的别扭,这不行那不行的.更不用说还有传统面向对象中,基于类的多态的概念.费劲. 所以说,react推出了hook, vue推出了3.0. 都提供了一定的函数式编程的概念.通过函数的组合来代替对象的继承. 🤓