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

面向对象编程

面向对象编程(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 类。

console.log(Object) // [Function: Object]
const obj = {}
console.log(obj.__proto__) // [Object: null prototype] {}

构造函数 Constructor

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

  1. 在 JavaScript 中用于创建和初始化对象,
  2. 通常采用首字母大写的命名规范,以便与普通函数区分开来。
  3. 构造函数通常与 new 关键字一起使用,用来创建新的对象实例。
  4. 当使用 new 关键字调用构造函数时,会创建一个新的对象实例,并将该实例绑定到构造函数的 this 上。
// 定义一个构造函数
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 指向构造函数本身。

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. 在生命周期上执行了构造函数的初始化代码。

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

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

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

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

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 方法。虽然这个方法在每个实例中都是相同的,但是它们是独立的函数实例,每个实例都会保存一份。

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

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

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 的实例。

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

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

function Course(){
    this.name = 'course'
    this.start = function(name){ return name}
}
​
const course1 = new Course()
const course2 = new Course()

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

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

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

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

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

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

// 继承 
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 // 将构造函数再指回 Childconst Child = new Child()

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

const Child1 = new Child()
const Child2 = new Child()
​
Child1.skin.push('ss')

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

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

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

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

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 传递到父类的构造函数中。
  • 父类构造函数就能在子类实例上设置属性和进行初始化操作。

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

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

解决方案: 组合继承

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 // 将构造函数再指回 Childconst Child1 = new Child()
const Child2 = new Child()
Child1.skin.push("ss")

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

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

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

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 合并

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

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

// 创建栈对象的工厂函数
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 变量的值,从而确保了数据的安全性和封装性。

原文链接:https://juejin.cn/post/7355078138667728935 作者:墩墩大魔王丶

(0)
上一篇 2024年4月8日 下午5:02
下一篇 2024年4月8日 下午5:13

相关推荐

发表回复

登录后才能评论