JS查漏补缺——JavaScript的继承实现

简介: JS查漏补缺系列是我在学习JS高级语法时做的笔记,通过实践费曼学习法进一步加深自己对其的理解,也希望别人能通过我的笔记能学习到相关的知识点。这一次我们来了解JavaScript的继承实现

面向对象的特性——继承

面向对象有三大特性:封装、继承、多态
  1. 封装:我们前面将属性和方法封装到一个类中,可以称之为封装的过程;

eg:编写构造函数的过程可以称之为是一个封装的过程

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

  this.eating = function() {
    console.log(this.name + "在吃东西~")
  }
}
  1. 继承:不仅可以重复利用一些代码(对代码的复用),也是多态前提(纯面向对象中);
  2. 多态:不同的对象在执行时表现出不同的形态;

继承

继承可以帮助我们将重复的代码和逻辑抽取到父类中,子类只需要直接继承过来使用即可。

原型链继承(在实践中不会使用,但后续优化的方法是基于它实现的)

// 父类: 公共属性和方法
function Person() {
  this.name = "a"
}

Person.prototype.eating = function() {
  console.log(this.name + " eating~")
}

// 子类: 特有属性和方法
function Student() {
  this.sno = 111
}

Student.prototype = new Person()

Student.prototype.studying = function() {
  console.log(this.name + " studying~")
}

var stu = new Student()

console.log(stu.name)
stu.eating()

当还没有继承前(上面代码去除Student.prototype = new Person()),父类和子类还是两个独立的函数:
Snipaste_2022-09-24_20-15-50.png
当加上Student.prototype = new Person()后,两个函数在内存中发生的变化:
Snipaste_2022-09-24_20-50-18.png

  1. 重写了Student的原型

    1. 让子类原型不再指向子类构造函数
    2. 子类原型上的属性被继承的属性所覆盖

(也是因为是重写了Student的原型,所以Student在原型上的方法不能写在Student.prototype = new Person()之后,不然它会被继承的属性所覆盖)

  1. 其原型指针__proto__指向了父类的原型对象,这样子类就可以沿着原型链访问到父类的方法eating。
  2. 子类原型是父类实例,通过父类构造函数,子类原型继承了父类的属性,最终,子类继承了父类的方法和属性,所以子类原型对象中有属性name = ‘a’

原型链继承的弊端:

  1. 打印stu对象时,继承的属性看不到(因为继承的属性在原型上,而原型上的属性不可枚举)
  2. 父类的实例属性会被子类所有实例共享

    1. 如果该属性是基本类型值时则没有问题
    2. 如果这个对象是一个引用类型(比如数组),那么就会造成问题[ 修改实例1的该属性(比如向数组push一个新值),实例2也会跟着改变。]
  3. 不好将参数传给父类

2.借用构造函数继承

做法:在子类型构造函数的内部调用父类型构造函数,创建子类实例会执行子类的构造函数(含父类的构造函数),也就完成了继承
// 父类: 公共属性和方法
function Person(name, age, friends) {
  // this指代的是stu
  this.name = name
  this.age = age
  this.friends = friends
}
// 子类: 特有属性和方法
function Student(name, age, friends, sno) {
  Person.call(this, name, age, friends) // 将上面的函数当成普通函数进行调用
  this.sno = 111
}

var stu = new Student("a", 18, ["kobe"], 111)
console.log(stu)

Snipaste_2022-09-24_21-46-18.png
解决原型链继承的弊端:

  1. 解决继承的属性打印不出来问题:因为借用构造函数进行继承在子类构造函数中调用父类的构造函数完成继承,与原型对象无关,所以打印出来的属性也是可枚举的
  2. 解决传参的问题 :通过apply()和call()方法给父类传参
  3. 解决共享的问题(实例与实例之间不会相互影响):把 this指向改成了指向新的实例,所以就会把 Person里面的this相关属性和方法赋值到新的实例上,而不是赋值到 Student 原型上面

虽然借用函数继承能解决原型链继承的问题,但它也带来了新的问题:

  1. 子类实例不能访问父类原型对象中的属性和方法,因为借用构造函数进行继承在子类构造函数中调用父类的构造函数完成继承,与原型对象无关,所以它是继承父类构造函数中的属性,而没有继承父类原型上的属性 (有人提出可以通过用Student.prototype = Person.prototype来解决,但是这样在子类原型上添加的方法也会加到父类的原型里面去,违背了面向对象的初衷)
  2. 无法实现函数复用,由于 call 有多个父类实例的副本,性能损耗。

原型式继承

原型式继承是针对对象的一种继承

简单实现一下原型式继承(实现的是对象的继承)

// 目的:让info的原型指向obj对象
var obj = {
  name: "a",
  age: 18
}
var info = Object.create(obj)
console.log(info.__proto__) // {name: "a", age: 18}


// 上面代码的本质如下:
var obj = {
  name: "a",
  age: 18
}
// 原型式继承函数
function createObject(o) {
  var newObj = {}
  Object.setPrototypeOf(newObj, o) // 将传入的o作为newObj的原型
  return newObj
}
var info = createObject(obj)
console.log(info.__proto__) // {name: "a", age: 18}

寄生式继承(存在工厂函数一样的弊端,了解即可)

原型式继承 + 工厂函数
var personObj = {
  running: function() {
    console.log("running")
  }
}

// 工厂函数
function createStudent(name) {
  var stu = Object.create(personObj) // 原型式继承
  stu.name = name
  stu.studying = function() {
    console.log("studying~")
  }
  return stu
}

var stuObj = createStudent("why")
var stuObj1 = createStudent("kobe")
var stuObj2 = createStudent("james")

寄生组合式继承(最终方案)

我们再来回顾一下我们的目的:子类要继承父类的原型且往子类添加属性或方法不影响到父类
// 工具函数: 实现封装(原型式继承 + 指定子类的constructor为自身)
function inheritPrototype(SubType, SuperType) {
  SubType.prototype = Object.create(SuperType.prototype)
  Object.defineProperty(SubType.prototype, "constructor", {
    enumerable: false,
    configurable: true,
    writable: true,
    value: SubType
  })
}
// 要继承的父类
function Person(name, age, friends) {
  this.name = name
  this.age = age
  this.friends = friends
}
Person.prototype.running = function() {
  console.log("running~")
}
Person.prototype.eating = function() {
  console.log("eating~")
}

// 子类
function Student(name, age, friends, sno, score) {
  Person.call(this, name, age, friends)
  this.sno = sno
  this.score = score
}

inheritPrototype(Student, Person) 

Student.prototype.studying = function() {
  console.log("studying~")
}

参考:
https://juejin.cn/post/6934498361475072014
https://www.jianshu.com/p/0045cd01e0be

目录
相关文章
|
4月前
|
资源调度 JavaScript 前端开发
Day.js极简轻易快速2kB的JavaScript库-替代Moment.js
dayjs是一个极简快速2kB的JavaScript库,可以为浏览器处理解析、验证、操作和显示日期和时间,它的设计目标是提供一个简单、快速且功能强大的日期处理工具,同时保持极小的体积(仅 2KB 左右)。
242 24
|
6月前
|
JavaScript 前端开发 算法
JavaScript 中通过Array.sort() 实现多字段排序、排序稳定性、随机排序洗牌算法、优化排序性能,JS中排序算法的使用详解(附实际应用代码)
Array.sort() 是一个功能强大的方法,通过自定义的比较函数,可以处理各种复杂的排序逻辑。无论是简单的数字排序,还是多字段、嵌套对象、分组排序等高级应用,Array.sort() 都能胜任。同时,通过性能优化技巧(如映射排序)和结合其他数组方法(如 reduce),Array.sort() 可以用来实现高效的数据处理逻辑。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
7月前
|
JavaScript 前端开发
JavaWeb JavaScript ③ JS的流程控制和函数
通过本文的详细介绍,您可以深入理解JavaScript的流程控制和函数的使用,进而编写出高效、可维护的代码。
157 32
|
6月前
|
数据采集 JavaScript 前端开发
JavaScript中通过array.filter()实现数组的数据筛选、数据清洗和链式调用,JS中数组过滤器的使用详解(附实际应用代码)
用array.filter()来实现数据筛选、数据清洗和链式调用,相对于for循环更加清晰,语义化强,能显著提升代码的可读性和可维护性。博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
自然语言处理 JavaScript 前端开发
学习javaScript必知必会(2)~js词法分析、介绍一下主流的浏览器的开发者工具(js调试和查看网络请求)
学习javaScript必知必会(2)~js词法分析、介绍一下主流的浏览器的开发者工具(js调试和查看网络请求)
192 0
学习javaScript必知必会(2)~js词法分析、介绍一下主流的浏览器的开发者工具(js调试和查看网络请求)
|
JavaScript 前端开发
Javascript之旅——第七站:说说js的调试
原文:Javascript之旅——第七站:说说js的调试      最近比较吐槽,大家都知道,现在web前端相对几年前来说已经变得很重了,各种js框架,各种面对对象,而且项目多了,就会提取公共模块, 这些模块的UI展示都一样,不一样的就是后台逻辑,举个例子吧,我们做企业差旅的时候,通常都有一个成本中心的js公共模块,客户在预定机票 的时候来填写这个成本中心,而这种成本中心分布在online,offline和app等预定端,这样也是方便后期和客户公司进行月结算。
863 0
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp的客户关系管理系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp的客户关系管理系统附带文章源码部署视频讲解等
232 2
|
10月前
|
JavaScript 前端开发
JavaScript中的原型 保姆级文章一文搞懂
本文详细解析了JavaScript中的原型概念,从构造函数、原型对象、`__proto__`属性、`constructor`属性到原型链,层层递进地解释了JavaScript如何通过原型实现继承机制。适合初学者深入理解JS面向对象编程的核心原理。
153 1
JavaScript中的原型 保姆级文章一文搞懂