JS原型与继承

前言

通过这篇文章我们了解一下JS的原型和实现继承的几种方式,以及他们各自的优缺点。

原型链

基本思想就是通过原型继承多个引用类型的属性和方法,我们来重温一个构造函数、原型和实例的关系。 每个构造函数都有原型对象(prototype),原型有一个属性(constructor)指回构造函数,而实例有一个内部指针(proto)指向原型,如果这个原型是另一个类型的实例呢?那么就意味着这个原型内部也有一个指针指向另一个原型,另一个原型也有一个属性指向另一个构造函数,这样就形成了一个原型链。

例如:c1.prototype = instance2 如果试图引用c1构造的实例instance1中的某个属性p1

  1. 首先会在instance1 内部找一遍
  2. 然后会在instance1.proto(c1.prorotype)中找,而c1.prototype 实际上是instance2,也就是在instance2中找
  3. 如果instance2中还没找到,会继续往上找instance2.proto,直到Object的原型对象。

instance1->instance2->instance2._proto->....->Object.prototype

原型链的问题

  1. 当原型链中包含引用值类型值是,该引用类型值会被所有实例共享。
  2. 子类型在实例化时不能给父类型的构造函数传参。

盗用构造函数(经典继承)

为了解决原型中包含引用值导致的继承问题 基本思路:在子类构造函数中调用父类构造函数。

typescript 复制代码
function father(){
  this.arr = [1,2,3]
}

function son(){
  father.call(this) // 继承father  可以传递参数 用 apply 也可以
}

const instance1 = new son()
instance1.arr.push(4)
console.log(instance1.arr) // 1,2,3,4
const instance2  = new son()
console.log(instance2.arr) // 1,2,3

当然这种继承方法也有问题,方法都在构造函数中定义,因此son函数也不能复用了,而且father 原型中定义的方法也不能访问。

组合继承(伪经典继承)

基本思路:使用原型链实现对原型属性和方法的继承,同时通过借用构造函数来实现对实例属性的继承。

typescript 复制代码
function father(name){
  this.name = name
  this.arr = [1,2,3]
}

father.prototype.sayName = function(){
  console.log(this.name)
}

function son(name,age){
  father.call(this,name) // 继承实例属性   第一次调用father
  this.age = age
}

son.prototype = new father() // 继承父类方法  第二次调用father
son.prototype.sayAge = function(){
  console.log(this.age)
}

let instance1 = new son('tao',5)
instance1.arr.push(4)
console.log(instance1.arr) // 1,2,3,4
instance1.sayName() // tao
instance1.sayAge() // 5

let instance2 = new son('lin',10)
console.log(instance2.arr) // 1,2,3
instance2.sayName() // lin
instance2.sayAge() // 10

这样就解决了原型链和盗用构造函数的缺陷,但是父类构造函数会调用两次。

原型式继承

基本思路:在object() 函数内部,创建一个临时的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时构造函数的实例。

typescript 复制代码
function object(o){
  function F(){}
  F.prototype = o
  return new F()
}

本质上讲,object() 函数是对传入的对象进行了一次浅复制

typescript 复制代码
let p = {
  name:['tao','lin']
}

let a1 = object(p)
a1.name.push('zhange')
let a2 = object(p)
a2.name.push('xu')
console.log(p.name)

把p 传入object() 函数中,然后返回一个新对象,这个新对象的原型指向p,因此新对象的原型中还包含引用类型的属性。 其实现在Object.create()方法已经规范了上面的原型式继承。

寄生式继承

基本思路:创建一个用于封装继承过程的函数,在函数内部以某种方式来增强对象,然后再返回这个对象

typescript 复制代码
function create(o){
  let clone = object(o) // 创建一个新对象,其他方法也可以
  clone.sayName = function(){ // 以某种方式增强这个对象
    console.log('li')
  }
  return clone
}

这个方法中基于 o 返回了一个新对象,新对象不仅具有 o 的属性和方法,还被增强了。 使用寄生式继承来为对象添加函数,也会导致函数难以复用,这一点与盗用构造函数相似

寄生组合式继承

基本思路:不通过调用父类构造函数给子类原型赋值,而是获取父类原型的一个副本。

typescript 复制代码
function extend(son,father){
  let prototype = Object.create(father.prototype) // 创建父类原型的副本
  prototype.constructor = son // 设置新对象的constructor,解决重写原型导致constructor丢失问题
  son.prototype = prototype // 将新创建的对象赋值给子类原型
}


function father(name){
  this.name = name
  this.arr = [1,2,3]
}

father.prototype.sayName = function(){
  console.log(this.name)
}

function son(name,age){
  father.call(this,name)  // 一次调用
  this.age = age
}

extend(son,father) // 

son.prototype.sayAge = function(){
  console.log(this.age)
}

let instance1 = new son('tao',5)
instance1.arr.push(4)
console.log(instance1.arr) // 1,2,3,4
instance1.sayName() // tao
instance1.sayAge() // 5

let instance2 = new son('lin',10)
console.log(instance2.arr) // 1,2,3
instance2.sayName() // lin
instance2.sayAge() // 10

这种继承模式可以是引用类型继承的最佳方式。

class继承

class继承本质上还是基于原型机制的语法糖,具体请看阮一峰老师的es6 class 这一章节。

总结

ES5 有5种实现继承的方式:

  1. 原型继承
  2. 盗用构造函数继承
  3. 组合继承
  4. 原型式继承
  5. 寄生式继承
  6. 寄生组合式继承

ES6 则可以通过class关键字来实现类继承。

如果本文对你有一点帮助,请点一个大大的赞,你的支持就是我创作的动力。

相关推荐
hackeroink9 分钟前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者2 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-2 小时前
验证码机制
前端·后端
燃先生._.3 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖4 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235244 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240255 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar5 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人6 小时前
前端知识补充—CSS
前端·css