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关键字来实现类继承。

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

相关推荐
崔庆才丨静觅13 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606114 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了14 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅14 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅15 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅15 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment15 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅15 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊16 小时前
jwt介绍
前端
爱敲代码的小鱼16 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax