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

目录
相关文章
|
23天前
|
机器学习/深度学习 人工智能 JavaScript
js和JavaScript
js和JavaScript
21 4
|
JavaScript 前端开发 Java
深入JS面向对象(原型-继承)(一)
深入JS面向对象(原型-继承)
30 0
|
1月前
|
JavaScript 前端开发
js开发:请解释原型继承和类继承的区别。
JavaScript中的原型继承和类继承用于共享对象属性和方法。原型继承利用原型链查找属性,节省内存但不支持私有成员。类继承通过ES6的class和extends实现,支持私有成员但占用更多内存。两者各有优势,适用于不同场景。
18 0
|
23天前
|
JavaScript 前端开发
JavaScript生成的随机数随机字符串JS生成的随机数随机字符串
JavaScript生成的随机数随机字符串JS生成的随机数随机字符串
14 1
|
1月前
|
JavaScript 前端开发
js开发:请解释什么是模块化(modularization),并说明如何在JavaScript中实现模块化。
模块化将复杂系统拆分为松散耦合的模块,提高代码可读性、可维护性、可复用性和可扩展性。JavaScript模块化历经CommonJS(Node.js中常见,使用`require()`和`module.exports`)、AMD(RequireJS,异步加载,`define()`和`require()`)和ES6 Modules(官方标准,`import`和`export`)三个阶段。打包工具如Webpack、Rollup处理兼容性问题,使模块化代码能在各种环境中运行。
|
1月前
|
JavaScript 前端开发
js开发:请解释this关键字在JavaScript中的用法。
JavaScript中的`this`关键字根据执行上下文指向不同对象:全局作用域中指向全局对象(如`window`),普通函数中默认指向全局对象,但作为对象方法时指向该对象。在构造函数中,`this`指向新实例。箭头函数不绑定`this`,而是继承上下文的`this`值。可通过`call`、`apply`、`bind`方法显式改变`this`指向。
10 2
|
1月前
|
设计模式 JavaScript 前端开发
JavaScript中继承的优缺点
JavaScript中继承的优缺点
13 3
|
1月前
|
JavaScript 前端开发
如何在 JavaScript 中实现继承?
如何在 JavaScript 中实现继承?
11 2
|
Web App开发 JavaScript 前端开发
|
2月前
|
JavaScript
Node.js【GET/POST请求、http模块、路由、创建客户端、作为中间层、文件系统模块】(二)-全面详解(学习总结---从入门到深化)
Node.js【GET/POST请求、http模块、路由、创建客户端、作为中间层、文件系统模块】(二)-全面详解(学习总结---从入门到深化)
27 0