面向对象中必须要知道的继承

564 阅读8分钟

面向对象中必须要知道的继承

身为一个靠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上]]):

  1. 如果在[[prototype]]链上层存在名为color的普通数据访问属性,并且没有被标记为只读(writable: false),那就回直接给Kitty添加一个color属性,它是屏蔽属性.
  2. 如果在[[prototype]]链上存在color,但是它被标记为(writable: true,). 那么无法修改已有属性或者在Kitty上面创建屏蔽属性. 在ES5严格模式下甚至会直接报错.
  3. 如果[[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() 

IMG_0069.JPG 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. 都提供了一定的函数式编程的概念.通过函数的组合来代替对象的继承.  🤓