面向对象编程 & 原型 & 原型链

面向对象编程

面向对象编程(Object-Oriented Programming,简称 OOP)是一种编程范式,它的核心思想是将程序中的数据和操作数据的方法组织成对象,从而模拟现实世界的实体和其相互作用。

JavaScript 面向对象编程的优势在于其灵活性和动态性,允许开发者使用原型继承、闭包、函数式编程等特性来实现高度可复用、可扩展的代码,同时能够适应不同的编程风格和需求。

知识导图

问题串联板

  • 什么是面向对象? 为什么要面向对象?
  • 什么是对象? JS 对象的本质是什么?
  • 什么是 constructor 构造函数?什么是类?
  • 什么是 new? new 的工作原理
  • 什么是原型?什么是原型对象?什么是原型链?
  • JS 数据类型的区别? 简单对象与函数对象的区别?
  • 什么是继承? 什么是原型链继承? 什么是盗用构造函数继承(经典继承)?
  • 什么是组合继承? 什么是寄生式组合继承?
  • 如何实现多重继承?

面向对象编程 OOP

  • 对象: 对物体的简单抽象。
  • 在面向对象编程中,程序被组织成对象的集合,这些对象可以包含数据以及操作数据的方法。
  • 对象之间通过消息传递进行通信,每个对象有自己的状态和行为。
  • OOP 有四个主要概念:封装、继承、多态和抽象。
  • 在 JavaScript 中,可以使用类和构造函数+原型来创建对象。React、Vue 等。
  • 模块 + 接口构成面向对象。

面向过程编程 POP

  • 注重程序执行的过程,以及如何按照一定顺序执行一系列的操作。
  • 程序的执行过程被拆分成一系列的步骤,每一步都是对数据进行处理。
  • POP 的重点是算法和函数。
  • 在 JavaScript 中,函数就是一种面向过程的编程范式的实现。
  • 举例: lodash 库、函数式编程

JavaScript 数据类型

在 JavaScript 中数据类型分为两类: 原始值和引用值。引用值又分为简单对象和函数对象。

而函数对象是一等公民。函数对象具有 prototype。

Object → 构造函数 + prototype

const obj = {} 创建了一个空对象实例, 继承自 Object 类。

javascript 复制代码
console.log(Object) // [Function: Object]
const obj = {}
console.log(obj.__proto__) // [Object: null prototype] {}

构造函数 Constructor

构造函数(Constructor)是一种特殊类型的函数,

  1. 在 JavaScript 中用于创建和初始化对象,
  2. 通常采用首字母大写的命名规范,以便与普通函数区分开来。
  3. 构造函数通常与 new 关键字一起使用,用来创建新的对象实例。
  4. 当使用 new 关键字调用构造函数时,会创建一个新的对象实例,并将该实例绑定到构造函数的 this 上。
javascript 复制代码
// 定义一个构造函数
function Person(name, age) {
    this.name = name;
    this.age = age;
    this.greet = function() {
        console.log('Hello, my name is ' + this.name + ' and I am ' + this.age + ' years old.');
    };
}
​
// 使用构造函数创建对象实例
var person1 = new Person('Alice', 30);
var person2 = new Person('Bob', 25);
​
// 调用对象的方法
person1.greet(); // 输出 "Hello, my name is Alice and I am 30 years old."
person2.greet(); // 输出 "Hello, my name is Bob and I am 25 years old."

constructor 构造函数内部逻辑, 它的存在意义是什么? 本质是什么?

构造函数是构造一类对象的模板。构造函数的 prototype 的 constructor 指向构造函数本身。

  1. 每个实例对象被创建时, 会自动拥有一个证明身份的属性 constructor。

    1. 为什么? 因为通过原型 prototype 继承了 父类的属性。
  2. constructor 来源于原型对象, 父类的 prototype, 执行了构造函数的引用。

  3. 构造函数原型的 constructor 指向构造函数本身。

javascript 复制代码
function Course() {}
Course.prototype.CourseName = 'Course Name'
const course = new Course()
​
console.log(Course.prototype.constructor) // [Function: Course]
console.log(course.__proto__.constructor)  // [Function: Course]
console.log(course.constructor)  // [Function: Course]

插一个小问题, 可以阅读完原型链后再回来看

当学习完原型链后, 可以知道

  • proto 原型对象, 父类的 prototype
  • course → proto → Course.prototype → proto → Object.prototype → proto → null

那么问题来了,

为什么上面都是 Course.prototype.proto 、Object.prototype.proto 而 course 没有 。prototype?

因为只有函数对象有原型 prototype , 对象实例没有原型, course 是继承的 Course 的原型上的属性和方法。

只有函数对象(Function objects)才会具有 prototype 属性。这是因为函数对象是 JavaScript 中的一等公民,函数本身也是对象的一种,因此它们可以拥有属性和方法。实例继承了类的属性。

new 的原理 | new 是什么? | 实例化的过程

  1. 在结构上, 创建一个新的对象实例, 它会在内存中分配空间,创建一个空对象,并将该对象的引用返回。
  2. 在属性上 将生成的空对象的原型对象指向了构造函数的 prototype。即 person1。proto => Person.prototype
  3. 在关系上将当前实例对象赋值给内部的 this, 即 this 指向新创建的对象实例。所以构造函数可以通过 this 来操作新对象的属性和方法。
  4. 在生命周期上执行了构造函数的初始化代码。

实例化生成的对象彼此之间有没有直接的联系?

没有。独立传参 & 属性独立 & 方法独立。

构造函数实例化对象有什么(性能)问题么?

如果多个对象实例化时,每个实例都拥有相同的方法,但是这些方法是在构造函数内部定义的,会导致资源浪费。 这是因为每个实例都会在内存中保存一份相同的方法副本,这样会占用额外的内存空间。

ini 复制代码
function Person(name) {
    this.name = name;
​
    this.sayHello = function() {
        console.log('Hello, my name is ' + this.name);
    };
}
​
var person1 = new Person('Alice');
var person2 = new Person('Bob');

在这个例子中,每次使用 new Person 实例化对象时,都会创建一个新的 sayHello 方法。虽然这个方法在每个实例中都是相同的,但是它们是独立的函数实例,每个实例都会保存一份。

为了避免这种资源浪费,可以将方法定义在构造函数的原型上,这样所有实例都可以共享同一个方法实例。

ini 复制代码
function Person(name) {
    this.name = name;
}
​
Person.prototype.sayHello = function() {
    console.log('Hello, my name is ' + this.name);
};
​
var person1 = new Person('Alice');
var person2 = new Person('Bob');

在这个修改后的例子中,sayHello 方法被定义在 Person.prototype 上,所有实例都共享同一个方法实例。这样可以节省内存,避免资源浪费。

当然, 这种解决方案并不是完美的, 也存在许多问题, 下面开始剖析关于"继承"的演进过程。

原型 & 原型对象 & 原型链 & 构造函数扩展链

原型prototype: 函数对象都有原型 prototype 属性。

原型对象 proto: 子类继承父级的原型

  • 实例的原型对象proto指向构造函数的 prototype
  • 构造函数的 prototype 的原型对象(proto) 指向的是 Object 的 prototype
  • Object 的 prototype 的原型对象(proto) 指向的是 null

原型链 & 构造函数扩展链

  • null → Object.prototype → 构造函数。prototype → 实例 => 形成原型链

  • 原型链自底向上查找(继承)顺序

    • 实例。proto → 构造函数。prototype

      • 构造函数。prototype.proto → Object.prototype

        • Object.prototype.proto → null
  • 实例继承了构造函数的原型上的属性和方法; 构造函数继承了 Object 的原型上的属性和方法; Object 的原型再向上查找就到了 null。原型链向上查找。

读图顺序

  1. 先看最左侧原型链

    1. 实例的原型对象指向构造函数的原型; 实例。proto →Object.prototype [实例是最底层一级的, 是一个具体的对象, 不是函数对象了,没有 prototype 了]
    2. 构造函数的原型的原型对象指向对象的原型; 构造函数。prototype.proto →Object.prototype
    3. 对象的原型的原型对象指向的是 null ; Object.prototype.proto →null
  2. 再看构造函数扩展链

    1. 构造函数 prototype 的 constructor 属性 指向 构造函数; 构造函数的原型指向 构造函数的 prototype

    2. 构造函数等价于 Object, 因为 Object 本质上也是一个 constructor 和 prototype。 他们的 constructor 指向 Function

    3. 构造函数和 Object 的原型对象指向 Function.prototype

      1. Functon.prototype 的原型对象 指向 Object.prototype

        1. Object.prototype 的原型对象指向 null
    4. Function.prototype 的 constructor 指向 Function

    5. Function 的原型 => Function.prototype

    6. Function.proto 指向 Function.prototype

构造函数等价于 Object

javascript 复制代码
function Person(name, age) {
  this.name = name
  this.age = age
}
​
console.log(Person instanceof Object) // true

构造函数 和 Object 的 Constructor 都是 Function

构造函数 和 Object 的 proto 是 Function.prototype。

构造函数 和 Object 继承 Function.prototype 上的属性和方法。

Function.prototype 继承 Object.prototype 的属性和方法: Function.prototype。proto === Object.prototype

Function.proto === Function.prototype

Function.__proto__Function 这个函数对象的原型,它指向 Function.prototype。这意味着函数对象本身也是 Function.prototype 的实例。

继承: 盗用构造函数 & 原型链继承 & 组合继承 & 寄生式继承 & 寄生式组合继承

由 问题: 构造函数实例化对象有什么(性能)问题么?引出

javascript 复制代码
function Course(){
    this.name = 'course'
    this.start = function(name){ return name}
}
​
const course1 = new Course()
const course2 = new Course()

构造函数创建对象具有性能上的问题, 如果多个对象实例化时,每个实例都拥有相同的方法,但是这些方法是在构造函数内部定义的,会导致资源浪费。

问题定位: 每次生成都会创建相同方法的副本, 造成资源浪费。

解决方案: 将公共方法挂载到父类的 prototype 上, 后续实例化的子类对象继承父类的方法。这是利用原型链传递的原理。

将属性和方法挂载到原型链上, 子类都可以继承

javascript 复制代码
function Course(){
    this.name = 'course'
    this.
}
// 共享属性 & 共享方法 & 静态属性 & 静态方法
Course.prototype.start = function(name){ return name}
​
const course1 = new Course()
const course2 = new Course()

重写原型的方式: Child 继承 Parent

javascript 复制代码
// 继承 
function Parent(){
    this.name = 'Child'
    this.skin = ['s']
}
Parent.prototype.getName = function(){
    return this.name
}
​
function Child(){}
Child.prototype = new Parent() // 继承构造函数上的属性&方法 以及原型上的属性和方法
Child.prototype.constructor = Child // 将构造函数再指回 Child
​
const Child = new Child()

重写原型的方式有没有缺点?

arduino 复制代码
const Child1 = new Child()
const Child2 = new Child()
​
Child1.skin.push('ss')

此时 Child1 和 Child2 的 skin 都是 ['s', 'ss']

父类的属性一旦赋值给子类的原型, 此时处于子类全部共享的, 继承者的实例间互相篡改影响。

  1. 继承者的实例间互相篡改影响
  2. 实例化时, 无法向父类传参

解决方案: 构造函数继承(经典继承)

javascript 复制代码
function Parent(){
    this.name = 'Child'
    this.skin = ['s']
}
Parent.prototype.getName = function(){
    return this.name
}
​
function Child(arg){
    Parent.call(this, arg) // 解决共享属性问题以及传参问题  | 构造函数内部调用 | 拿不到原型属性
}
​
const Child1 = new Child()
const Child2 = new Child()
Child1.skin.push("ss")
  • 通过 Parent.call(this, arg) 将父类的构造函数在子类的上下文中执行一次。
  • 这样做的结果是,父类中定义的属性和方法会被应用到子类中,因为它们都在相同的上下文中被执行。
  • 通过 call(this, arg),将子类函数的当前实例(this)以及可能的参数 arg 传递到父类的构造函数中。
  • 父类构造函数就能在子类实例上设置属性和进行初始化操作。

过在子类构造函数中调用父类构造函数,实现了继承,即子类实例继承了父类的属性和方法。

问题: 原型链上的方法无法读取继承。

解决方案: 组合继承

javascript 复制代码
let   empltyCash = []
function Parent(){
    this.name = 'Child'
    this.skin = ['s']
    this.cash = empltyCash.push(1)
}
Parent.prototype.getName = function(){
    return this.name
}
​
function Child(arg){
    Parent.call(this, arg) // 解决共享属性问题以及传参问题  | 构造函数内部调用 | 拿不到原型属性
}
​
// 解决拿不到原型链上的方法
Child.prototype = new Parent() // 继承构造函数上的属性&方法 以及原型上的属性和方法
Child.prototype.constructor = Child // 将构造函数再指回 Child
​
const Child1 = new Child()
const Child2 = new Child()
Child1.skin.push("ss")

问题: new 会执行构造函数的代码, 构造函数中的代码被执行了两次。

解决方案: 寄生式组合继承

Object.create 用来创建一个空对象, 解决 new 问题

javascript 复制代码
let   empltyCash = []
function Parent(){
    this.name = 'Child'
    this.skin = ['s']
    this.cash = empltyCash.push(1)
}
Parent.prototype.getName = function(){
    return this.name
}
​
function Child(arg){
    Parent.call(this, arg) // 解决共享属性问题以及传参问题  | 构造函数内部调用 | 拿不到原型属性
}
​
// 寄生
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child

多重继承

Child 想继承 Parent 又想继承 Store

使用组合继承 + 寄生 + Object.assign 合并

javascript 复制代码
let   empltyCash = []
function Parent(){
    this.name = 'Child'
    this.skin = ['s']
    this.cash = empltyCash.push(1)
}
Parent.prototype.getName = function(){
    return this.name
}
​
function Store(){
    this.name = 'steam'
}
Store.prototype.getName = function(){
    return this.name
}
​
function Child(arg){
    Parent.call(this, arg) // 解决共享属性问题以及传参问题  | 构造函数内部调用 | 拿不到原型属性
    Store.call(this, arg)
}
​
​
Child.prototype = Object.create(Parent.prototype)
​
// 多重继承: 优先级, 合并, 后者属性覆盖前者
Object.assign(Child.prototype, Store.Prototype)
​
Child.prototype.constructor = Child

闭包 & 模块 & 封装 & 私有变量

javascript 复制代码
// 创建栈对象的工厂函数
function createStack() {
    const items = []; // 闭包中的私有变量
​
    return {
        push(item) {
            return items.push(item); // 向栈中添加元素
        },
        getItems() {
            return items; // 获取栈中的元素
        }
    };
}
​
// 主函数
function Main() {
    this.createStack = createStack;
}
​
// 实例化主函数对象
const main = new Main();
​
// 调用工厂函数创建栈对象,并获取栈中的元素
main.createStack().getItems();
  1. 闭包(Closure):

    1. 闭包是指函数可以访问其词法作用域之外的变量。在这个例子中,createStack 函数内部的 pushgetItems 函数都可以访问 createStack 函数作用域内的 items 变量,形成了闭包。
    2. 闭包使得 pushgetItems 方法能够持续访问 items 变量,即使 createStack 函数已经执行完毕。
  2. 模块(Module):

    1. 模块是指将代码分割成独立且可复用的单元。在这个例子中,createStack 函数充当了一个简单的模块,它封装了栈数据结构的实现,并提供了一组操作该数据结构的方法。
    2. 外部通过调用 createStack 函数来获取栈对象,并通过对象提供的方法来操作栈数据,而无需关心内部的具体实现细节。
  3. 封装(Encapsulation):

    1. 封装是指将数据和操作数据的方法打包在一起,形成一个独立的单元。在这个例子中,createStack 函数封装了栈数据结构的实现,并提供了一组操作栈数据的方法。
    2. 外部无需了解栈的具体实现细节,只需要使用提供的方法来操作栈数据,从而降低了代码的耦合度和复杂性。
  4. 私有变量(Private Variable):

    1. createStack 函数内部声明了 items 变量,并且该变量没有被直接返回,因此外部无法直接访问到 items 变量,从而实现了私有变量的效果。
    2. 外部只能通过 pushgetItems 方法来间接地操作和获取 items 变量的值,从而确保了数据的安全性和封装性。
相关推荐
崔庆才丨静觅3 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60613 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了4 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅4 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅4 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅4 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment5 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅5 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊5 小时前
jwt介绍
前端
爱敲代码的小鱼5 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax