通过之前的几篇博客,我已经知道了.
虽然 javascript
不像传动的 java
和 .Net
那样,有非常完毕的继承系统.
但通过 JavaScript
的构造器函数对象的 .prototype
属性,我们可以完成一些类似于继承的操作.
补充记忆:
实例对象对原型对象的修改是COW(copy on write)
简单的继承体系
在javascript
中,有一种特别特殊,又被我们常常忽略掉的对象.
那么就是函数对象.
特殊之处在于,所有的函数都可以当做是构造器存在.
当使用 new 来调用这个所谓的构造器(不管这个函数是否是以构造一个对象的功能作用而声明的).
在此函数内部都会有一个 this 关键字.
和普通调用函数不同的是.
当时用 new 调用函数时,情况就会非常简单
里面的this就是构造出来的那个对象.
且这个对象默认会从构造器的 .prototype继承属性或者方法.
同时还有一条非常隐蔽的链条.
构造器的.prototype 同时也是继承 Object.prototype 的.
function Animal (name) {
this.name = name || 'Animal'
this.sleep = function () {
console.log(this.name + ' sleep')
}
}
const cat = new Animal() // 用new调用,而不是像普通函数那样调用.于是 this 就指向明晰了,就是构造出来的 cat 对象.
Animal.prototype.eat = function () {
console.log(this.name + ' eat')
}
cat.eat() // 所有的构造出来的对象,都会从构造它的函数的prototype上继承.
// 一条比较隐蔽的继承链(也就说所谓的原型链)
console.log(AnimalAnimal.prototype.__proto__ === Object.prototype) // true
一张图
其中,画红色箭头就是时常会忽略,但是为什么原型链为什么会这么完整的核心.
也就是为什么所有对象可以正常的调用
Object.prototype.functions
的原因.
实现继承的方式一 - 原型继承
我们都知道,如果使用new关键字,把一个函数当构造器来使用,那么函数构造器是会返回一个对象的.
且返回的这个对象,会从此构造器的prototype上继承一些属性.
而客观存在的情况是,构造器prototype本身不是只读的.
我们甚至可以修改覆盖它的配置.
让它变成一个我们希望可以继承的对象.
比如:
function Animal() { }
const parentObject = {
name: '我是被继承的数据',
fn () {
console.log('我是被继承的方法')
}
}
Animal.prototype = parentObject
const a = new Animal()
console.log(a.name)
a.fn()
有了这个基本的前提之后,就开始定义我们继承自 Animal
构造器的子类 Cat
了.
function Animal (name) {
this.name = name || 'Animal'
this.sleep = function () {
console.log(this.name + ' sleep')
}
}
Animal.prototype.eat = function () {
console.log(this.name + ' eat')
}
function Cat () { }
Cat.prototype = new Animal('狗子')
const cat = new Cat()
console.log(cat.name)
cat.sleep()
cat.eat()
原型继承的核心就是上述代码:Cat.prototype = new Animal('狗子')
我们让自己定义的构造器的 prototype 对象指向父类构造器生成的对象.
由于父类构造器生成的对象包含了,父类实例定义的所有属性以及父类构造器原型上的属性.
所以,子类可以完整的从父类那里继承所有的属性.
一张图
实现继承的方式二 -- 借用函数继承
在说明这个这种继承方式之前,首先要稍微复习一下.
JavaScript
中 函数作为对象,它除了和普通对象一样有 proto 属性以外.
还有方法.
其中就有两个比较常用的办法 call
& apply
.
在 JavaScript
的 函数调用中.
函数从来都是不独立调用的.
在浏览器环境里.
function somefn () {}
somefn()
// 等同于
someFn(window)
对于一些其他的常用的函数调用模式.
obj.method()
//
其实等同于 method(obj)
所以,函数的调用从来都不是独立存在的.都会默认有一个隐蔽的参数.
我们可以通过 函数对象本身的 call
和 apply
来显示的指定函数调用时的这个必备的参数是谁.
obj.method.call(obj2)
此时,在obj
里定义的函数内部访问this
不是 obj
,而是 obj2
了.
有了上述复习.
可以开始写构造器继承了.
首先定义一个基类
function Animal (name) {
this.name = name || 'Animal'
this.sleep = function () {
console.log(this.name + ' sleep')
}
}
然后定义子类 Cat
function Cat (name) {
Animal(this, name)
}
关键一句是在 Animal.call(this,name)
虽然,之前,我们都把 Animal
当成构造器存在,要使用new关键字来调用.
但是在这里,我们把 Animal当成普通函数而非构造器.
利用普通函数的 call
方法,改变 this
..
const cat = new Cat('葫芦娃')
console.log(cat.name)
cat.sleep()
这里的 this 是由 new Cat('葫芦娃')
来创建的,所以就表明了是 cat
的一个实例.
结果:
这种继承方式有一个违反直觉的缺点:
既然我们本意是让
Cat
继承自Animal
我们当然也希望Cat
能当做原型继承那样能够正常的调用Animal.prototype
上的方法.
但这种方式不行.
Animal本来是个构造函数.
但是由于,借用函数继承,把它当成了一个普通的函数来使用.(调用.call
方法)
所以 new Cat()
对象,无法调用Animal函数定义在 prototype 上的属性和方法.
function Animal (name) {
this.name = name || 'Animal'
this.sleep = function () {
console.log(this.name + ' sleep')
}
}
Animal.prototype.run = function () {
console.log(`${this.name} run!`)
}
function Cat (name) {
Animal.call(this, name)
}
const cat = new Cat('葫芦娃')
console.log(cat.name) // 没问题
cat.sleep() // 没问题
cat.run()// cat.run is not a function
一张图
红色的路径,压根就不在 Cat
的原型继承链条中,所以就无法使用到 Animal.prototype
上的属性和方法了.
实现继承的方式三 -- 组合继承
组合继承,组合的是:
- 原型链继承
- 借用函数继承
这种方式的做法,是为了解决:
借用函数构造方法,无法使用函数原型上的属性和方法而产生的.
function Animal (name) {
this.name = name
this.eat = function () {
console.log(`${this.name} eat`)
}
}
Animal.prototype.run = function () {
console.log(`${this.name} run`)
}
function Cat (name) {
// 实例数据继承到了. name,eat()
Animal.call(this,name)
}
// 原型数据继承到了 run()
// 原型数据继承到了 run()
Cat.prototype = new Animal('🐶') // 这样写,会造成两次Animal实例化.且没有自己的原型了.
Cat.prototype = Animal.prototype // 这样写,不会造成两次Animal实例化,且没有自己的原型了.
const cat = new Cat('🐶')
cat.eat()
cat.run()
结果
- 使用
Animal.call()
来继承Animal
的实例属性和方法. - 使用
Cat.prototype = Animal.prototype
来使用Animal.prototype
属性和方法. 这样避免了两次调用Animal
构造函数,但是Cat
没有自己的原型prototype
- 使用
Cat.prototype = new Animal()
会造成两次构造函数调用.第一次new Animal()
,第二次:Animal.call(this,name)
,同样的让Cat
也弃用了自己的原型prototype
实现继承的方式四 -- 原型式继承
原型式继承的核心,其实很简单.
需要提供一个被继承的对象.(这里不是函数,而是是实实在在的对象)
然后把这个对象挂在到某个构造函数的prototype上.
此时,如果我们使用这个构造函数的new,就可以创建出一个对象.
这个对象就继承了上述提供的实实在在对象上的属性和方法了.
function inherit (obj) {
function Constructor () { } // 提供一个函数
Constructor.prototype = obj // 设置函数的 prototype
return new Constructor() // 返回这个函数实例化出来的对象.
}
function Animal (name) {
this.name = name
this.eat = function () {
console.log(`${this.name} eat`)
}
}
Animal.prototype.run = function () {
console.log(`${this.name} run`)
}
const animal = new Animal('小猫')
const cat = inherit(animal) // cat 要从animal对象上继承它所有的方法和属性.
cat.eat()
cat.run()
结果:
这种继承方式,就是可以创建出一个继承自某个对象的对象.
Object.create 方法内部差不多也是这么一个实现原理.
const cat2 = Object.create(animal, {
food: {
writable: true,
enumerable: true,
configurable: true,
value: '小鱼干'
}
}) // cat2 对象从 animal 对象上继承. 并扩展自己一个food属性.
cat2.name = '小猫2'
console.log(cat2.food)
cat2.run()
cat2.eat()
从一个对象继承,而不是类.
弱化的类的概念.
实现继承的方式五 -- 寄生式继承
寄生?
寄生谁?
就是把上述的 inherit
函数在包装一下.
function inherit (obj) {
if (typeof obj !== 'object') throw new Error('必须传入一个对象')
function Constructor () { }
Constructor.prototype = obj
return new Constructor()
}
function createSubObj (superObject, options) {
var clone = inherit(superObject)
if (options && typeof options === 'object') {
Object.assign(clone, options)
}
return clone
}
const superObject = {
name: '张三',
age: 22,
speak () {
console.log(`i am ${this.name} and ${this.age} years old!`)
}
}
const subObject = createSubObj(superObject, {
professional: '前端工程师',
report : function () {
console.log(`i am a ${this.professional}`)
}
})
subObject.speak()
subObject.report()
结果:
仍然没有class
的概念. 依然是从对象上继承.
包装起来的意义在哪?
仅仅只是包装起来了而已...可以渐进增加一下对象的感觉????
实现继承的方式六 - 寄生组合式继承
上面讲述的 原型式继承 和 寄生式继承
都是对象在参与,弱化了类的概念.
而继承应该是由类来参与的.(之类说的的类来参与指的是让构造函数的prototype
来参与)
所以,寄生组合式继承还是让类来参与继承.
function inheritPrototype (SuperType, SubType) {
if (typeof SuperType !== 'function' || typeof SubType !== 'function') {
throw new Error('必须传递构造函数!')
}
// 这个地方利用Object.create(Subtype.prototype)
// 非常巧妙的让Subtype.prototype对象继承自 SuperType.prototype.
// 而不是去覆盖自己.
// 特别注意:!!!!!!!!!!!!! Object.create 方法会返回一个对象 obj. obj.__proto__ = Object.create 函数接受的参数.
// 所以,任何在此代码前给 obj 设置的属性和方法,都应该在此方法执行完毕之后在执行,否则会被覆盖.
// 引用都变了,当然会时效.
SubType.prototype = Object.create(SuperType.prototype)
}
inheritPrototype(SuperType, SubType)
function SuperType (name) {
this.name = name
this.showName = function () {
console.log('from SuperType:' + this.name)
}
}
SuperType.prototype.super_protoProperty = 'SuperType原型属性'
SuperType.prototype.super_protoFunction = function () {
console.log('SuperType原型方法')
}
function SubType (name, age) {
SuperType.call(this, name)
this.age = age
this.showAge = function () {
console.log('from SubType:' + this.age)
}
}
SubType.prototype.sub_protoProperty = 'SubType原型属性'
SubType.prototype.sub_protoFunction = function () {
console.log('SunType原型方法')
}
const sub = new SubType('张三', 22)
sub.showAge()
sub.showName()
console.log(sub.super_protoProperty) // 拿不到 undefined
sub.super_protoFunction() // 方法不存在.
sub.sub_protoFunction() // 拿自己的原型没问题
console.log(sub.sub_protoProperty) // 拿自己的原型没问题
核心代码就是上述的
SubType.prototype = Object.create(SuperType.prototype)
这句代码利用 Object.create()
方法,非常巧妙的让
SubType.prototype
继承 SuperType.prototype
这儿做: SubType
既保留了自己的原型对象.又能从 SuperType
的原型上继承.
运行结果:
from SubType:22
from SuperType:张三
SuperType原型属性
SuperType原型方法
SunType原型方法
SubType原型属性
这样做法的好处非常明显.
子类不光可以从父类继承实例属性.(SubType.call(this).
还能从父类的原型继承属性 (SubType.prototype = Object.create(SubperType.prototype)
一张图
-
SubType
从SuperType.call(this)
继承到了SuperType
的实例属性. -
SubType
在new SubType
里声明了自己的属性. - 由于
Subtype.prototype
不是想原型组合集成那样是覆盖自己的原型,而是让原型对象继承子SuperType.prototype
. - 所以
SubType.prototype
原型对象仍然存在.所以SubType
可以从自己的原型上继承. - 同时
Subtype.prototype
:SuperType.prototype
. 所以,Subtype
还可以从SuperType.prototype
上继承属性.
new SubType()
- 自己的实例属性 --> bingo
- 自己的原型对象 ---> bingo
- 父类的实例属性 ---> bingo
- 父类的原型对象 ---> bingo