前端 JavaScript 之 继承 _ 原理版

目录

继承 : 

为什么需要继承 ? 

构造函数 : 

常见的继承方案 :  

1. 原型继承 : 

 图解 : 

2. call 继承 :

 扩展 : 第二种写法 

3. 组合继承 : 

4. ES6 的继承语法 :

简图 : 


继承 : 

继承 : 

        + 继承是一个关于  构造函数  的高阶应用

        + 继承一定是出现在 两个构造函数 之间的关系

        + 什么是 继承 : 

          => 当 构造函数 A 的 实例 , 使用了 构造函数 B 原型 上书写的属性和方法

          => 此时, 我们就说 A 和 B 出现了 继承关系

          => 构造函数 A 继承自 构造函数 B

          => 构造函数 A 是 构造函数 B 的 子类

          => 构造函数 B 是 构造函数 A 的 父类

为什么需要继承 ? 

    + 我们面向对象开发的核心, 是 =>  高内聚 低耦合  <=

    + 来思考 ? 进行假设 : 

      => 我书写一个 苹果类 :

        ~> 颜色, 品牌, 重量, 水分, 名称

        ~> 吃的方法: 削皮吃, 去核吃

      => 我书写一个 橘子类 :

        ~> 颜色, 品牌, 重量, 水分, 名称

        ~> 吃的方法: 剥皮吃, 吐籽吃

可以看到 : 苹果类 和 橘子类 有 共同的 部分

    + 所以换一种书写方法 : 

      => 水果类 : 

        ~> 颜色, 品牌, 重量, 水分, 名称

      => 苹果类 :

        ~> 继承 水果类

        ~> 书写我独立的内容

        ~> 形状

      => 橘子类 : 

        ~> 继承 水果类

        ~> 书写我独立的内容

        ~> 额外用途


构造函数 : 

+ 构造函数 : 

  => 原型 ( prototype ) :

**每一个函数 天生自带一个属性, 叫做 prototype , 是一个 对象数据类型 **

  => 为什么需要原型 :

    ~> 为了解决 构造函数 的不合理

    ~> 书写 构造函数 的时候

    ~> 属性 直接书写在 构造函数体内

    ~> 方法 书写在 构造函数 的 原型 上

  => 目的: 为了书写一些方法, 给到该 构造函数 的 实例 使用

    ~> 把每一个实例都会用到的方法, 提取出来放在了 构造函数 的 原型

A 继承自 C , A 是 子类 , C 是 父类

B 继承自 C , B 是 子类 , C 是 父类

继承的目的 , 就是为了能够使用 构造函数 C 身上的 属性 和 方法 , 

把公共的东西提取出来成为更公共的东西


常见的继承方案 :  

1. 原型继承 : 

原型继承(原型链继承) :

        + 核心: 让子类的  原型  指向 父类的  实例 

        + 语法: 子类.prototype = new 父类( )

      优点:

        + 父类的 构造函数体内的 属性 和 原型 上的 方法 都可以继承下来

      缺点:

        + 本该一个构造函数完成的所有参数, 需要分开两个地方传递

        + 同样都是给 子类实例 使用的属性, 在两个位置传递参数

        + 继承下来的属性不在自己身上, 子类的实例的所有属性分开了两部分书写

        + 访问的属性需要去到 __proto__ 上才能找到

// 父类
function Person(name, age) {
  this.name = name
  this.age = age
}
Person.prototype.sayHi = function () { console.log('hello world') }
/*
  父类的实例 :
    const p = new Person('Jack', 18)
    p === {
      name: 'Jack',
      age: 18,
      __proto__(Person.prototype): {
        sayHi: function () {},
        constructor: Person,
        __proto__(Object.prototype): {
          __proto__: null
        }
      }
    }
  不管是谁继承了 Person 类
    + 要求继承的构造函数的实例, 可以使用 name 和 age 属性
    + 要求继承的构造函数的实例, 可以使用 sayHi 方法
*/
// 子类
function Student(gender) {
  this.gender = gender
}

// 当 Student 构造函数书写完毕以后

// 此时会伴生一个 Student.prototype 是一个 对象数据类型

// 我就可以向 Student.prototype 上添加一个成员

// 因为 Student.prototype 是一个对象

// Student.prototype.a = 100

// 现在不直接向这个对象上添加内容了

// 而是可以把 Student.prototype 重新赋值

// 本身你的 Student.prototype 保存的是一个 对象数据类型 的 地址

// 我给你赋值为一个新的 对象数据类型 的地址

// 就把你本身保存的地址覆盖了

const obj = {

   message: '我是自定义的 obj 对象'

 }

 Student.prototype = obj

// Student.prototype 既然可以被赋值为一个对象, 那么就是任何一个对象都可以

// 那么我的 Person 的实例 p 也是一个 对象数据类型, p 是父类的实例对象

// 我就可以直接把 实例对象 p 赋值给 Student.prototype

// 因为这句代码的执行

// Student 的 实例 可以使用 Person 书写的属性和方法了

// 我们就说 Student 继承自 Person

// Student 是 Person 的 子类

// Person 是 Student 的 父类

// const p = new Person('Jack', 18)
// Student.prototype = p
// 进行一下 等量代换
Student.prototype = new Person('Jack', 18)
// s 是 Student 的实例
// s 所属的构造函数是 Student
// s.__proto__ 指向 所属构造函数的 Prototype
// 因为 Student.prototype === p
// s.__proto__ === p
const s = new Student('男')
console.log(s)

 

// 根据原型链的规则
// 对象访问
// 当你需要访问 s 的 gender 成员的时候
// s 自己本身就有, 直接使用
console.log(s.gender) // 自己有的属性 => 男
// 当你需要访问 s 的 name 成员的时候
// s 自己本身没有, 需要去到 自己的 __proto__ 上查找
// s 自己本身的 __proto__ 是 p, 所以就是在使用 p 身上的 name 成员
// p 是 Person 的实例, Student 的实例使用的是 Person 构造函数体内书写的 属性
console.log(s.name)  // 打印 => Jack
// 当你需要访问 s 的 sayHi 成员的时候
// s 自己本身没有, 需要去到 自己的 __proto__ 上查找
// 因为自己的 __proto__ 上也没有, 去到再 __proto__ 上查找
// 又因为自己的 __proto__ 就是 p, 再__proto__ 就是 p.__proto__
// 其实就是去 Person.prototype 上查找的内容
// 此时 Student 的实例使用了 Person.prototype 上书写的 方法
s.sayHi()  // 打印 => hello world

 图解 : 


2. call 继承 :

call 继承 ( 借用构造函数继承 ) :

  + 核心: 把父类构造函数体, 当做普通函数调用,

  利用 call 方法调用父类构造函数 并 改变 this 指向

  + 语法: 在子类构造函数体内书写  父类.call(this)

优点:

  + 一个实例使用的属性可以在一个位置传递参数

  + 可以把继承来的属性直接出现在 子类的实例 身上

缺点:

  + 只能继承 父类构造函数体内 书写的内容, 父类构造函数 原型上 的内容不能继承

 call 语法 回顾 : 

 call( ) : 
  + 语法:
          => 函数名.call()
          => 对象名.函数名.call()
        + 参数:
          => 第一个参数: 要修改的函数内的 this 指向
          => 第二个参数开始: 依次给函数内每一个 参数 赋值
        + 特点:
          => 会立即调用函数
        + 用处:
          => 在立即执行的函数时才使用
          => 事件处理函数, 和 定时器处理函数, 一般不会使用这个方法
// 父类
function Person(name, age) {
  this.name = name
  this.age = age
}
Person.prototype.sayHi = function () { console.log('hello world') }

构造函数概念 :

    + 构造函数也是一个函数

    + 是函数就可以直接调用, 可以不和 new 连用

    + 只是不和 new 连用的时候, 没有自动创建对象的能力了

    // 把 Person 当做一个普通函数调用

    // 因为当做 普通函数 调用, Person 内的 this 指向谁 ? => window

    // Person 函数体内的两个内容添加给 window 了

Person('Jack', 18)

console.log(window.name)   // 打印 => Jack

console.log(window.age)    // 打印 => 18
// 既然 Person 可以当做普通函数调用
// call 方法就是用来调用函数, 并且改变 this 指向的
const obj = { message: '我是自定义的一个 obj 对象' }

// 在调用 Person 的时候
// 使用 call 方法, 把 Person 内的 this 改成 obj
// Person 内书写的两个代码添加的成员, 加在 obj 身上了
Person.call(obj, 'Jack', 20)
console.log(obj)

// 子类
function Student(gender, name, age) {
  this.gender = gender
}
Student.prototype.play = function () { console.log('你好 世界') }
const s = new Student('男')
// 因为 call 方法调用 Person 的时候, 把 Person 内的 this 修改为 s 了
// 所以 name 和 age 成员添加到了 s 身上
Person.call(s, 'Rose', 20)
console.log(s)

 

// 子类
function Student(gender, name, age) {
  this.gender = gender
  // 这里是 Student 的构造函数体内
  // 这里的 this 是 Student 的每个实例对象
  // 利用 call 调用 Person 构造函数, 把 Person 内的 this 修改为 Student 内的 this
  // 把 Person 内的 this 修改为 Student 的每一个实例对象
  // Person 构造函数体内书写成员就都添加到了 Student 的实例身上
  Person.call(this, name, age)
}
Student.prototype.play = function () { console.log('你好 世界') }
const s = new Student('男', '张三', 20)
console.log(s)

 

 扩展 : 第二种写法 


3. 组合继承 : 

组合继承 : 

        + 核心: 把 原型继承 和 call 继承 合并在一起就做组合继承

        + 优点:

          => 父类构造函数体内的属性和原型上的方法都可以继承

          => 继承下来的属性在子类实例对象自己的身上

        + 缺点:

          => 原型上会多一套继承下来的属性名

 原型继承 优缺点 : 

call 继承 优缺点 : 

 

// 父类
function Person(name, age) {
  this.name = name
  this.age = age
}
Person.prototype.sayHi = function () { console.log('hello world') }
// 子类
function Student(gender, name, age) {
  this.gender = gender
  // 实现 call 继承
  // 目的: 为了使用继承把父类构造函数体内的属性放到子类实例自己身上
  Person.call(this, name, age)
}

// 实现原型继承
// 目的: 为了把 Person 的 prototype 内的内容继承下来
// 利用原型继承, 继承父类构造函数身上的方法
Student.prototype = new Person()

Student.prototype.play = function () { console.log('你好 世界') }
// 创建子类的实例
const s = new Student('男', '张三', 20)
console.log(s)

 


4. ES6 的继承语法 :

ES6 的继承方案 :

  + ES6 官方提出了 关键字 来实现继承

ES6 的继承语法:

+ 语法分成两个部分 :

  1. 书写子类的时候, 使用 extends 关键字

    => class 子类名 extends 父类 { ... }

  2. 在子类的 constructor 内书写

    => super( )

  + 注意:

    => 必须要两个条件同时书写

    => 在 constructor 内书写 super 的时候, 必须写在所有 this 的 最前面

( 必须要先继承 :  你必须要继承传统 , 然后再创新  )

    => 父类如果是一个 ES5 的构造函数, 那么可以正常继承

    => 可以继承自 ES6 的 类, 也可以继承自 ES5 的构造函数

// ES5 父类
function Person(name, age) {
  this.name = name
  this.age = age
}
Person.prototype.sayHi = function () { console.log('hello world') }
//ES6 父类
class Person {
  constructor (name, age) {
    this.name = name
    this.age = age
  }

  sayHi () { console.log('hello world') }
}
// ES6 的类的继承
// 创建一个 继承自 Person 的 Student 类
// extends 关键字相当于原型继承
class Student extends Person {
  constructor (gender, name, age) {
    // 你必须要先继承传统 , 然后再创新
    super(name, age)
    // 相当于在调用父类构造函数体, 把 name 和 age 传递过去
    // 相当于把父类的属性继承在自己的身上
    // 相当于 call 继承
    this.gender = gender
  }

  play () { console.log('你好 世界') }
}

const s = new Student('男', 'Jack', 20)
console.log(s)

 

 


简图 : 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值