继承

124 阅读14分钟

参考了这篇,结合自己的理解写的

如何阅读继承相关代码?

读这种继承代码最好的方式就是看你是否能用笔画出构造函数新建的对象的完整结构

  • 你直接先找到let obj=new FunctionName()这句话去用你的大脑跟随代码运行一次
  • 以下为寄生组合式继承

构造一个对象有两种方式

  • 构造函数+prototype
  • class

对构造函数的理解

在我看来 构造函数就是普通的无return函数但是里面大量用到了this而已

// 现有如下这个最基础的'构造函数',该如何调用它产生一个对象呢?
function Parent(name) {
	this.name=name; 
}
let obj=Parent('ryan');
console.log(window.name) // ryan
  • let obj=Parent('ryan')当然是错误的,Parent根本没有return啊,obj有东西就见鬼了
  • 而且里面的this指谁呢?(我们都知道直接调用一个函数,函数内部的this就是window)
function Parent(name) {
	console.log(this)
}
Parent();// window
  • 因此直接这样调用构造函数你,里面this哪会有什么作用,只会为window徒增一个name属性

那么我们为Parent指定this好不好?

  • 比如我们用call指定:let obj=Parent.call({})
function Parent(name) {
	this.name=name; 
}
Parent.call({},'ryan') // 那我就给它一个空对象好了,试图让它把name放在我的空对象name上
  • 但是发现没有,这个构造函数并不会给我们return新对象啊,无法接收这个对象

于是乎,new应运而生

  • 构造函数正确使用方法是let obj=new Parent('ryan')
function Parent(name) {
	this.name=name;
}
let obj=new Parent('ryan')
console.log(obj)
  • 现在我们就能明白了,new为我们将this指向新对象obj,同时也会自动return这个对象
  • 但是如果你就是像上面这样设置一个如此简单的构造函数就去new的话,显然没有充分利用到prototype

let obj={} 与 let obj1=new MyConstructor() 创建出的对象有何区别?

  • 前者相当于一个一个地造对象的方式,后者相当于批量造对象
  • 后者可以设置一个原型,然后通过new的方式让原型上的属性全都自动给到对象,批量操作更加快捷~

对继承的理解

  • 继承只是为了实现新建的子对象能与生俱来拥有一些属性(方法)而已
    • es6之前没有太固定的套路,所以es6之前很多种继承的方式都可以叫继承
  • 不用刻意在意什么父类子类的,没有绝对的父类和子类的区别

继承其实就分为两类

  • es6之前都属于基于原型链的继承(细分又可分以下两种)
    • 构造函数方式实现的
    • 非构造函数方式实现的
  • es6官方的class extends

基础概念

  • parentFunc:父构造函数, childFunc:子构造函数, obj: 构造函数生成的子对象
  • 属性即包含了方法
  • 构造函数的 [自身属性] 是 [每个obj] 的私有属性, 构造函数的 [原型属性] 是 [每个obj] 的公有属性
  • 构造函数只是继承的基础知识而已
  • 如果控制台打印出一个对象时是如下效果,那么不用惊讶
function Myconstructor(name) {
    this.name = name
}
let obj=new Myconstructor('ryan')
console.log(obj)
  • 你会看到最前面有个Myconstuctor,表示的是这个对象是用哪个构造函数new出来的

以下6种方式的分类

  • 前三种是基于原型链的继承且以构造函数的方式(即 new MyFunction()调用)
  • 后三种是基于原型链的继承但以非构造函数的方式(即 MyFunciton()调用)

(借用)原型链式继承

  • 很多地方称之为原型链式继承,我认为更准确应该叫 借用 parentFunc.prototype 的原型链式继承

核心思想

设置childFunc.prototype= [parentFunc生成的obj] 可以让 [childFunc生成的obj] 通过层层的.__proto__不仅访问到childFunc [自身属性] 与 [原型属性] 还能访问到parentFunc的 [原型属性]。

function Parent() { // 父构造函数
	this.gender='male'
}
Parent.prototype.age = 18
Parent.prototype.getName = function () {
    return this.name // 3处
}

function Child(name) { // 子构造函数
    this.name = name
}
Child.prototype = new Parent() // 1处 这句是原型链式继承的关键 
Child.prototype.constructor = Child // 4处 如何理解?

var child = new Child('leo') // 2处
// 这样子类就可以调用父类的属性和方法
console.log(child.getName())    // leo
console.log(child.age)          // 18

可能有人会问为什么不直接写Child.prototype = Parent.prototype呢? 如果是这样的话,注意,parentFunc的 [自身属性] 就无法得以继承

原型链式继承的理解

  • 1处:new Parent()即生成一个Parent对象aaa(即Child.prototype),此对象自身无属性,但是有原型属性age与getName
    • 此时你直接Child.prototype.age为18,但是Child.prototype.getName()为undefined,因为this指的是点前的Child.prototype
    • 我们假设Child.prototype.name=111然后我们调用一下Child.prototype.getName()能发现这确实是打印出111,印证了我们的第二步
  • 2处:随后因为Child本身自己也是一个构造函数,所以当然也是可以new的,于是乎child自身有了child.name即leo
  • 3处:如何理解3处的这种写法是可以让child.getName()时能打印出leo的呢?
    • 其实很简单,this指的就是点前的对象child,恰巧child自身有name
  • 4处:解释一下为什么要板正Child.prototype.constructor的指向,主要是为了避免childFunc实例的继承链紊乱,儿子直接叫爷爷为爸爸了
  • 如果不写4处,那么
console.log(child.__proto__.constructor === Parent) // true  childFunc实例的继承链发生了紊乱 
  • 如果写4处,那么
console.log(child.__proto__.constructor === Child) // true 

我们知道,每个函数都生来自带 fn.prototypefn.prototype.constructor === fn (*这是上一章的公式2)

// 这里顺便给出验证
function fn(){}
console.log(fn.prototype.constructor === fn) // true 

Child.prototype = new Parent() 这句话把Child这个childFunc的原型销毁重置了,
原先的Child.prototype.constructor === Child不复存在,
现在,Child.prototype.constructor 就相当于 [ parentFunc的实例 ].constructor

let obj=new Parent();
console.log( obj.constructor === obj.__proto__.constructor ) // true
// obj.constructor 即为 obj.__proto__.constructor 当然指向的是Parent

原型链式继承的缺点

  1. 由于原型链上的属性对 [所有obj] 而言是共用的,因此会带来一些数据混用的问题。
  2. 此外,obj 无法把参数传给 parentFunc这个函数
// 这里向你展示原型链式继承的缺点
function Parent() {
    this.likeFood = ['水果', '鸡', '烤肉']
}
Parent.prototype.age = 18
Parent.prototype.getName = function () {
    return this.name
}
function Child(name) {
    this.name = name
}
Child.prototype = new Parent()
var chongqiChild = new Child('重庆孩子')
var guangdongChild = new Child('广东孩子')

// 重庆孩子还喜欢吃花椒。。。
chongqiChild.likeFood.push('花椒')
console.log(chongqiChild.likeFood)      // ["水果", "鸡", "烤肉", "花椒"]
console.log(guangdongChild.likeFood)    // ["水果", "鸡", "烤肉", "花椒"]

借用构造函数式继承

  • 注意到没有,这里根本不用prototype原型了,根本就没有Child.prototype = new Parent()这句话了
  • 不用原型也就是不用共用空间了,自然就是让每个子对象拥有自己独立空间

核心思想

childFunc 内call parentFunc,以达到 [childFunc生成的obj] 可以继承 parentFunc [自身属性] 的目的

function Parent(name) {
    this.name = name 
    this.likeFood = ["水果", "鸡", "烤肉"] // 
}
function Child(name) {
    Parent.call(this, name) // 2处   这一步是关键,这是必须把this传过去的
}
Parent.prototype.getName = function() {
    return this.name
}
var chongqingChild = new Child('重庆孩子') // 1处
var guangdongChild = new Child('广东孩子')
chongqingChild.likeFood.push('花椒')

console.log(chongqingChild.likeFood)    //  ["水果", "鸡", "烤肉", "花椒"]
console.log(guangdongChild.likeFood)    //  ["水果", "鸡", "烤肉"]
console.log(chongqingChild.name)        //  "重庆孩子"
console.log(chongqingChild.getName())   //  报错,无法调用父类方法

借用构造函数式继承的理解

2处的作用是什么呢?如何理解呢?

  • 当你试图创建新对象时即你在执行new Child()的时候,new会自动去运行Child中的代码
  • 2处的意图是运行一下Parent里面的代码,让它为自己的新对象所用(this指向新对象)
    • 这时你可能会问2处写成Parent(name)不行吗?当然不行,因为Parent函数运行时,里面的this是window啊,为window徒增name和likeFood而已啊
  • 我们需要Parent.call(this, name)把Child里的this传过去
  • 那么你可能会问了,为什么这里的this就这么确定是新对象呢?
  • 你就思考如果用如下构造函数当你执行let obj=new Child()时,其中的this指谁?当然是指向新对象obj自己!
function Child(name) { 
    this.name = name 
}

总结:这样就把原型链式继承中的缺点给弥补了,因为数组也都是每个对象自身的而非共用的

借用构造函数式继承的缺点

- 我们把原型链式继承的两个缺点解决了:
	- 1.父类 [自身属性] 不再共用 
	- 2.我们也可以给parentFunc传参了(给父类传了name)
- 但是我们不能调用父类的 [原型方法] 了

组合式继承

  • 为了能用到父类身上的方法,因此写了Child.prototype = new Parent(),但是这里就产生了冗余属性name和likefood,具体看图:
  • 为了数据独立因此childFunc内部call parentFunc让parentFunc的属性在childFunc上复制一份
function Parent(name) {
    this.name = name
    this.likeFood = ["水果", "鸡", "烤肉"]
}
function Child(name, age) {
    Parent.call(this, name) // 2处  给父类传参name  第二次调用parent
    this.age = age
}
Parent.prototype.getName = function() { // 父类方法
    return this.name
}
Child.prototype = new Parent() // 1处 第一次调用parent
Child.prototype.constructor = Child // 3处
Child.prototype.getAge = function() {
    return this.age
}
var chongqingChild = new Child('重庆孩子', 18)
var guangdongChild = new Child('广东孩子', 19)
chongqingChild.likeFood.push('花椒')

console.log(chongqingChild.likeFood)    // ["水果", "鸡", "烤肉", "花椒"]
console.log(guangdongChild.likeFood)    // ["水果", "鸡", "烤肉"]
console.log(chongqingChild.name)        // "重庆孩子"
console.log(chongqingChild.getName())   // "重庆孩子"
console.log(chongqingChild.getAge())    // 18

组合式继承的理解

- 这里有两次调用父构造函数Parent
- 第一次在1处,Child 的原型被赋值了 name 和 likeFood 属性
- 第二次在2处,注入this,会在Child 的实例对象上注入 name 和 likeFood 属性,这样就屏蔽了原型上的属性,但却可以享受到父类原型上的getName,但这就是数据的冗余啊!

看懂了前两种方式(原型链式继承与借用构造函数式继承),这个组合式继承就没啥好解释的了
1处:原型链式继承的手法
2处:借用构造函数式继承的手法
3处:不过是形式主义,凑数用的

组合式继承的总结

解决了上述两种方式的所有缺点,  
1.父类 [自身属性] 不再共用   
2.给父类传参     
3.可以调用父类的 [原型方法]

但是由于二次调用parentFunc(),导致parentFunc的[自身属性]有一份冗余存在

原型式继承

  • 初衷:我手头有一个对象,我只想用你的属性,不想搞得那么麻烦的parentFunc和childFunc
  • 核心:需要创建一个临时的构造函数,设置一个对象作为临时构造函数的原型
    • 原型式继承的调用方式并非严格意义的构造函数调用方式(即new)
    • 但是原型式继承的内部确实是用构造函数实现的
function realizeInheritance(parent) {
    function tempFunc() {} // 临时函数
    tempFunc.prototype = parent
    return new tempFunc() // 返回一个空对象,这个对象的原型是传进来的参数
}
var baba = {
    name: "爸爸",
    likeFoods: ["水果", "鸡", "烤肉"]
}
var child1 = realizeInheritance(baba)
var child2 = realizeInheritance(baba)
child1.likeFoods.push('花椒')
console.log(child1.likeFoods) //    ["水果", "鸡", "烤肉", "花椒"]
console.log(child2.likeFoods) //    ["水果", "鸡", "烤肉", "花椒"]

由原型式继承引出Object.create(parentObj)

ES5新增了Object.create(parentObject) 函数来更加便捷的实现上述继承

var baba = {
    name: "爸爸",
    likeFoods: ["水果", "鸡", "烤肉"]
}
var child1 = Object.create(baba)
var child2 = Object.create(baba)
child1.likeFoods.push('花椒')
console.log(child1.likeFoods) //    ["水果", "鸡", "烤肉", "花椒"]
console.log(child2.likeFoods) //    ["水果", "鸡", "烤肉", "花椒"]

原型式继承的理解

不论是临时构造函数还是Object.create()生成的对象都是空对象,只不过.--proto--属性引用着父对象而已

原型式继承的缺点

显然,父类的属性对于子类来说又是共享的。所以,如果我们只是想把一个对象和另一个对象保持一致,这可以。

寄生式继承

别被名字吓唬到,寄生式继承也就是在原型式继承的基础上多了一个函数,往创建的空对象自身上放一个属性而已,所谓的工厂模式就是如此

function realizeInheritance(parent) { // 照抄原型式继承
    function tempFunc() {}
    tempFunc.prototype = parent
    return new tempFunc() // 返回一个空对象,这个对象的原型是传进来的参数
}
// Parasitic: 寄生的    inheritance: 继承    一个最简单的工厂函数。
function parasiticInheritance(object) {
    var clone = realizeInheritance(object) // 得到了一个空对象(有原型)
    clone.sayName = function() { // 给空对象自身设置sayName
        console.log('我是'+this.name)
    }
    return clone
}
var baba = {
    name: "爸爸",
    likeFoods: ["水果", "鸡", "烤肉"]
}
var child = parasiticInheritance(baba)
child.name = '儿子'
child.sayName() // 我是儿子

寄生式继承的理解

也就是在原型式继承所建的对象身上运用所谓的工厂模式加了点自身的属性

寄生组合式继承

function Parent(name) { // parentFunc
    this.name = name
    this.likeFood = ["水果", "鸡", "烤肉"]
}
function Child(name, age) { // childFunc
    Parent.call(this, name)
    this.age = age
}
Parent.prototype.getName = function() { 
    return this.name
}

// 以下这两句使用新方法解决
// 这两句本来作用是为childFunc设置原型--->parentFunc的子对象
// childFunc的构造函数--->childFunc

// Child.prototype = new Parent()  
// Child.prototype.constructor = Child

inheritPrototype(Child, Parent) 

// inheritPrototype作用是让parentFunc.prototype作为每个子对象的第二层原型  
function inheritPrototype(childFunc, parentFunc) {
    // 创建一个空对象名叫prototype,这个空对象.prototype->parentFunc.prototype
    var prototype = realizeInheritance(parentFunc.prototype) 
    // 设置空对象的构造函数->childFunc  
    prototype.constructor = childFunc             
	// childFunc.prototype->这个空对象
    childFunc.prototype = prototype               
}
function realizeInheritance(parent) { // 寄生式继承
    function tempFunc() {}
    tempFunc.prototype = parent
    return new tempFunc() // 返回一个空对象,这个对象的原型是传进来的参数
}

Child.prototype.getAge = function() {
    return this.age
}


// ---------------直接看这部分----------------
var chongqingChild = new Child('重庆孩子', 18)
var guangdongChild = new Child('广东孩子', 19)
chongqingChild.likeFood.push('花椒')
// -----------------输出结果------------------
console.log(chongqingChild.likeFood)    // ["水果", "鸡", "烤肉", "花椒"]
console.log(guangdongChild.likeFood)    // ["水果", "鸡", "烤肉"]
console.log(chongqingChild.name)        // "重庆孩子"
console.log(chongqingChild.getName())   // "重庆孩子"
console.log(chongqingChild.getAge())    // 18

寄生组合式继承的理解

我们可以通过输出结果看出:  
得到的子对象得到了parentFunc身上的属性name与likefood,得到了childFunc身上的属性age
且每个子对象的第一层原型挂着childFunc的原型方法getAge,
每个子对象的第二层原型还挂着parentFunc的原型方法getName
两个对象之间的引用类型数据相互独立
inheritPrototype函数的作用是让parentFunc.prototype作为每个子对象的第二层原型  
(事实是:每个子对象的第一层原型也就是childFunc的原型)

es6 class extends

看了上面的那些,会觉得这个太简单了

class Person { // 这个算父类
    constructor(name, age) {
        console.log('父类构造函数执行。')
        this.name = name
        this.age = age
        this.commonLikeFood = ["水果", "鸡", "烤肉"]
    }
    showInfo() {
        console.log(`我是${this.name},我今年${this.age}岁了`)
    }
    showLikeFood() {
        console.log(`我是${this.name},我喜欢吃${this.commonLikeFood}`)
    }
}
class Child extends Person{ // 这个算子类
    constructor(name, age) {
        super(name, age)    // 向父类中传参,要写在第一行
        console.log('子类构造函数执行。')
    }
}
let child1 = new Child('小明', 27)
let child2 = new Child('小红', 28)

child1.commonLikeFood.push('火锅')

child1.showInfo()   // 我是小明, 我今年27岁了
child1.showLikeFood()   // 我是小明, 我喜欢吃水果,鸡,烤肉,火锅

child2.showInfo()   // 我是小红, 我今年28岁了
child2.showLikeFood()   // 我是小红, 我喜欢吃水果,鸡,烤肉

总结

以上所有写法用到了哪些模式:
所有的模式都只不过是一种写法而已,别太当回事以为有多难以理解和高大上

  • 工厂模式:创建中间对象,给中间对象添点属性(方法)再返回出去。
  • 构造函数模式:就是自定义函数,并用过 new 关键子创建实例对象。缺点也就是无法复用。
  • 原型模式: 使用 prototype 来规定哪一些属性和变量能被共享。

以上所有写法优缺点分析:

  • 原型链继承:
优点:只调用一次parentFunc,能复用原型链属性
缺点:引用类型的数据也被共享,影响数据之间独立性;无法向parentFunc传参。
  • 借用构造函数式继承:
优点:可以向parentFunc传参;属性可以不被共享。
缺点:无法使用parentFunc上的属性(方法)
  • 组合式继承
优点:可以传参,属性可独立,能使用原型链上的属性(方法)
缺点:parentFunc被调用2次,childFunc.prototype上有冗余属性
  • 原型式继承:(用于对象与对象之间)
优点:在对象与对象之间无需给每个对象单独创建自定义函数即可实现对象与对象的继承,无需调用构造函数。
缺点:父对象属性被完全共享。
  • 寄生式继承:
优点:基于原型式继承仅仅可以为子类单独提供一些功能(属性),无需调用构造函数。
缺点:父类属性被完全共享。
  • 寄生组合式继承:
优点:组合继承+寄生式继承,组合继承缺点在于调用两次父类构造函数,
子类原型有冗余属性,寄生式继承的特性规避了这类情况,
集寄生式继承和组合继承的优点与一身,是实现基于类型继承的最有效方式。
  • class & extends:
优点:个人认为是十分相似寄生组合继承,几乎可以说是寄生组合继承的语法糖。
但还是有一点的区别,就是寄生组合继承是先创建子类实例对象,然后再对其增强;
而ES6先将父类实例对象的属性和方法,通过调用super方法,
添加到实例对象上,然后再用子类的构造函数,改变this指向。