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

目录
相关文章
|
14天前
|
JavaScript 前端开发
如何在 JavaScript 中使用 __proto__ 实现对象的继承?
使用`__proto__`实现对象继承时需要注意原型链的完整性和属性方法的正确继承,避免出现意外的行为和错误。同时,在现代JavaScript中,也可以使用`class`和`extends`关键字来实现更简洁和直观的继承语法,但理解基于`__proto__`的继承方式对于深入理解JavaScript的面向对象编程和原型链机制仍然具有重要意义。
|
23天前
|
JavaScript 前端开发
Javascript如何实现继承?
【10月更文挑战第24天】JavaScript 中实现继承的方式有很多种,每种方式都有其优缺点和适用场景。在实际开发中,我们需要根据具体的需求和情况选择合适的继承方式,以实现代码的复用和扩展。
|
17天前
|
JavaScript 前端开发
如何使用原型链继承实现 JavaScript 继承?
【10月更文挑战第22天】使用原型链继承可以实现JavaScript中的继承关系,但需要注意其共享性、查找效率以及参数传递等问题,根据具体的应用场景合理地选择和使用继承方式,以满足代码的复用性和可维护性要求。
|
17天前
|
JavaScript 前端开发 开发者
js实现继承怎么实现
【10月更文挑战第26天】每种方式都有其优缺点和适用场景,开发者可以根据具体的需求和项目情况选择合适的继承方式来实现代码的复用和扩展。
30 1
|
5天前
|
JSON JavaScript 前端开发
使用JavaScript和Node.js构建简单的RESTful API
使用JavaScript和Node.js构建简单的RESTful API
|
1月前
|
人工智能 JavaScript 前端开发
使用Node.js模拟执行JavaScript
使用Node.js模拟执行JavaScript
|
1月前
|
消息中间件 JavaScript 前端开发
用于全栈数据流的 JavaScript、Node.js 和 Apache Kafka
用于全栈数据流的 JavaScript、Node.js 和 Apache Kafka
45 1
|
1月前
|
Web App开发 JavaScript 前端开发
Node.js:JavaScript世界的全能工具
Node.js:JavaScript世界的全能工具
|
1月前
|
JSON JavaScript 前端开发
使用JavaScript和Node.js构建简单的RESTful API服务器
【10月更文挑战第12天】使用JavaScript和Node.js构建简单的RESTful API服务器
17 0
|
Web App开发 JavaScript 前端开发