从0开始学习JavaScript--JavaScript对象封装

JavaScript中的对象封装是一种重要的编程概念,它允许将数据和方法组织成一个独立的单元,实现了数据的保护和抽象。本文将深入探讨JavaScript对象封装的原理、实践和最佳实践。

封装的基础概念

封装是面向对象编程的基础概念之一,它强调将数据和行为组合成一个单一的单元,并通过访问器方法来控制对数据的访问和修改。下面介绍如何使用构造函数和闭包创建封装对象,以及封装的优势。

使用构造函数创建封装对象

构造函数是一种用于创建对象的特殊函数,通过构造函数可以初始化对象的属性。通过将属性和方法添加到构造函数中,我们可以创建具有封装特性的对象。

function Car(make, model) {
  // 私有属性
  let _make = make;
  let _model = model;

  // 公有方法
  this.getMake = function () {
    return _make;
  };

  this.getModel = function () {
    return _model;
  };

  this.displayInfo = function () {
    console.log(`Make: ${_make}, Model: ${_model}`);
  };
}

// 创建封装对象
const myCar = new Car('Toyota', 'Camry');

// 访问封装对象的公有方法
console.log(myCar.getMake()); // 输出: Toyota
console.log(myCar.getModel()); // 输出: Camry

// 调用封装对象的公有方法
myCar.displayInfo(); // 输出: Make: Toyota, Model: Camry

使用闭包创建封装对象

使用闭包也可以实现封装,通过在一个函数内部定义私有变量和方法,然后返回一个包含这些变量和方法的对象。

function createPerson(name, age) {
  // 私有变量
  let _name = name;
  let _age = age;

  // 私有方法
  function increaseAge() {
    _age++;
  }

  // 返回包含私有变量和方法的对象
  return {
    getName: function () {
      return _name;
    },
    getAge: function () {
      return _age;
    },
    celebrateBirthday: function () {
      increaseAge();
      console.log(`Happy Birthday, ${_name}! Now you are ${_age} years old.`);
    },
  };
}

// 创建封装对象
const person = createPerson('John', 25);

// 访问封装对象的公有方法
console.log(person.getName()); // 输出: John
console.log(person.getAge()); // 输出: 25

// 调用封装对象的公有方法
person.celebrateBirthday(); // 输出: Happy Birthday, John! Now you are 26 years old.

封装的优势

  1. 隐藏实现细节: 封装允许将对象的实现细节隐藏起来,只暴露必要的接口,使得对象的内部结构对外部不可见。

  2. 提高代码可维护性: 封装将数据和行为封装在一个单元中,使得代码更易于理解和维护。修改对象的内部实现不会影响外部代码,只需要关注对象提供的接口。

  3. 控制访问权限: 通过封装,可以控制属性的访问权限,使一些属性只能通过特定的方法进行访问或修改,增强了代码的安全性。

  4. 简化接口: 封装使得对象的接口可以被简化,只暴露对外必要的方法,降低了使用者的认知负担。

封装是面向对象编程的基石之一,它使得代码更加模块化、可维护和可复用。通过构造函数和闭包等机制,我们可以在JavaScript中灵活地实现封装。

高级封装:使用ES6 Class

使用ES6 Class创建封装对象

ES6 Class语法简化了对象的创建和继承过程,使得封装对象更加清晰易懂。

class Car {
  constructor(make, model) {
    // 私有属性
    this._make = make;
    this._model = model;
  }

  // Getter方法
  get make() {
    return this._make;
  }

  get model() {
    return this._model;
  }

  // 公有方法
  displayInfo() {
    console.log(`Make: ${this._make}, Model: ${this._model}`);
  }
}

// 创建封装对象
const myCar = new Car('Toyota', 'Camry');

// 使用Getter方法访问私有属性
console.log(myCar.make); // 输出: Toyota
console.log(myCar.model); // 输出: Camry

// 调用封装对象的公有方法
myCar.displayInfo(); // 输出: Make: Toyota, Model: Camry

使用Getter和Setter方法

Getter和Setter方法允许更灵活地控制对属性的访问和修改。

class Person {
  constructor(name, age) {
    // 私有属性
    this._name = name;
    this._age = age;
  }

  // Getter方法
  get name() {
    return this._name;
  }

  get age() {
    return this._age;
  }

  // Setter方法
  set age(newAge) {
    if (newAge > this._age) {
      console.log(`Happy Birthday, ${this._name}!`);
    }
    this._age = newAge;
  }
}

// 创建封装对象
const person = new Person('John', 25);

// 使用Getter方法访问私有属性
console.log(person.name); // 输出: John
console.log(person.age); // 输出: 25

// 使用Setter方法修改私有属性
person.age = 26; // 输出: Happy Birthday, John!
console.log(person.age); // 输出: 26

静态方法与继承

Class还支持静态方法,这些方法属于类而不是类的实例,可以用于创建与类相关的工具函数。

class MathUtils {
  // 静态方法
  static add(a, b) {
    return a + b;
  }

  static subtract(a, b) {
    return a - b;
  }
}

// 使用静态方法
console.log(MathUtils.add(5, 3)); // 输出: 8
console.log(MathUtils.subtract(5, 3)); // 输出: 2

ES6 Class提供了更加清晰和语义化的语法,使得封装对象的创建和维护更加便捷。Getter和Setter方法以及静态方法增加了更多的灵活性和功能性。通过合理运用这些特性,可以构建出更具可读性和可扩展性的高级封装对象。

封装的实际应用

通过实际场景演示,探讨封装在现实项目中的应用。例如,模拟账户系统的封装、使用封装创建可复用组件等。

class BankAccount {
  #balance;

  constructor(initialBalance) {
    this.#balance = initialBalance;
  }

  get balance() {
    return this.#balance;
  }

  deposit(amount) {
    if (amount > 0) {
      this.#balance += amount;
    }
  }

  withdraw(amount) {
    if (amount > 0 && amount <= this.#balance) {
      this.#balance -= amount;
    }
  }
}

const account = new BankAccount(1000);
account.deposit(500);
account.withdraw(200);
console.log(account.balance); // 获取账户余额

封装与设计原则

封装是面向对象编程中的一个核心概念,与设计原则密切相关,能够帮助我们实现更加健壮、可维护的代码。以下是封装与几个设计原则的关系:

1. 单一职责原则(Single Responsibility Principle)

单一职责原则要求一个类应该只有一个引起变化的原因。封装有助于实现单一职责原则,因为封装将类的内部实现细节隐藏起来,使得类的外部只需要关心其提供的接口。这样,当需求变化时,只需修改一个类而不影响其他部分。

2. 开放-封闭原则(Open-Closed Principle)

开放-封闭原则要求软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。通过良好的封装,我们可以将变化隔离在类内部,使得外部代码无需修改即可扩展系统功能。新功能的引入可以通过扩展类而不是修改现有类来实现,从而符合开放-封闭原则。

3. 高内聚低耦合原则

封装有助于实现高内聚低耦合的设计,即将相关的功能放在一个类内部,减少类与类之间的依赖关系。高内聚表示类内部的元素紧密相关,而低耦合表示类与类之间的关联性较弱。这种设计使得系统更加灵活、可维护,并且易于单独测试和修改。

4. 接口隔离原则(Interface Segregation Principle)

封装有助于实现接口隔离原则,即一个类不应该被强迫实现它用不到的接口。通过封装,我们可以将类的接口设计得更加精细,每个类只需要关注与其职责相关的方法,而不用实现不相关的接口。

5. 依赖倒置原则(Dependency Inversion Principle)

封装也与依赖倒置原则相关,该原则要求高层模块不应该依赖于低层模块,而是应该依赖于抽象。通过封装,我们可以定义良好的抽象接口,使得高层模块依赖于抽象而不是具体实现,从而提高系统的灵活性和可维护性。

封装与继承

封装与继承是面向对象编程中两个关键的概念,它们之间的关系影响着代码的可维护性和灵活性。下面深入研究封装与继承的关系以及在继承中如何正确使用封装。

封装的作用

封装是将对象的状态(数据)和行为(方法)包装在一起,形成一个独立的单元。通过封装,可以隐藏对象的内部实现细节,只暴露必要的接口给外部使用。这样可以防止外部直接访问对象的内部数据,提高了安全性和稳定性。

继承与封装

在继承关系中,子类通常会继承父类的属性和方法。封装在继承中的作用主要体现在以下几个方面:

  1. 数据隔离: 封装确保子类不会直接访问父类的私有成员,从而实现数据的良好隔离。子类通过父类提供的公共接口来访问数据,而不需要了解父类的内部实现。

  2. 代码可维护性: 封装使得类的内部结构可以更灵活地调整,而不影响外部代码。当需要修改父类的实现时,只需保持公共接口不变,而不会对继承父类的子类造成影响。

  3. 接口定义: 封装定义了对象与外部的交互接口,这个接口是继承中的重要部分。子类通过继承父类的接口来获取父类的功能,并且可以根据需要进行扩展。

组合和聚合

在某些情况下,组合和聚合可以作为替代继承的方案,从而更灵活地构建对象关系。这些模式利用封装来将对象组合在一起,而不是通过继承来建立层次结构。这种方式可以避免一些继承带来的问题,例如多重继承的复杂性和耦合度的增加。

组合: 将多个对象组合成一个更大的对象。每个对象都是独立的,它们通过组合来实现新的功能。

class Engine {
  start() {
    console.log('Engine started');
  }
}

class Car {
  constructor() {
    this.engine = new Engine();
  }

  start() {
    this.engine.start();
    console.log('Car started');
  }
}

聚合: 将一个对象作为另一个对象的部分,但是两者的生命周期是独立的。

class Wheel {
  roll() {
    console.log('Wheel rolling');
  }
}

class Car {
  constructor() {
    this.wheels = [new Wheel(), new Wheel(), new Wheel(), new Wheel()];
  }

  drive() {
    this.wheels.forEach(wheel => wheel.roll());
    console.log('Car is moving');
  }
}

封装与继承相辅相成,通过良好的封装实践,可以在继承关系中实现数据隔离、提高代码可维护性,并且利用组合和聚合等方式来更灵活地构建对象关系。选择合适的方式取决于具体的场景和需求,通过理解封装与继承的关系,能够更好地设计和组织我们的代码。

封装的异步操作

封装异步操作是在现代JavaScript应用中保持代码清晰、可读性和可维护性的关键。通过使用Promise、async/await等技术,可以更好地组织和管理异步代码。以下是关于如何封装异步操作的一些建议:

使用Promise

Promise是一种用于处理异步操作的对象,它可以表示一个异步操作的最终完成或失败及其结果值。通过使用Promise,可以更清晰地表达异步操作的流程。

function fetchData() {
  return new Promise((resolve, reject) => {
    // 异步操作,例如请求数据
    setTimeout(() => {
      const data = /* 获取的数据 */;
      if (data) {
        resolve(data); // 操作成功
      } else {
        reject('Error fetching data'); // 操作失败
      }
    }, 1000);
  });
}

// 使用Promise
fetchData()
  .then(data => {
    console.log('Data:', data);
  })
  .catch(error => {
    console.error('Error:', error);
  });

使用async/await

async/await是异步操作的一种更现代的处理方式,它基于Promise,并提供了更直观的语法。通过async关键字声明一个函数为异步函数,使用await等待Promise的结果。

async function fetchData() {
  return new Promise((resolve, reject) => {
    // 异步操作,例如请求数据
    setTimeout(() => {
      const data = /* 获取的数据 */;
      if (data) {
        resolve(data); // 操作成功
      } else {
        reject('Error fetching data'); // 操作失败
      }
    }, 1000);
  });
}

// 使用async/await
async function fetchDataWrapper() {
  try {
    const data = await fetchData();
    console.log('Data:', data);
  } catch (error) {
    console.error('Error:', error);
  }
}

fetchDataWrapper();

封装异步操作

为了更好地组织异步代码,可以封装异步操作为一个可复用的函数,将具体的异步细节隐藏在函数内部,提供清晰的接口。

function fetchData() {
  return new Promise((resolve, reject) => {
    // 异步操作,例如请求数据
    setTimeout(() => {
      const data = /* 获取的数据 */;
      if (data) {
        resolve(data); // 操作成功
      } else {
        reject('Error fetching data'); // 操作失败
      }
    }, 1000);
  });
}

async function fetchDataWrapper() {
  try {
    const data = await fetchData();
    console.log('Data:', data);
  } catch (error) {
    console.error('Error:', error);
  }
}

// 调用封装的异步操作
fetchDataWrapper();

通过封装,我们可以在整个应用程序中轻松地复用这个异步操作,提高了代码的可维护性和可读性。

封装的测试与调试

封装的测试与调试是确保代码质量和稳定性的重要步骤。下面将介绍如何编写针对封装对象的单元测试以及如何使用调试工具来追踪封装对象的行为。

单元测试

单元测试是一种验证代码单个模块(函数、类等)是否按预期工作的测试方式。对于封装的异步操作,我们可以使用测试框架(如Jest、Mocha)和断言库(如Chai)来编写测试。

// fetchData.js

function fetchData() {
  return new Promise((resolve, reject) => {
    // 异步操作,例如请求数据
    setTimeout(() => {
      const data = /* 获取的数据 */;
      if (data) {
        resolve(data); // 操作成功
      } else {
        reject('Error fetching data'); // 操作失败
      }
    }, 1000);
  });
}

module.exports = fetchData;
// fetchData.test.js

const fetchData = require('./fetchData');

describe('fetchData', () => {
  test('should resolve with data', async () => {
    const data = await fetchData();
    expect(data).toBeDefined();
  });

  test('should reject with error message if no data', async () => {
    await expect(fetchData()).rejects.toMatch('Error fetching data');
  });
});

在这个例子中,使用Jest测试框架和Chai断言库,编写了两个测试用例,验证了fetchData函数的正确性。这些测试可以自动运行,确保封装的异步操作在不同情况下都能正常工作。

调试

调试是解决代码问题的关键步骤。通过使用调试工具(如Chrome DevTools、Node.js的内置调试器),可以追踪封装对象的执行流程,查看变量的值,以及定位代码中的错误。

// fetchData.js

async function fetchData() {
  try {
    // 异步操作,例如请求数据
    const data = await new Promise((resolve, reject) => {
      setTimeout(() => {
        const responseData = /* 获取的数据 */;
        if (responseData) {
          resolve(responseData); // 操作成功
        } else {
          reject('Error fetching data'); // 操作失败
        }
      }, 1000);
    });

    // 调试点
    debugger;

    return data;
  } catch (error) {
    console.error('Error:', error);
    throw error;
  }
}

// 在调试工具中设置断点,运行代码
fetchData();

在这个例子中,在fetchData函数中使用了debugger关键字,这会在代码执行到这一行时启动调试器。通过在调试器中设置断点,可以逐步执行代码,观察变量的值,并在需要时中断执行以进行进一步的检查。

封装的最佳实践

封装在软件开发中是一项关键的实践,正确的封装可以提高代码的可维护性、可测试性和可读性。以下是一些封装的最佳实践:

1. 选择合适的封装方式

在封装时,应根据具体情况选择合适的封装方式。有时候选择函数封装足够,有时候可能需要使用类或模块。确保选择的封装方式符合代码的结构和功能需求。

// 函数封装
function fetchData(url) {
  // 异步操作
}

// 类封装
class DataLoader {
  constructor(url) {
    // 初始化
  }

  fetchData() {
    // 异步操作
  }
}

2. 保持接口简洁

封装的接口应该简洁明了,不暴露过多的细节给调用者。通过提供清晰的方法和属性,尽量隐藏封装内部的复杂性。

// 不好的封装
function fetchDataAndProcess(url, callback) {
  // 复杂的异步操作
  // ...
  // 处理结果
  callback(result);
}

// 更好的封装
function fetchData(url) {
  // 简单的异步操作
  // ...
  return result;
}

3. 适时更新封装

随着项目的演进和需求的变化,封装可能需要不断更新。及时优化和更新封装,确保它仍然满足项目的需求,并且在新场景下能够更好地工作。

// 初始封装
function fetchData(url) {
  // 初始实现
}

// 更新封装
function fetchData(url, options) {
  // 更新实现
}

4. 灵活运用设计原则

应该根据设计原则(如单一职责原则、开放-封闭原则等)来指导封装的实践。遵循这些原则可以使封装更为健壮、灵活,有助于代码的长期维护。

// 单一职责原则
class DataFetcher {
  fetchData(url) {
    // 异步操作
  }
}

class DataProcessor {
  process(data) {
    // 处理数据
  }
}

总结

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/194536.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

笔记十七、认识React的路由插件react-router-dom和基本使用

react-router 分类 web使用 react-router-dom native使用 react-router-native anywhere&#xff08;使用麻烦&#xff09; react-router 安装 yarn add react-router-dom main.jsx import React from "react"; import ReactDOM from "react-dom/client"…

蓝桥杯第100 题 九宫幻方 DFS 全排列 C++ 解题思维

题目 九宫幻方https://www.lanqiao.cn/problems/100/learning/?page1&first_category_id1&name%E4%B9%9D 思路和解题方法 一 &#xff08;DFS) 首先&#xff0c;定义了一些全局变量和数组。vis数组用于标记已经出现过的数字&#xff0c;a数组用于存储数独的初始状态…

轻松配置PPPoE连接:路由器设置和步骤详解

在家庭网络环境中&#xff0c;我们经常使用PPPoE&#xff08;点对点协议过夜&#xff09;连接来接入宽带互联网。然而&#xff0c;对于一些没有网络专业知识的人来说&#xff0c;配置PPPoE连接可能会有些困难。在本文中&#xff0c;我将详细介绍如何轻松配置PPPoE连接&#xff…

动静分离+多实例实验(nginx+tomcat)

Nginx服务器&#xff1a;192.168.188.14:80 Tomcat服务器1&#xff1a;192.168.188.11:80 Tomcat服务器2&#xff1a;192.168.188.12:8080 192.168.188.12:8081 部署Nginx负载均衡器 关闭防火墙 systemctl stop firewalld setenforce 0 安装依赖 yum -y install pcre-dev…

经典神经网络——AlexNet模型论文详解及代码复现

一、背景 AlexNet是在2012年由Alex Krizhevsky等人提出的&#xff0c;该网络在2012年的ImageNet大赛上夺得了冠军&#xff0c;并且错误率比第二名高了很多。Alexnet共有8层结构&#xff0c;前5层为卷积层&#xff0c;后三层为全连接层。 论文地址&#xff1a;ImageNet Classif…

稳定视频扩散数据管理解密【stable video diffusion】

Stability AI 最近于 2023 年 11 月 21 日推出了其最新模型—稳定视频扩散&#xff08;SVD&#xff09;。视频生成模型的这一突破取决于数据管理的关键作用。 除了模型检查点之外&#xff0c;他们还发布了一份技术报告。 让我们在 Stability AI 的技术报告和一些引人注目的示例…

LeetCode Hot100 394.字符串解码

题目&#xff1a; 给定一个经过编码的字符串&#xff0c;返回它解码后的字符串。 编码规则为: k[encoded_string]&#xff0c;表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。 你可以认为输入字符串总是有效的&#xff1b;输入字符串中没有额外的…

虚幻学习笔记—点击场景3D物体的两种处理方式

一、前言 本文使用的虚幻引擎为5.3.2&#xff0c;两种方式分别为&#xff1a;点击根物体和精准点击目标物体。 二、实现 2.1、玩家控制器中勾选鼠标点击事件&#xff1a;这一步很重要&#xff0c;如图2.1.1所示&#xff1a;在自定义玩家控制器中勾 图2.1.1 选该项&#xff0c…

解密人工智能:线性回归

导言 人工智能&#xff08;AI&#xff09;已经成为当今科技领域的热门话题&#xff0c;其应用领域涵盖了各个行业。线性回归作为人工智能中的一种关键统计学方法&#xff0c;被广泛应用于预测和决策支持系统中。本文将为您详细介绍线性回归在人工智能中的应用原理与方法&#x…

相同JS代码,多次混淆加密能得到不同的结果吗?

一份相同的JavaScript代码&#xff0c;进行多次混淆加密&#xff0c;能得到不同的结果吗&#xff1f; 答案是肯定的&#xff0c;能。 JShaman可以实现这个效果。即&#xff1a;加密结果具有多态性、变化性。 下面实测展示。 来到JShaman网站&#xff0c;用它默认的示例代码…

案例分析-FATfs文件系统移植单片机内存不够问题分析和解决

在通过cubeMX自带的FATfs 文件系统在STM32F103C8T6上进行移植&#xff0c;正式调用后&#xff0c;发现系统报错&#xff0c;出现内存空间不足问题。如下&#xff1a; 更改更大容量的单片机进行编译&#xff0c;通过了 说明刚开始分析空间不够是对的&#xff0c;是flash不够还是…

【vue】浏览器安装vue插件不生效

上一篇&#xff1a;浏览器安装vue插件 https://blog.csdn.net/m0_67930426/article/details/134598104 目录 问题情景 解决办法 问题情景 输入框无内容 解决办法 添加 Vue.config.devtools true; 并且控制台不显示的vue又出现

红米手机如何远程控制荣耀手机?

很多人都知道&#xff0c;华为体系有【畅联】&#xff0c;与华为手机或平板“畅连”通话时&#xff0c;可共享屏幕给对方&#xff0c;一边聊天一边演示&#xff0c;还可在屏幕上涂鸦帮助理解。同样&#xff0c;小米体系有【小米通话】&#xff0c;它的远程协助功能可以帮助朋友…

JAVA配置jdk17 Graa1VM

按照网上内容下载好对应的jdk17版本的Graa1VM&#xff0c; 解压后&#xff0c;修改环境变量中的JAVA_HOME为当前的目录&#xff0c;例如 D:\ruanjian\jdk\gra_jdk17\graalvm-ce-java17-22.3.0 。 然后在命令行中输入java -version的时候&#xff0c; 返回的并不是 Graa1VM 相关…

【Java Spring】SpringBoot 五大类注解

文章目录 Spring Boot 注解简介1、五大类注解的作用2、五大类注解的关系3、通过注解获取对象4、获取Bean对象名规则解析 Spring Boot 注解简介 Spring Boot的核心就是注解。Spring Boot通过各种组合注解&#xff0c;极大地简化了Spring项目的搭建和开发。五大类注解是Spring B…

Selenium 连接到现有的 Google Chrome 示例

python 3.7 selenium 3.14.1 urllib3 1.26.8 Google Chrome 119.0.6045.160 (64位) chromedriver.exe 119.0.6045.105(win32) 1 Google Chrome 添加参数 "--remote-debugging-port9222" 2 测试效果(chromedriver.exe 要和 Google Chrome 版本…

vue2使用ts vue-class-component

目前&#xff0c;对于Vue3来说&#xff0c;TypeScript的支持已经相当成熟&#xff0c;但公司的老项目一直处于迭代和维护无法从v2重构成v3&#xff0c;并且重构的成本也是很大的一个问题&#xff0c;所以记录一下vue2如何去搭配TypeScript。 目录 一、脚手架创建项目 二、vu…

【AUTOSAR-DoIP】通过 DoIP 进行符合 Autosar 的车辆诊断

前言 通信协议 DoIP(互联网协议诊断)可通过以太网、WLAN 和移动通信对系统进行灵活而强大的诊断:无论是车间外的诊断还是车载诊断,以及驾驶时的远程访问。 DoIP 可以通过多种方式使用:现代车辆中的电子控制系统高度互连并执行各种复杂的功能。 Autosar 支持的通信协议 Do…

vue3中shallowReactive与shallowRef

shallowReactive与shallowRef shallowReactive: 只处理了对象内最外层属性的响应式(也就是浅响应式) shallowRef: 只处理了value的响应式, 不进行对象的reactive处理 总结: reactive与ref实现的是深度响应式, 而shallowReactive与shallowRef是浅响应式。 什么时候用浅响应…

08 木谷博客系统RBAC权限设计

这节内容说一下木谷博客系统的权限设计,采用现在主流的权限模型RBAC,对应关系如下: 以上5张表都在mugu_auth_server这个库中 该部分的服务单独定义在user-boot这个模块中。 将角色、权限对应关系加载到Redis 木谷博客系统在认证中心颁发令牌的时候是将用户的角色保存到令牌…