学习笔记之复盘——“封装和继承”

283 阅读6分钟

下述内容为个人学习的复习记录,主要是对一些网络资料的摘抄记录。以及一些自己的总结,可能会有一些错误,如有发现,欢迎指正!!!

学习内容主要来至:github.com/stephentian…

一、封装

  • 将客观的事物封装成抽象的类,隐藏属性和方法实现的细节,仅对外公开接口,这就是封装
  • es5 中没有class的概念,但是我们可能通过很多方法实现 class 的功能

1. class 功能实现方法

(1)构造函数

  • js 中提供了构造函数模式,用来创建对象时初始化对象
  • 构造函数就是普通的函数,具有以下特点
    • 函数名首字母大写(建议)
    • 内部使用 this,this 到时候会指向实例化后的对象
    • 使用 new 生成实例

e.g.

/* 定义构造函数 */
function Person (name, age) {
  this.name = name
  this.age = age
  this.setAge = function (age) {
    this.age = age
  }
}

/* 使用构造函数生成实例 */
const xiaoming = new Person('xiaoming', 18)
console.log(xiaoming.name, xiaoming.age) //xiaoming 18
xiaoming.setAge(20)
console.log(xiaoming.name, xiaoming.age) //xiaoming 20
  • 缺点: 构造函数每创建一个实例,其实例化对象的属性和方法都会在内存中创建一份,而这便会造成内存的浪费
  • 优点: 每个创建的实例都是一个独立的对象,某一对象对自身的属性或方法的更改不会影响其他实例对象

(2)原型方法(prototype)

  • 在对一个对象的属性或方法引用时,如果没有在当前对象中找到时,就会去原型对象中去查找,该过程会一致顺着原型链往上找,直到找到最后一个原型都没找到则会报错或返回 undefined
  • 所有的函数都有一个prototype属性,该属性指向其原型对象,构造函数实例化的对象会继承该原型对象
  • 为了节省内存空间,我们可以将一些对于每个实例都不变的属性和方法放到原型对象当中去,因为同一个构造函数生成的实例对象都拥有一个共同的原型对象,因此这样就可以在不影响功能实现的情况下,节省内存空间

e.g.

function Cat (name) {
  this.name = name
}
Cat.prototype.say = function () {
  console.log('Moe~')
}

let xiaohua = new Cat('xiaohua')
xiaohua.say() //Moe~
  • 缺点: 实例化的对象原型都是同一个,如果某一个实例对象更改自身原型后会导致其他实例对象在原型上的方法一同被更改。

(3).方法

  • 构造函数也是函数,函数又是特殊的对象,因此我们能通通过函数.xxx的方法往函数对象中添加一些方法属性
  • 通过该方法添加的属性或方法,被实例化的对象无法访问,只有类自身能够访问

2. new 的实质

e.g.

function Cat (name) {
  this.name = name
}
Cat.prototype.say = function () {
  console.log('Moe~')
}

let xiaohua = new Cat('xiaohua')
  • new 主要做了以下事情
    • 新建一个对象 xiaohua
    • 构造函数的显示原型(prototype)赋值给实例化对象的隐式原型(__proto__)。即 xiaohua.__proto__ = Cat.prototype
    • 将 this 指向新建的对象
    • 返回对象(注意: 如果构造函数中写了return,且返回的是基本数据类型,那么将会忽略这个 return,创建的对象返回,如果返回的是对象类型,那么就是返回对应的对象)

二、继承

  • 子类可以拥有父类的所有功能,并且能够根据这些功能进行扩展
  • js 中的继承主要有以下几种方法
    • 原型链继承
    • 经典继承
    • 组合继承
    • 寄生继承
    • 寄生组合继承

1. 原型链继承

  • 父类使用原型方法创建类,将子类的原型对象设置为父类的实例对象

e.g.

// 父类构造函数
function Super (value) {
  this.superValue = value
}

// 给父类添加方法
Super.prototype.getSuperValue = function () {
  return this.superValue
}

// 定义子类构造函数
function Sub (value) {
  this.subValue = value
}
// 子类原型链继承父类
Sub.prototype = new Super()

// 子类添加自己的方法
Sub.prototype.getSubValue = function () {
  return this.subValue
}

// 创建子类实例
const instance = new Sub(123)
instance.getSubValue() //123
instance.getSuperValue //undefined
  • 缺点:
    • 由于子类原型对象是父类的一个实例对象,因此父类实例中的所有属性和方法将被子类实例共享
    • 子类在使用构造函数创建实例对象时,无法向父类构造函数传参。 如上述例子,由于无法在 new 子类对象时向父类传参,导致父类中虽然会有 superValue 属性,但是其值为 undefined

2. 经典继承(借由构造函数/伪造对象)

  • 在子类的构造函数中调用父类的构造函数,使用applycall函数,更改父类构造函数的指向,让子类自己生成父类构造函数中定义的属性和方法

e.g.

// 定义父类构造函数
function Super () {
  this.superArray = ['a', 'b', 'c']
}

// 定义子类构造函数
function Sub () {
  // 在子类构造函数中执行父类构造函数
  Super.call(this)
}

const instance = new Sub()
instance.superArray.push('d') //['a','b','c','d']
  • 缺点:
    • 类的方法得在构造函数中声明,无法实现函数复用
    • 父类原型上的方法子类无法访问

3. 组合继承(伪经典继承)

  • 结合原型链继承和构造函数继承两者的优势,使用原型链继承来继承父类原型的属性和方法,使用构造函数继承来继承对实例属性的继承。

e.g.

// 父类构造函数
function Super () {
  this.colors = ['red', 'yellow']
}
// 父类方法
Super.prototype.getColors = function () {
  return this.colors
}

// 定义子类构造函数
function Sub () {
  // 构造函数继承父类属性
  Super.call(this)
}
// 使用原型链继承父类原型对象
Sub.prototype = new Super()

// 修正子类原型的构造函数
Sub.prototype.constructor = Sub

const instance = new Sub()
instance.getColors() //['red','yellow']
  • 组合继承融合了上述两种继承方式的优势,既可以通过子类构造函数向父类构造函数传参,也可以继承父类原型上的方法。因此这是 js 中最常用的继承模式
  • 注意: 上述代码中,Sub.prototype.constructor = Sub是为了将子类原型对象的构造函数设置为子类自身的构造函数,不进行这一步操作也不会影响继承,但是如果再执行new Sub.prototype.constructor()时返回的将是Super对象,为了继承的合理性,这里我们在原型链继承完毕后,将其构造函数修正为父类构造函数
  • 不足: 使用组合继承时父类的构造函数会被调用两次,一次在子类构造函数中调用,一次在子类原型赋值时调用

4. 寄生式继承

  • 创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回这个对象
  • 本质是用已有的对象创建一个新的对象,然后再增强这个对象,得到的对象就是子,原有对象为父

e.g.

function createPerson (original) {
  var clone = Object.create(original) // 通过 Object.create() 函数创建一个新对象
  clone.sayGood = function () {
    // 增强这个对象
    alert('hello world!!!')
  }
  return clone // 返回这个对象
}
  • 缺点: 做不到函数复用

6. 寄生组合继承

  • 在组合继承的基础上,将子类原型继承父类继承的方式替换为寄生继承,即用父类原型创建一个对象副本作为子类的原型对象,这样在就可以减少一次父类构造函数的调用

e.g.

// 父类构造函数
function Super () {
  this.colors = ['red', 'yellow']
}
// 父类方法
Super.prototype.getColors = function () {
  return this.colors
}

// 定义子类构造函数
function Sub () {
  // 构造函数继承父类属性
  Super.call(this)
}
// 使用原型链继承父类原型对象
Sub.prototype = Object.create(Super.prototype)

// 修正子类原型的构造函数
Sub.prototype.constructor = Sub

const instance = new Sub()
instance.getColors() //['red','yellow']