面向对象编程
面向对象编程(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)是一种特殊类型的函数,
- 在 JavaScript 中用于创建和初始化对象,
- 通常采用首字母大写的命名规范,以便与普通函数区分开来。
- 构造函数通常与
new
关键字一起使用,用来创建新的对象实例。 - 当使用
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 指向构造函数本身。
-
每个实例对象被创建时, 会自动拥有一个证明身份的属性 constructor。
- 为什么? 因为通过原型 prototype 继承了 父类的属性。
-
constructor 来源于原型对象, 父类的 prototype, 执行了构造函数的引用。
-
构造函数原型的 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 是什么? | 实例化的过程
- 在结构上, 创建一个新的对象实例, 它会在内存中分配空间,创建一个空对象,并将该对象的引用返回。
- 在属性上 将生成的空对象的原型对象指向了构造函数的 prototype。即 person1。proto => Person.prototype
- 在关系上将当前实例对象赋值给内部的 this, 即 this 指向新创建的对象实例。所以构造函数可以通过
this
来操作新对象的属性和方法。 - 在生命周期上执行了构造函数的初始化代码。
实例化生成的对象彼此之间有没有直接的联系?
没有。独立传参 & 属性独立 & 方法独立。
构造函数实例化对象有什么(性能)问题么?
如果多个对象实例化时,每个实例都拥有相同的方法,但是这些方法是在构造函数内部定义的,会导致资源浪费。 这是因为每个实例都会在内存中保存一份相同的方法副本,这样会占用额外的内存空间。
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。原型链向上查找。
读图顺序
-
先看最左侧原型链
- 实例的原型对象指向构造函数的原型; 实例。proto →Object.prototype [实例是最底层一级的, 是一个具体的对象, 不是函数对象了,没有 prototype 了]
- 构造函数的原型的原型对象指向对象的原型; 构造函数。prototype.proto →Object.prototype
- 对象的原型的原型对象指向的是 null ; Object.prototype.proto →null
-
再看构造函数扩展链
-
构造函数 prototype 的 constructor 属性 指向 构造函数; 构造函数的原型指向 构造函数的 prototype
-
构造函数等价于 Object, 因为 Object 本质上也是一个 constructor 和 prototype。 他们的 constructor 指向 Function
-
构造函数和 Object 的原型对象指向 Function.prototype
-
Functon.prototype 的原型对象 指向 Object.prototype
- Object.prototype 的原型对象指向 null
-
-
Function.prototype 的 constructor 指向 Function
-
Function 的原型 => Function.prototype
-
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 // 将构造函数再指回 Child
const Child = new Child()
重写原型的方式有没有缺点?
const Child1 = new Child()
const Child2 = new Child()
Child1.skin.push('ss')
此时 Child1 和 Child2 的 skin 都是 [‘s’, ‘ss’]
父类的属性一旦赋值给子类的原型, 此时处于子类全部共享的, 继承者的实例间互相篡改影响。
- 继承者的实例间互相篡改影响
- 实例化时, 无法向父类传参
解决方案: 构造函数继承(经典继承)
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 // 将构造函数再指回 Child
const 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();
-
闭包(Closure):
- 闭包是指函数可以访问其词法作用域之外的变量。在这个例子中,
createStack
函数内部的push
和getItems
函数都可以访问createStack
函数作用域内的items
变量,形成了闭包。 - 闭包使得
push
和getItems
方法能够持续访问items
变量,即使createStack
函数已经执行完毕。
- 闭包是指函数可以访问其词法作用域之外的变量。在这个例子中,
-
模块(Module):
- 模块是指将代码分割成独立且可复用的单元。在这个例子中,
createStack
函数充当了一个简单的模块,它封装了栈数据结构的实现,并提供了一组操作该数据结构的方法。 - 外部通过调用
createStack
函数来获取栈对象,并通过对象提供的方法来操作栈数据,而无需关心内部的具体实现细节。
- 模块是指将代码分割成独立且可复用的单元。在这个例子中,
-
封装(Encapsulation):
- 封装是指将数据和操作数据的方法打包在一起,形成一个独立的单元。在这个例子中,
createStack
函数封装了栈数据结构的实现,并提供了一组操作栈数据的方法。 - 外部无需了解栈的具体实现细节,只需要使用提供的方法来操作栈数据,从而降低了代码的耦合度和复杂性。
- 封装是指将数据和操作数据的方法打包在一起,形成一个独立的单元。在这个例子中,
-
私有变量(Private Variable):
- 在
createStack
函数内部声明了items
变量,并且该变量没有被直接返回,因此外部无法直接访问到items
变量,从而实现了私有变量的效果。 - 外部只能通过
push
和getItems
方法来间接地操作和获取items
变量的值,从而确保了数据的安全性和封装性。
- 在
原文链接:https://juejin.cn/post/7355078138667728935 作者:墩墩大魔王丶