JavaScript 原型链(二)

366 阅读12分钟

JavaScript 原型链(二)

JavaScript 面向对象之继承

面向对象就是书写的一种面向对象的编程方式,就是我们的面向对象式编程

面向对象编程具有的特性含有:

  • 封装 —— 就是实现的是将我们的一些属性或者方法抽象在一个类中的过程,这个就是封装的思想
  • 继承 ——继承的话就是实现的是我们的将一个类实现我们的继承另一个类,这样就提高了代码的复用以及维护性,多态实现的前提
  • 多态 —— 不同的对象在实现的时候展现出来的不同的形态

继承可以帮助我们的实现的好处是什么呐???

  • 可以实现的是帮助我们的进行将重复代码的抽离,以及将我们的代码的具体的实现全部抽象在父类中,子类直接继承过来使用即可
  • 在很多的编程语言中,多态的实现是以继承为前提的,实际上从表面形式来看的话就是我们的多继承吧!!!

使用我们的不使用原型链实现的一些东西:

/**
 * 定义的是人这一个类
 * @param {string} name
 * @param {number} age
 * @param {string} gender
 * @constructor
 */
function Person(name, age, gender) {
    this._name = name
    this._age = age
    this._gender = gender
    console.log(this)
​
    Person.prototype.running = function() {}
    Person.prototype.eating  = function() {}
}
​
/**
 * 定义的是学生类
 * @param {string} name
 * @param {number} age
 * @param {string} gender
 * @constructor
 */
function Student(name, age, gender) {
    this._name = name
    this._age = age
    this._gender = gender
​
    Student.prototype.running = function() {}
    Student.prototype.eating  = function() {}
    Student.prototype.studying = function() {}
}
​
/**
 * 但是我们通过发现,我们这样的书写实际上两个类之间是具有很多的相同的代码的
 * 所以说这就导致了我们的很多的重复代码,这个时候就可以利用继承来实现使用即可
 * 继承可以实现的是将我们的代码实现抽离,然后进行一些更加升华的操作了
 */
console.dir(Person)
console.dir(Student)

这个时候,我们就会发现一点的是我们就具有了很多的重复性的代码,这个时候我们就需要想办法实现我们的类的继承来解决这个问题了

JavaScript 原型链

这个时候就会出现一个新的问题了,我们该如何实现自己的继承了呐???

  • 如果想要实现自己的继承的话,我们就需要使用得到原型链的一些机制了
  • 继承就是使用的是我们的原型链的知识来实现的继承
const obj = {}
// 实际上的话上面的定义我们的对象和通过 Object 构造函数实现底我是基本的方法是一致的‘
let obj01 = new Object()
console.log(Object.getPrototypeOf(obj) === Object.prototype)  // true
console.log(Object.getPrototypeOf(obj01) === Object.prototype)  // true

通过上面的的代码,我们就可以发现一点的是,我们的创建的一个普通的对象的话

他的隐式原型的话实际上就是指向的是我们的 Object 的原型 prototype

但是这个时候,我们如果在一个 Object 的原型 prototype 上面实现查找出来的东西的话,还是没有找到

这个时候实现的查找就是网上一层中继续实现查找,这个就是就是 Object 的隐式原型的指向了 —— null

这个就是我们的普通对象的原型链了

实际上的话,我们的 Array | Object | Function | Number | String 都是我们的内置的一些构造函数

通过**上一节的原型的讲解**,我们是可以发现一点的就是: 对于一个函数而言的话

我们是具有两个原型的: 一个就是显式原型 | 一个就是隐式原型

对于构造函数的角度来进行讲解的时候,就是使用的式我们的显示原型

对于实例化对象而言,具有的原型就是我们的隐式原型了

const obj = {}
​
console.log(Object.getPrototypeOf(Object.getPrototypeOf(obj)))  // null
console.log(obj.__proto__.__proto__)  // null
// 这个就是我们的原型链的尽头了

image-20241113010744799.png

那么这个时候,我们就可以将我们具有的共同的东西实现存放的位置是我们的原型链顶层然后实现获取一些

相同的方法或者属性了,这个就是 ES5 中的实现继承的基本的方法

注意一点的是: 我们的获取对象的原型的话,

  • 包准的方法是通过的是我们的 __proto__
  • 但是包准的方法就是同通过的我们的 Object.getprototypeof(obj) 来实现获取的呐!!!

原型链的形成的即使通过的是我们的: 实例对象的隐式原型 指向的是我们的 构造函数显式原型

通过这样一层一层的,最终就实现了我们的原型链,但是原型链的最后的指向就是我们的 Object 的隐式原型了,

其指向是我们的 null

这里我还拓展两个概念性的东西吧

  • 就是我们的可变数据类型和不可边数据类型

    • 可变数据类型: 即是说我们的变量的值进行了改变,但是变量保存的内存地址未发生变化,这个就是可变数据类型
    • 不可变数据类型: 就是说我们的变量的值进行了改变,但是变量保存的内存地址也进行变化,这个就是不可变数据类型
    • 上面的两点也是用来解释我们的 python 中函数的形参和实参之间的相互是否影响的一种说法之一
    • 但是在我们的 JavaScript 中也是可以用的!!!(语言的交叉性!!!)
  • 在函数的内部如果是传递的是我们的可变的数据类型的话,那么形参的改变会影响实参的变化

  • 在函数内部传递的参数是我们的不可变数据类型的话,那么函数形参的改变不会影响实参的改变

JavaScript 原型链实现继承

function Person(name, age) {
    this.__name = name
    this.__age = age
​
    Person.prototype.running = function() {}
    Person.prototype.eating = function() {}
}
​
​
function Student(name, age) {
    this.__name = name
    this.__age = age
​
    Person.prototype.running = function() {}
    Person.prototype.eating = function() {}
    Person.prototype.studying = function() {}
}
​
/**
 * 我们通过现在的观察可以发现一点的是:
 * 上面的代码实际上的话重复的代码是很多的
 * 这个时候就可以使用我们的继承来实现一些基础的东西
 * 将重复的代码进行提取,然后通过继承来使用这些重复的代码
 */

首先需要注意的一点的是,我们的JavaScript 的原型链中,最顶层的是 Object 的原型

其原型指向是我们的 null,同时这个也是我们的原型链的终点

JavaScript 原型共享机制实现继承

/**
 * 人类
 * @param name
 * @param age
 * @constructor
 */
function Person(name, age) {
    this.__name = name
    this.__age = age
}
Person.prototype.running = function() {
    console.log(this.__name + " is running")
}
Person.prototype.eating = function() {
    console.log(this.__name + " is eating")
}
​
​
/**
 * 学生类
 * @param name
 * @param age
 * @param args
 * @constructor
 */
function Student(name, age, ...args) {
    this.__name = name
    this.__age = age
}
Student.prototype.studying = function() {
    console.log(this.__name + " is studying")
}
​
​
// 先来一个错误的做法,就是使用我们的原型之间的赋值操作
Student.prototype = Person.prototype
console.log(Person.prototype)
​
stu01 = new Student("76433", 18)
stu01.running()  // 76433 is running 

这里我们需要注意一点,当我们的函数没有运行的时候,内部的一些方法是不会执行的,所以说这个时候

给自定义的类(自定义的构造函数)添加方法的时候,就需要在全局作用域中进行添加

但是这种通过原型之间赋值实现继承的效果实际上的话是具有一定的缺陷:

  • 共享原型的危险: 就是我们的 Student 和 Person 类任意两个都是会修改 Person 的原型的
  • 如果含有多个自定义类(自定义构造函数)进行赋值为 Person 类的原型的话,那么后续Person 类的原型就会变得十分的臃肿

image-20241113193619994.png

JavaScript 通过父类实例对象建立继承

/**
 * 人类
 * @param name
 * @param age
 * @constructor
 */
function Person(name, age) {
    this.__name = name
    this.__age = age
}
Person.prototype.running = function() {
    console.log(this.__name + " is running")
}
Person.prototype.eating = function() {
    console.log(this.__name + " is eating")
}
​
​
/**
 * 学生类
 * @param name
 * @param age
 * @param args
 * @constructor
 */
function Student(name, age, ...args) {
    this.__name = name
    this.__age = age
}
Student.prototype.studying = function() {
    console.log(this.__name + " is studying")
}
​
const per = new Person();
Student.prototype = per
​
const stu = new Student("76433", 18)
stu.running()

即是说,通过了我们的原型链的查找规则 + 中间对象实现了两个类的继承关系

这个图就不画了:基本的原型链流程为:(可以自己尝试着画一下!!!)

  • object.getprototypeof(stu) -> Student.prototype -> per
  • Object.getprototypeof(per) -> Person.prototype
  • Object.getprototypeof(Person) -> Object.prototype
  • Object.getprototypeof(Object) -> null

但是这样的方法的话会导致一个缺点就是: 我们的最终的属性可能导致共享

为什么会有这样的效果呐???

  • 首先的一点就是因为我们的 JavaScript 是十分的自由的,一个函数定义了几个参数,但是不代表我们就要传入几个参数
  • 然后就是我们在创建实例对象的时候,可能实现的就是我们的在父类中进行了传递实例参数,但是在子类中没有传递
  • 或者说子类中传递了,父类没传递
  • 更或者说,我们的子父类否传递了实例参数
  • 但是不管是什么情况,都是按照的是原型链进行查找的我们的属性值的
  • 那么这种实现继承的方案的话,就可以能导致一点的是,我们最终获取的属性来自于父类
  • 这中方案就导致了属性的共享机制,这是十分不妥当的,和上面的原型共享大同小异吧

JavaScript 组合借用构造函数 constructor 实现属性继承【constructor steal

/**
 * 人类
 * @param name
 * @param age
 * @constructor
 */
function Person(name, age) {
    this.__name = name
    this.__age = age
}
Person.prototype.running = function() {
    console.log(this.__name + " is running")
}
Person.prototype.eating = function() {
    console.log(this.__name + " is eating")
}
​
​
/**
 * 学生类
 * @param name
 * @param age
 * @param args
 * @constructor
 */
function Student(name, age, ...args) {
    // 开始实现借用构造函数来实现解决属性继承问题
    Person.prototype.constructor.call(this, name, age, ...args)
    
    // console.log(Person === Person.prototype.constructor)  true
    // Person(this, name, age, ...args)  等价于上面的一步哈
}
Student.prototype.studying = function() {
    console.log(this.__name + " is studying")
}
​
const per = new Person()
console.log(per.__name)  // undefined
console.log(per.__age)  // undefinedStudent.prototype = per
​
const stu = new Student("76433", 18)
console.log(stu.__name)  // 76433
console.log(stu.__age)  // 18
stu.running()

通过我们的直接使用父类的构造函数以及使用我们的this 的显式绑定不仅仅可以实现的是我们的

  • 构造函数是什么???

    • 简单的理解,就是我们的构造函数名
    • 从底层来理解就是: Person === Person.prototype.constructor
  • 解决最终的属性继承的问题,还可以实现的是我们的 this 的指向问题

    • this 的指向问题解决的就是属性共享的问题
  • 回顾 new 操作符做的事情有那些:

    • 创建一个空对象
    • 将这个空对象赋值给 this ,同时指向类(构造方法)的显式原型
    • 最终实现调用构造函数
  • 但是我们的组合继承的话,还是具有一定的小问题的

    • 就是我们实现的任意的两种实例对象,都是有两个地方含有这些属性值
    • 一个就是本身的实例化对象
    • 一个就是实现建立子类和父类连接的对象中还具有数据,这个时候就有一定的小问题了,但是问题不大!!!

JavaScript 最终实现继承的方案 —— 寄生式函数

  • 新的解决方案需要解决的地方

    • 通过上面的组合继承的话实际上是已经解决了很多存在的问题的
    • 现在的话,我们需要再进行优化的就是我们的创建子类和父类建立联系的一步
    • 所以说又需要回到最终的原型链进行分析了
  • 原型式继承函数

    • 这种模式的话是通过 道格拉斯.克罗克福德(JSON 数据的创始人)提出的一种模式
    • JavaScript 实现继承需要解决的问题含有: 实现重复的使用另一个对象的属性和方法,同时还可以实现保证自己的状态
  • 这个时候,我们就可以实现通过建立一个空的对象作为真真的中间变量

    • 但是这个中间变量的的原型指向是继承过程中的父类的原型
实现方案一
/**
 * 人类
 * @param name
 * @param age
 * @constructor
 */
function Person(name, age) {
    this.__name = name
    this.__age = age
}
Person.prototype.running = function() {
    console.log(this.__name + " is running")
}
Person.prototype.eating = function() {
    console.log(this.__name + " is eating")
}
​
/**
 * 学生类
 * @param name
 * @param age
 * @param args
 * @constructor
 */
function Student(name, age, ...args) {
    Person.prototype.constructor.call(this, name, age, ...args)
}
​
// 优化创建中间原型实例对象的方法
/**
 * 创建用来生成建立继承的中间的实例对象的方法
 * @param {Function} constructorFn
 * @returns {Function}
 */
function createInheritMiddleObj(constructorFn) {
    function InheritMiddleFn() {}
    Object.setPrototypeOf(InheritMiddleFn, constructorFn.prototype)  // 设置隐式原型
    return objFn
}
​
Student.prototype = createInheritMiddleObj(Person.prototype.constructor)
​
Student.prototype.studying = function() {
    console.log(this.__name + " is studying")
}
​
// console.dir(Student)const obj01 = new Student("76433", 18)
console.dir(obj01)
obj01.studying()

上面的就是实现的是手动的指定我们的原型了

  • 首先创建了一个函数来实现设置一个函数对象的隐式原型 setPrototypeOf
  • 然后这个时候设置构造函数的显式原型的指向就是这个继承的中间函数
  • 但是继承的中间函数的隐式原型指向是父类的显示原型

image-20241113225152740.png

实现方案二
/**
 * 人类
 * @param name
 * @param age
 * @constructor
 */
function Person(name, age) {
    this.__name = name
    this.__age = age
}
Person.prototype.running = function() {
    console.log(this.__name + " is running")
}
Person.prototype.eating = function() {
    console.log(this.__name + " is eating")
}
​
​
​
/**
 * 学生类
 * @param name
 * @param age
 * @param args
 * @constructor
 */
function Student(name, age, ...args) {
    Person.prototype.constructor.call(this, name, age, ...args)
}
​
// 优化创建中间原型实例对象的方法
/**
 * 创建用来生成建立继承的中间的实例对象的方法
 * @param {Function} constructorFn
 * @returns {Object}
 */
function createInheritMiddleObj(constructorFn) {
    return Object.create(constructorFn.prototype)  // 创建一个新对象并且绑定隐式原型
}
​
Student.prototype = createInheritMiddleObj(Person.prototype.constructor)
​
Student.prototype.studying = function() {
    console.log(this.__name + " is studying")
}
console.dir(Student)
​
const obj01 = new Student("76433", 18)
console.dir(obj01)
obj01.studying()

或者说使用这种方法也是可以的

Object.create()

抽离继承函数
/**
 * 人类
 * @param name
 * @param age
 * @constructor
 */
function Person(name, age) {
    this.__name = name
    this.__age = age
}
Person.prototype.running = function() {
    console.log(this.__name + " is running")
}
Person.prototype.eating = function() {
    console.log(this.__name + " is eating")
}
​
​
​
/**
 * 学生类
 * @param name
 * @param age
 * @param args
 * @constructor
 */
function Student(name, age, ...args) {
    Person.prototype.constructor.call(this, name, age, ...args)
}
​
// 优化创建中间原型实例对象的方法
/**
 * 继承的中间转换函数
 * @param SubtypeFn
 * @param SupertypeFn
 */
function inherit(SubtypeFn, SupertypeFn) {
    SubtypeFn.prototype = Object.create(SupertypeFn.prototype)
    Object.defineProperty(SubtypeFn.prototype, 'constructor', {
        value: SubtypeFn.prototype.constructor,
        enumerable: false,
        writable: true,
        configurable: true
    })
}
​
inherit(Student, Person)
const stu = new Student("76433", 18)
​
​
Student.prototype.studying = function() {}
console.dir(stu)
console.dir(Person)

其原型链的组成结构

Student.prototype -> {}.__proto__ -> Person.prototype

stu.__proto__ -> {}.__proto__ -> Person.prototype

这种实现继承的方法,还是可以抽象成我们的一个工具方法的,需要进行使用的时候直接使用即可

JavaScript 原型链总结:

反正看其根本的话,

我们实现的想办法生成新的原型链

同时实现属性和方法可以继承

最后保证原型链的互不干扰性

这个的话,博主个人感觉的话,就像是 C++ | C 中链表的实现一般,思想都是大致相同的