Js Advance --- 原型链 和 ES5中对象的继承

399 阅读9分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

原型链

从一个对象上获取属性,会触发[[get]]操作

如果对象自身上没有属性,会去对象的[[prototype]](也就是对象的__proto__属性)上去查找对象的属性

如果依旧没有,因为对象的[[prototype]]本质上依旧是一个对象,而对象也会存在__proto__

所以此时会去对象的原型的原型上进行查找,一直找到顶层原型。

如果找到了对象对应的值,那么就结束查找并将对应的值返回,如果到顶层原型依旧没有找到那么就返回undefined

而这个查找的规则就被称之为原型链

Object的原型

整个原型链一定需要有一个顶层原型,也就是查找的尽头,在JS中这个顶层原型就是Obejct对象的原型

const obj = {}
// Object也是有原型的,在这个原型上挂载了toString,valueOf等方法
console.log(obj.__proto__) // => [Object: null prototype] {}

// 而Obejct的原型的原型是不存在的,其值是null
console.log(obj.__proto__.__proto__) // => null

凡是使用字面量来创建对象的时候,对象的原型对象就是Object的原型对象

因为所谓的字面量创建对象的方式在本质上其实就是使用new Object来创建对应的对象的一种语法糖

const obj = {}
console.log(obj.__proto__ === Object.prototype) // => true

通过Object的原型是顶层原型对象我们可以得出,Object是所有类的父类

IFm7EU.png

类 vs 对象

我们使用new关键字去调用一个函数的时候,我们就可以称这个函数为构造函数

从很多面向对象语言过来的开发者,也习惯称之为类,因为类可以帮助我们进行实例的创建

面向对象三大特性

面向对象有三大特性: 封装、继承、多态

特性说明
封装将多个具有关联性的属性和方法封装到一个类的过程就可以称之为封装的过程
继承继承可以帮助我们将重复的代码和逻辑抽取到父类中,子类只需要直接继承过来使用即可
继承不仅仅可以减少重复代码的数量,也是多态前提
多态不同的对象在执行时表现出不同的形态

继承

不使用继承
function Person() {
  this.name = 'Klaus'
  this.age = 23
  this.address = '上海市'
}

Person.prototype.playing = () => console.log('playing')

function Student() {
  this.name = 'Alex'
  this.age = 18
  this.sno = 1810166
}

Student.prototype.playing = () => console.log('playing')

我们可以看到Student类和Person类中有许多的属性和方法,其实是重复的,是可以被抽离出来的

所以我们可以将一些公共的属性和方法抽离到一个单独的类中,然后继承这个类即可,

此时存在公共属性和方法的类就被称之为父类,而继承父类(使用父类上公共属性和方法)的类就被称之为子类

使用原型链实现继承
function Person() {
  this.name = 'Klaus'
  this.age = 23
}

// 此时Student的原型就变为了Person的实例对象
Student.prototype = new Person()
Student.prototype.constructor = Student

// 往修改后的原型链上添加子类特有的方法
Student.prototype.playing = () => console.log('playing')

function Student() {
  this.sno = 1810166
}

const stu = new Student()
console.log(stu.name) // => 'Klaus'
stu.playing() // => playing

但是使用原型链继承有三个很大的弊端:

  1. 可以复用的属性和方法是从父类继承过来的,所以这些属性和方法并不在Student对应的实例对象上,我们通过直接打印对象看不到这个属性
function Person() {
  this.name = 'Klaus'
  this.age = 23
}

Student.prototype = new Person()
Student.prototype.constructor = Student

Student.prototype.playing = () => console.log('playing')

function Student() {
  this.sno = 1810166
}

const stu = new Student()
// 直接打印stu对象,并不存在name和age属性
console.log(stu) // => Student { sno: 1810166 }
  1. 引用类型的属性被所有实例共享
function Person() {
  this.age = 23
  this.friends = []
}

Student.prototype = new Person()
Student.prototype.constructor = Student

Student.prototype.playing = () => console.log('playing')

function Student() {
  this.sno = 1810166
}


const stu1 = new Student()
const stu2 = new Student()

stu1.friends.push('Alex')
console.log(stu2.friends) // => Alex
console.log(stu1.friends === stu2.friends) // => true
console.log(stu1.__proto__ === stu2.__proto__) // => true
function Person() {
  this.age = 23
  this.friends = []
}

Student.prototype = new Person()
Student.prototype.constructor = Student

Student.prototype.playing = () => console.log('playing')

function Student() {
  this.sno = 1810166
}

const stu1 = new Student()
const stu2 = new Student()

// 但是直接赋值,是不会影响stu2.friends属性的值的
// 因为此时相当于在stu1对象上添加了一个名为friends的属性
// 所以在查找stu1的friends属性的时候,会直接在stu1对象上找到对应的值
// 因此就不会去stu1对象的原型上再进行查找操作
stu1.friends = ['Alex']
console.log(stu2.friends) // => []
  1. 创建子类的时候,无法往父类传参
借用构造函数继承

为了解决原型链继承中存在的问题,提出了借用构造函数继承

function Person(name, age) {
  this.name = name
  this.age = age
}

// 原型链继承 + 借用构造函数继承 = 组合继承

// 这里其实是原型链继承
Student.prototype = new Person()
Student.prototype.constructor = Student

Student.prototype.playing = () => console.log('playing')

function Student(name, age, sno) {
  // 此时使用call/apply借助父类的构造方法为子类实例上添加对应的属性
  // 这样在代码复用的同时保证所有的属性其实是添加到子类实例上的
  // 这样就不会出现执行打印属性缺失和引用属性使用冲突的问题
  Person.call(this, name, age) // 这里才是真正的借用构造函数继承
  this.sno = sno
}

const stu1 = new Student('Klaus', 23, 1810166)

但是这么操作依旧是存在如下问题:

无论在什么情况下,都至少会调用两次父类构造函数

  1. 在修正子类原型指向的时候 --- new Person()
  2. 另一次在子类构造函数内部为子类添加对应属性值的时候 --- Person.call(this, name, age)

在修正子类原型指向的时候需要调用一次没有传递任何参数的构造函数,这样就导致了Student原型上依旧是存在值为undefined的name和age属性

而Student的实例对象中已经存在了可以复用的name和age属性,那么Student.prototype上的name和age属性其实是永远不会被访问到的,所以没有存在的必要

寄生组合式继承

为了解决使用构造函数继承存在的问题,主要实现方案是创建一个空对象,而这个空对象的原型指向了父类的原型对象

原型式继承

这种继承方式主要是完成一个对象复用另一个对象中的属性

const parent = {
  name: 'Klaus',
  age: 23
}

function inherit(obj) {
  function Fun() {}
  Fun.prototype = obj
  return new Fun()
}

const child = inherit(parent)

console.log(child.name)
console.log(child.age)

在ES6中提供了两个新的方法,可以实现上述的内容

const parent = {
  name: 'Klaus',
  age: 23
}


function inherit(obj) {
  const child = {}

  // 手动设置原型对象
  // 参数1 --- 子类
  // 参数2 --- 父类
  Object.setPrototypeOf(child, obj)

  return child
}

const child = inherit(parent)

console.log(child.name)
console.log(child.age)

// 输出child对象的原型对象
console.log(Object.getPrototypeOf(child))
const parent = {
  name: 'Klaus',
  age: 23
}

function inherit(obj) {
  // Object.create(obj) --- 返回一个对象,该对象的原型对象为obj
  return Object.create(obj)
}

const child = inherit(parent)

console.log(child.name)
console.log(child.age)

寄生式继承

原型式继承最大的弊端就是,没有绑定复用子类特有的属性和方法,为了可以实现这一点,我们可以将原型式继承使用一个工厂函数进行包裹

const parent = {
  name: 'Klaus',
  age: 23
}

function createChild(obj) {
  const child = Object.create(obj)
  child.sno = 1810166
  child.study = () => console.log('study')
  return child
}

const child = createChild(parent)
console.log(child.sno)
child.study()

寄生组合式继承

寄生组合式继承 = 寄生式继承 + 组合式继承

寄生组合式继承是在ES5中最理想的实现继承的方式

function Parent(name, age) {
  this.name = name
  this.age = age
}

Parent.prototype.eatting = () => console.log('eattings')

function Child(name, age, sno) {
  Parent.call(this, name, age)
  this.sno = sno
}

// 具体实现继承的代码
Child.prototype = Object.create(Parent.prototype)
Object.defineProperty(Child.prototype, 'constructor', {
  configurable: true,
  writable: true,
  value: Child
})

const child = new Child('Klais', 24, 1810166)

console.log(child)
child.eatting()

对象原型方法补充

Object.create

const parent = {
  name: 'Klaus',
  age: 23
}

// Object.create(原型对象,需要添加到返回对象上添加的属性)
// 第二个参数 类型为一个对象 --- 这里设置的值是添加到子类实例上的
// key是属性名
// value是key所对应的属性描述符
const child = Object.create(parent, {
  sno: {
    value: 1810166,
    enumerable: true
  }
})

console.log(parent) // => { name: 'Klaus', age: 23 }
console.log(child) // => { sno: 1810166 }

hasOwnProperty ----- 对象是否有某一个属于自己的属性(不是在原型上的属性)

const parent = {
  name: 'Klaus',
  age: 23
}

const child = Object.create(parent)

child.sno = 1810166

// 对象.hasOwnProperty(属性) ---> 返回值为boolean
console.log(child.hasOwnProperty('name')) // => false
console.log(child.hasOwnProperty('sno')) // => true

<属性> in <对象 > --- 属性是否在对象上或对象的原型链上

const parent = {
  name: 'Klaus',
  age: 23
}

const child = Object.create(parent)

child.sno  = 1810166

// 属性名需要为字符串,或存储了属性名的对象
console.log('sno' in child) // => true
console.log('name' in child) // => true

for...in循环 --- 遍历对象及其原型链上所有enumberable的值为true的属性

const parent = {
  name: 'Klaus',
  age: 23
}

const child = Object.create(parent)

child.sno  = 1810166

for (const key in child) {
  console.log(key)
  /*
    =>
      sno
      name
      age
  */
}

实例对象 instanceof 构造函数 --- 用于检测构造函数的pototype,是否出现在某个实例对象的原型链上

--- 可以用来检测某一个实例对象是不是由某一个构造函数或该构造函数的父类所创建的

function Parent(name, age) {
  this.name = name
  this.age = age
}

function Child(name, age, sno) {
  Parent.call(this, name, age)
  this.sno = sno
}

const child = new Child('Klaus', 23, 1810166)

// 实例 instanceof 函数
console.log(child instanceof Child) // => true Child.prototype在child实例的原型对象上

console.log(child instanceof Parent)
// => true Parent.prototype在child实例的原型对象上, 但是Parent不在child实例的原型对象上

console.log(child instanceof Object)// => true Object.prototype在child实例的原型对象上

原型对象.isPrototypeOf(实例对象) --- 用于检测某个对象,是否出现在某个实例对象的原型链上

function Parent(name, age) {
  this.name = name
  this.age = age
}

function Child(name, age, sno) {
  Parent.call(this, name, age)
  this.sno = sno
}

const child = new Child('Klaus', 23, 1810166)

// 对象.isPrototypeOf(实例对象)
console.log(Child.prototype.isPrototypeOf(child)) // => true

Function 和 Object 之间的关系

Ik675O.png

  1. 在JS中,Object的实例对象是所有对象的父对象
  2. 所有使用字面量方式Object构造函数创建的对象,默认都有一个父类对象,也就是Object对象的实例对象
  3. 所有的函数都是由Function构造函数创建的,而Function本身又是Object的子类
  4. 有一个特例,即Function函数本身也是由Function函数创建的
  5. Object函数本身也是由Function函数创建的
  6. 函数是一个特殊的对象,其即有prototype属性,也有__proto__属性
// Function函数本身也是由Function函数创建的
console.log(Function.__proto__ === Function.prototype) // => true

// Object函数本身也是由Function函数创建的
console.log(Object.__proto__ === Function.prototype) // => true

// Function本身又是Object的子类
console.log(Function instanceof Object) // => true