构造函数实现封装特性与原型(一、构造函数的封装)

叠甲(是个人学习记录,会有些车轱辘话,有理解错误还请大佬纠正

一、面向过程与面向对象

面向过程:步骤划分,第一步-->第二步-->... 

面向对象(oop):功能划分,灵活、易复用、易维护、易扩展,适合多人合作开发。

面向对象的特性:封装、继承、多态、抽象,基于特性可以设计出低耦合的系统。

1.1 面向对象的特性

封装(Encapsulation):

封装指的是将数据(属性或状态)和操作数据的行为(方法)绑定在一起,并隐藏对象的内部实现细节,只暴露有限的接口给外部使用。 这有助于保护数据的安全性,同时减少系统各部分之间的依赖,使代码更容易维护和扩展。

继承(Inheritance): 

继承允许创建一个新的类(子类),从一个或多个现有的类(父类或基类)那里获取属性和方法。 这可以促进代码复用,简化程序结构,并且支持层次化的分类概念。子类不仅可以继承父类的特性,还可以添加新的属性和方法或者覆盖已有方法。

多态(Polymorphism): 

多态意味着同一个接口可以有不同的实现方式。在OOP中,它通常体现在两个方面:编译时多态(方法重载)和运行时多态(方法覆盖)通过多态,可以在不知道具体类型的情况下调用对象的方法,而实际执行哪个方法取决于运行时的对象类型。

抽象(Abstraction): 

抽象是指提取事物的本质特征,忽略非本质的细节。 在OOP中,抽象可以通过定义抽象类和接口来实现,它们规定了一组方法但不提供具体的实现。具体的方法由子类实现。这样做的好处是可以定义通用的接口,让不同的对象遵循相同的协议,同时保持各自的具体实现。

二、面向对象中的封装特性

2.1 使用构造函数实现封装特性

在JS中可以借助 构造函数 实现面向对象中的 封装 特性。

2.1.1 前置明确条件

如图,明确以下几点:

  1. Person()为构造函数。
  2. person1,person2为实例对象。
  3. name 与SayHello方法为实例属性。
  4. 当使用 new 关键字调用一个函数时实际进行了什么步骤

1 当你使用 new 关键字调用一个函数时,JavaScript 引擎会创建一个新的空对象 {},但这里的空并不是真正意义上的的空,描述它为“空对象”只是为了说明这个对象目前没有自定义的属性,它依旧拥有[[Prototype]]等内部属性。

2 Js将新对象的内部 [[Prototype]] 属性链接到构造函数的 prototype 属性所指向的原型对象。这使得新对象能够继承并访问原型对象上的所有属性和方法。

3 接着,js将将构造函数内部的this关键字绑定到这个新创建的对象上。这意味着,在构造函数内部,this 指向的就是这个新的空对象。随着构造函数代码的执行,这个对象被填充了数据(例如属性),从而成为了一个完整的、具有特定状态的实例对象。

4最后,如果构造函数中没有显式返回一个对象,则默认返回新创建的对象。通常我们会将new表达式的返回值赋值给一个变量或常量,这个变量或常量就是我们所说的实例对象,而构造函数内部的this指向这个实例对象。

5 注意点:

如果构造函数返回的是一个原始类型(如数字、字符串、布尔值等),则这些返回值会被忽略,仍然返回新创建的实例。

2.1.2 构造函数实现封装特性

了解了以上的概念后,我们来看几个构造函数实现封装特性的例子:

1.闭包

使用闭包或私有字段来隐藏内部状态,实现封装特性。

闭包允许一个函数访问其定义时所在的作用域中的变量,即使这个函数在另一个不同的作用域中被执行。允许你在构造函数内部创建私有变量和方法,并且只有通过特定的公共方法才能访问这些私有成员。这种方式有效地隐藏了内部实现细节。

在这个例子中:

count是一个私有变量,外部代码无法直接访问。

increment和getValue是公共方法,允许外部代码以受控的方式访问和修改count。

myCounter不是一个实例对象,myCounter是因为拿到了构造函数Counter返回的一个对象,而这个对象中包含它可以访问的increment和getValue方法。所以他才可以访问到方法。

但由于 increment 和 getValue 方法是在 Counter 函数内部定义的,每次调用 Counter 都会重新创建这些方法,导致每个返回的对象都有这些方法的独立副本。所以会导致内存浪费。

2.使用 ES6 类和私有字段

ES6 引入了类语法,并支持私有字段(以 # 开头)。这使得定义私有成员更加直观和简洁。

在这个例子中:

myCounter是一个实例对象。所以它可以获取到实例方法。

#count是一个私有字段,只有Counter类的方法可以访问它。

increment和getValue是公共方法,允许外部代码以受控的方式访问和修改#count。

在 ES6 类中,任何在类体内定义的方法都会自动成为该类所有实例的公共方法。ES6 类机制会自动将这些方法添加到每个实例的原型链上。这意味着每个通过 new Counter() 创建的实例都可以访问这些方法,并且这些方法可以操作类内部的状态(如私有字段)。

在 ES6 类中,类体内部定义的方法默认会被添加到类的原型链上,这意味着所有实例共享同一个方法副本。因此,使用 ES6 类和私有字段并不会导致每次创建实例对象时都有独立的相同方法副本,从而避免了这方面的内存浪费问题。

3.使用特权方法

特权方法是指在构造函数内部用this.x = function() {}的方式创建的方法,并且可以访问构造函数内部的私有变量和方法的方法。

这些方法是在构造函数执行时被定义的,也就是说:js先将构造函数内部的this关键字绑定到新创建的对象上。再随着构造函数代码的执行,对象被填充了属性或方法。最后把实例对象赋值给一个变量或常量。最后的这个量就是包含填充了属性或方法的实例对象。

所以当我们用实例对象去获取一个构造函数内部使用this.x = function() {}创建的方法时,这个实例对象实际上是在获取它自身就有的方法。因此能够访问构造函数内部创建的私有变量和方法。

在这个例子中:

count是一个私有变量,外部代码无法直接访问。

increment和getValue是特权方法,允许外部代码以受控的方式访问和修改count。

2.2 传统过程式编程与构造函数在实现封装特性上的对比

2.2.1 对比相同点

都可以将变量(数据)和函数(行为或方法)组合在一起。

2.2.1.1 构造函数组合变量与函数
1.使用this关键字

通过this关键字将属性和方法绑定到创建的对象实例上,从而实现组合变量与函数。

2.使用原型链

在构造函数内定义实例属性,在原型对象上定义实例方法。

构造函数用于初始化实例属性,原型用于共享方法。

3.使用 ES6 类语法

ES6 引入了类(class)语法,它提供了一种更简洁的方式来定义构造函数、方法和属性。此外,ES6 还支持静态方法和私有字段(使用 # 符号定义)。它本质上是基于原型的语法糖。

4.使用 getter 和 setter 方法

通过定义 getter 和 setter 来控制对属性的访问。

4.1使用 Object.defineProperty 定义 getter 和 setter

4.2 使用独立的 getter 和 setter 方法

2.2.1.2 传统过程式编程组合变量与函数
1.在全局作用域中定义变量,并编写操作这些变量的函数。

这时的函数依赖这个全局变量,在某种程度上“组合”了变量与函数。但是,它们并没有被明确地组合或封装在一个单元内,这使得它们的关系不那么紧密。

由于所有东西都在全局命名空间中,任何地方都可以访问和修改这些全局变量,这增加了意外改变数据与命名冲突的风险。随着项目规模增大,管理会变得困难。

为了解决这个问题,我们可以采用:立即执行函数表达式 (IIFE)来组合变量与函数

2.立即执行函数表达式 (IIFE)

IIFE 是一个匿名函数,在定义后立即被执行。

它会创建一个新的作用域,防止内部变量污染全局命名空间,同时允许你在局部作用域内定义变量和函数,然后返回一个接口给外部使用.

虽然解决了全局命名空间的问题,但当IIFE 内部调用全局的变量或函数时,没有任何地方显式地说明它依赖于它调用的全局变量或函数。并且,如果你需要使用其他模块的功能,它的做法通常是直接引用全局变量或通过参数传递依赖,容易导致耦合度高,难以维护。

且IIFE 通常是为了解决特定问题而设计的一次性代码块。虽然可以在多个地方复制粘贴 IIFE 来复用某些逻辑,但这会导致代码冗余,且不利于更新和维护。

为了让依赖管理和复用机制再清晰一些,我们可以尝试使用:对象字面量创建来组合变量和函数。

3.对象字面量

创建一个对象字面量,用它来组合相关的函数和变量,这种方式下,data 和 changeData 方法被封装在同一个对象中,形成了一个更加紧密和有组织的单元。这种方法提供了一定程度的封装性,并且可以通过对象属性的形式访问公共方法和变量,在依赖管理上更清晰了一些。但这种情况下,有的属性和方法都是公开的,无法实现真正的私有成员。

为了进一步加强了代码的模块化和封装性,实现真正的私有成员,还有更好的方法:使用模块模式来组合变量和方法。

4.使用模块模式

通过 ES6 引入的 import 和 export 关键字,将变量和函数封装在不同的文件中,并只暴露必要的部分给其他文件。

模块模式结合了IIFE和对象字面量的优点,不仅提供了封装性,还允许定义私有变量和方法。

加强了代码的模块化和封装性。

基于将相关代码封装在不同文件中,使每个文件专注于特定功能,并降低了单个文件的复杂度,更便于独立开发、测试和维护。

通过控制公开内容,未导出的变量和函数对外部不可见,达到了私有成员的优势。这么做更好的防止了全局命名空间被过多变量和函数占用,减少冲突,避免污染。

由于可以自定义导入导出,实现了代码复用的需求,提高了程序的灵活性和扩展性。

明确显示模块间的依赖,便于理解和管理项目结构。这是目前最推荐的做法。

2.2.2  对比不同点

在现代 JavaScript 中,无论是构造函数还是过程式编程,都可以利用 ES6 模块系统提供的 import 和 export 机制来实现模块化开发。

但他们在代码组织、封装性、依赖管理等方面有许多不同。

2.2.2.1 代码组织

构造函数的代码组织:

构造函数具有紧密耦合的数据和行为,构造函数或类将数据(属性)和操作这些数据的方法封装在一起,形成了一个逻辑单元。

借助构造函数创建出来的实例对象是彼此独立互不影响的。

通过构造函数创建多个对象实例后,每个实例都有自己的状态,但共享相同的行为。这种方式非常适合描述现实世界中的实体或概念。

过程式编程的代码组织

函数和数据通常是分开定义的,这可能导致代码分散,尤其是在大型项目中,管理起来更加困难。

虽然可以通过导入导出机制来复用函数,但任何导入这些函数的地方都可以自由地调用它们,这意味着没有内置的机制来限制对这些函数的访问或修改。但这种复用方式通常意味着共享全局状态或通过参数传递状态,这可能增加了复杂度。

2.2.2.2 封装性

构造函数的封装性

构造函数可以利用闭包或 ES6 的私有字段语法来创建私有变量和方法(私有成员),从而保护内部状态不被外部直接访问。

构造函数具有更严格的访问控制,只有通过公开的方法才能修改对象的状态,增强了数据的安全性和一致性。

过程式编程的封装性

传统上,过程式编程缺乏内置的机制来定义私有成员,虽然可以通过命名约定和IIFE来实现某种程度的封装和私有性,但这两种方法都有一些劣势。

1.命名约定:

使用下划线 _ 前缀来表示某个变量或函数应该是“私有的”,即仅供内部使用。这种做法依赖于开发者的自律和团队的约定,而不是语言级别的特性,容易被忽视或绕过。

2.立即执行函数表达式(IIFE)

利用闭包的概念来隐藏内部变量和函数来模拟私有性,但如果需要多个实例,每个实例都需要独立地创建一个 IIFE 的新副本,这可能会导致性能问题(不过可以通过工厂模式等方法解决)。

随着 ES6 的普及,现代 JavaScript 开发更倾向于使用类和私有字段等原生支持的方式来进行封装,但这并不意味着传统的技巧不再有用。根据具体需求选择合适的方法仍然是关键。

2.2.2.3 依赖注入

构造函数的依赖注入:

通过构造函数参数或class语法显式地声明和管理依赖,使代码更具灵活性和可测试性。

过程式编程的依赖注入:

过程式编程通常依赖于全局变量或参数传递,这可能会发生全局状态被意外的修改的情况,并且增加耦合度还不易扩展。导致代码难以维护和扩展。

2.3 构造函数实现封装特性导致的浪费内存问题

使用闭包与特权方法方法实现封装特性时,会出现浪费内存的问题。

以特权方法举例:

每当创建一个新的实例对象时,JavaScript引擎都会在堆中为该实例对象分配新的内存来存储方法代码,所以假设你创建了2个实例对象 并且构造函数内部通过使用 this 定义了一个实例方法,在堆中的这两个实例对象的内存区域就存储了这个方法。那么理所当然的堆中这两块内存区域内的方法逻辑是相同的。也就是说,每个通过该构造函数创建的实例对象都会拥有这些方法的独立副本。

相同的代码被多次复制到内存中,导致了不必要的内存占用 尤其是在创建大量实例的情况下。

我们可以使用:原型链优化、ES6 类机制、工厂函数结合闭包这些优化方法来避免由于特权方法或闭包导致的内存浪费问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值