前端温故知新之原型和原型链

578 阅读8分钟

72mdr6.png

1、构造函数

new关键字来调用的函数,称为构造函数。用于创建某一类对象(实例化),其首字母大写,用来区分普通函数

// 字面量创建对象为new Object创建对象的语法糖
let obj = {
  name: '名称',
  age: 18
}

let obj = new Object({
  name: '名称',
  age: 18
})

构造函数中的属性和方法称之为成员

实例成员: 通过构造函数内部的this添加的属性,只能通过实例化对象来访问

静态成员: 在构造函数本身上添加的属性,只能通过构造函数来访问(函数也是一个对象)

function Person(name, age) {
  // 实例成员
  this.name = name
  this.age = age
}
//静态成员
Person.sex = '男'

let personObj = new Person('张三', 18) // 实例化
console.log(personObj) // {name: "张三", age: 18}
console.log(personObj.sex) // undefined     实例无法访问静态成员sex属性

console.log(Person.age) // undefined     通过构造函数无法直接访问实例成员
console.log(Person.sex) // '男'       通过构造函数可直接访问静态成员

2、原型

2.1 概念

每一个对象从被创建开始就和另一个对象关联,从另一个对象上继承其属性,另一个对象就是原型(原型对象)

2.2 作用

存储对象共享的属性,来为其它对象提供共享属性

2.3 类型

显式原型(prototype): prototype是函数才具有的属性,这个属性指向一个对象

隐式原型(__ proto__或[[Prototype]]): __proto__是对象的一个属性,默认值是构造函数的 prototype 属性值(即原型对象)

// 构造函数的 prototype(显式原型)和其实例的__proto__(隐式原型)是指向同一个地方(原型对象)
function Person(name, age) {
  this.name = name
  this.age = age
}
let personObj = new Person('张三', 18)
console.log(Person.prototype) // 显式原型,函数的 prototype 属性
console.log(personObj.__proto__) // 隐式原型,对象的 __proto__ 属性
console.log(Person.prototype === personObj.__proto__) // true

2.4 属性

原型对象中默认有一个constructor属性指向构造函数,一旦替换了原型对象,这个constructor属性就需要手动赋值

3、原型链

3.1 概念

实例对象和原型对象通过// proto/*/*属性层层关联,直到内置对象 Object 的原型对象(null)止,从而形成原型链

70jIP0.png

7DWihd.png

3.2 作用

让实例化对象可以通过原型链找到公用的属性或方法(通过原型链实现属性的继承)

3.3 检测

通过instanceof运算符检测构造函数的prototype属性是否出现在某个实例对象的原型链上,即A instanceof B,判断Bprototype是否在A的原型链上

// 函数也是对象,所以也有__proto__属性,函数的构造函数是 Function
function Person(name, age) {
  this.name = name
  this.age = age
}
console.log(Person instanceof Function) // true, Person.__proto__ === Function.prototype
console.log(Person instanceof Object) // true,Function.prototype.__proto__ === Object.prototype

instanceof模拟实现

function myInstanceof(obj, Constructor) {
  let proto = Object.getPrototypeOf(obj) // 获取对象的原型,即 obj.__proto__
  let prototype = Constructor.prototype // 获取构造函数的 prototype 对象
  // 判断构造函数的 prototype 对象是否在对象的原型链上
  while (true) {
    if (!proto) return false // 到达原型链顶层还未找到则返回 false
    if (proto === prototype) return true // 对象实例的隐式原型等于构造函数显示原型则返回 true
    proto = Object.getPrototypeOf(proto)
  }
}

3.4 属性查找

当访问一个对象的属性时,先在对象的本身找,找不到就沿着原型链查找,直到找到为止。如查找到最顶层的原型对象中也没有找到,就结束查找,返回undefined

4、继承

继承就是子类可以使用父类的所有功能,并且对这些功能进行扩展

4.1 原型链继承

将父类的实例作为子类的原型

function Parent(name, age) {
  this.name = name
  this.age = age
  this.hobby = ['read', 'ball']
}
Parent.prototype.getName = function () {
  console.log(this.name)
}
function Child(name) {
  this.name = name
}

// 通过手动修改 Child 构造函数的原型对象指向 Parent 构造函数的实列,Child.prototype = {name: '张三', age: 18}
Child.prototype = new Parent('张三', 18)
let childObj = new Child('李四')
console.info(childObj) // {name: '李四'}
childObj.getName() // '李四',childObj 本身没有 getName 函数,沿着原型链在 Parent 原型中找到 getName 函数执行,this 指向 childObj 实例
console.log(childObj.age) // 18,childObj 本身没有 age 属性,沿着原型链在 Child 原型中找到 age 函数

let childObj2 = new Child('王五')
childObj.hobby.push('walk')
console.log(childObj2.hobby) // ['read', 'ball', 'walk'],原型对象的所有属性被所有实例共享

缺点:

  • 来自原型对象的所有属性被所有实例共享,所以如果修改了原型对象的引用类型数据,所有子类的数据会同步

  • 子类型实例不能给父类型构造函数传参,只能在父类实例化时传值,而不能直接在子类实例化传值给父类

4.2 借用构造函数继承

通过call/apply复制父类的实例属性给子类,即在子类构造函数内部使用call/apply来调用父类构造函数

function Parent(name, age) {
  this.name = name
  this.age = age
  this.getAge = function () {
    console.log(this.age)
  }
}
Parent.prototype.getName = function () {
  console.log(this.name)
}
function Child(name, age, sex) {
  Parent.call(this, name, age) // 执行 Parent 父类构造函数, 复制父类的实例属性给子类
  this.name = '李四' // 覆盖实例对象 name 属性
  this.sex = sex // 实例对象新增 sex 属性
}
// Child 实例化时既可以传参给 Parent 父类型函数也可以传参给 Child 子类型函数
let childObj = new Child('张三', 18, '男')
console.log(childObj instanceof Parent) // false childObj 实例并不是父类的实例,只是子类的实例
console.log(childObj instanceof Child) // true
childObj.getAge() // 18,每次创建一个实例都会生成一个 getAge 函数
childObj.getName() // childObj.getName is not a function,Child 和 Parent 原型没有关联,childObj 实例在原型链中无法找到 getName 函数

缺点:

  • 子类不能继承父类原型上的属性和方法

  • 实例并不是父类的实例,只是子类的实例

  • 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能,即便是相同的函数方法,也会同样的复制一份,而不会共享同一个方法

4.3 组合继承

用原型链实现对原型属性和方法的继承,用借用构造函数技术来实现实例属性的继承

function Parent(name, age) {
  this.name = name
  this.age = age
  this.hobby = ['read', 'ball']
}
Parent.prototype.getName = function () {
  console.log(this.name)
}
function Child(name, age, sex) {
  // 第二次调用 Parent 构造函数, 复制父类的实例属性给子类,子类实例中的属性和方法会覆盖子类原型(父类实例)上的属性和方法
  Parent.call(this, name, age)
  this.sex = sex // 实例对象新增 sex 属性
}
Child.prototype = new Parent() // 第一次调用 Parent 构造函数
Child.prototype.constructor = Child // Child.prototype 重新赋值,原有的 constructor 属性需要手动添加
let childObj = new Child('张三', 18, '男') // Child 实例化时既可以传参给 Parent 父类型函数也可以传参给 Child 子类型函数
console.log(childObj instanceof Parent) // true
console.log(childObj instanceof Child) // true
console.log(childObj.sex) // '男',即可以继承父类实例属性和方法,也能够继承父类原型属性和方法
childObj.getName() // '张三'

let childObj2 = new Child('王五', 20, '男')
childObj.hobby.push('walk') // 引用属性不被所有实例共享
console.log(childObj2.hobby) // ['read', 'ball']

缺点:

  • 父类构造函数会被调用两次(原型链继承和构造函数继承)

  • 生成了两个实例,子类实例中的属性和方法会覆盖子类原型(父类实例)上的属性和方法,增加了不必要的内存

4.4 原型式继承

利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型,本质是对象浅拷贝(不用实例化父类了,直接实例化一个临时副本实现了相同的原型链继承)

// objcetCreate 函数是 Object.create 的模拟实现
function objcetCreate(obj) {
  function F() {}
  F.prototype = obj
  F.prototype.constructor = F
  return new F()
}
let person = {
  name: '张三',
  age: 18,
  hobby: ['read', 'ball']
}

let personObj1 = objcetCreate(person) // 没有创建构造函数的情况下,实现了原型链继承
let personObj2 = objcetCreate(person)

personObj1.hobby.push('walk')
console.log(personObj2.hobby) // ['read', 'ball', 'walk']

缺点:

  • 对象的所有属性被所有实例共享

4.5 寄生式继承

在原型式继承的基础上再封装一层,来增强对象,之后将这个对象返回

let person = {
  name: '张三',
  age: 18,
  hobby: ['read', 'ball']
}

function createAdd(obj) {
  let clone = Object.create(obj)
  // 增强对象的属性方法
  clone.addMethod = function () {
    console.log(obj.name) // '张三'
  }
  return clone
}
let personObj = createAdd(person)
personObj.addMethod()

缺点:

  • 对象的所有属性被所有实例共享

  • 无法实现函数复用,每次创建对象都会重新创建一遍方法

4.6 寄生组合式继承

原型链继承修改成通过Object.create来手动指定原型对象

// 在组合继承需要修改的地方
Child.prototype = new Parent() // 组合继承存在父类构造函数调用 2 次,修改成使用 Object.create 指定原型对象,其它不变
Child.prototype = Object.create(Parent.prototype)

4.7 混入方式继承

一个子类继承多个父类,在寄生组合继承的基础上使用Object.assign来合并不同父类的原型对象

// 在寄生组合式继承上需要修改的地方
function Child(name, age, sex) {
  Parent.call(this, name, age) // 原来的父类
  OtherParent.call(this, sex) // 新增的父类
}
Child.prototype = Object.create(Parent.prototype)
Object.assign(Child.prototype, OtherParent.prototype) // 新增的父类原型对象

4.8 class 中的继承

类似寄生组合继承的语法糖,主要使用extends关键字实现继承

class Parent {
  constructor(name, age) {
    this.name = name
    this.age = age
    this.hobby = ['read', 'ball']
  }
  // 类的所有方法都定义在类的 prototype 属性上面
  getName() {
    console.log(this.name)
  }
}
class Child extends Parent {
  constructor(name, age, sex) {
    super(name, age) // 调用父类的 constructor(name, age)
    this.sex = sex
  }
}
let childObj = new Child('张三', 18, '男')
console.log(childObj instanceof Parent) // true
console.log(childObj instanceof Child) // true
console.log(childObj.sex) // '男'
childObj.getName() // '张三'

let childObj2 = new Child('王五', 20, '男')
childObj.hobby.push('walk') // 引用属性不被所有实例共享
console.log(childObj2.hobby) // ['read', 'ball']

参考