V少JS基础班之第九弹:面向对象之封装

一、 前言

第九弹内容是面向对象之封装。 面向对象是JavaScript语言中不可或缺的一部分。它非常非常非常的重要, 学好面向对象会为未来js逆向打下良好的基础,如果不能深刻的理解面向对象的思维,不能熟练掌握面向对象的内容,将会在后面的js逆向中走很多的弯路。 面向对象的学习是一个很大的工程,我无法在一篇文章中详细的讲完,所以决定将面向对象分成三部分来讲;也是面向对象的三大核心,他们分别是面向对象的: 封装继承多态

在开始之前还是有点废话的, 面向对象已经是js逆向学习中比较重要的知识点了, 我会尽心的给大家分享,也是真心的希望大家能通过我的文章学习到一些东西,那还是那句话,如果觉得文章对大家有帮助,请帮忙点赞、收藏,其实之前也只是零零散散的写一些文章,这次能如此有规划的写一篇专栏,大家鼓励的成分占了大半。好的,废话说完,让我们开始今天的内容。

本系列为一周一更,计划历时6个月左右。从JS最基础【变量与作用域】到【异步编程,密码学与混淆】。希望自己能坚持下来, 也希望给准备入行JS逆向的朋友一些帮助, 脸皮厚了一点,明目张胆的要点赞,评论和收藏。也是希望如果本专栏真的对大家有帮助可以点个赞,有建议或者疑惑可以在下方随时问。

先预告一下【V少JS基础班】的全部内容。。
第一个月【变量作用域BOMDOM数据类型操作符
第二个月【函数闭包原型链this
第三个月【面向对象编程、 异步编程、nodejs】
第四个月【密码学、各类加密函数】
第五个月【jsdom、vm2、express】
第六个月【基本请求库、前端知识对接】

==========================================================

二、本节涉及知识点

面向过程&面向对象, 封装:意义、属性私有化

==========================================================

三、重点内容

1、前言

面向对象 & 面向过程
面向过程:

按“步骤/函数”组织程序,数据与处理分开(函数处理传入的数据)。

面向对象:

把数据(属性)和对数据的操作(方法)封在一起,模型化现实世界的“对象”。

os:

好, 到此又到了我们的解说环节。 我们以示例举例看看面向对象和面向过程的区别。 

面向过程:

// 学生信息用单独的变量保存
let student1Name = "Tom";
let student1Age = 18;

let student2Name = "Lucy";
let student2Age = 20;

// 打印学生信息的函数
function printStudent(name, age) {
  console.log(`姓名: ${name}, 年龄: ${age}`);
}

printStudent(student1Name, student1Age);
printStudent(student2Name, student2Age);

面向过程我们的思想更像是数据与工具的关系。 我们有一个打印学生信息的工具, 我们只需要提供海量的数据(name, age),就能通过一直使用(调用)这个工具(方法)去得到我们想要的结果。

这个核心就是将函数作为一个方法, 我们将参数传入这个方法,返回对应的结果。

面向对象:

// 用对象封装学生
const student1 = {
  name: "Tom",
  age: 18,
  print() {
    console.log(`姓名: ${this.name}, 年龄: ${this.age}`);
  }
};

const student2 = {
  name: "Lucy",
  age: 20,
  print() {
    console.log(`姓名: ${this.name}, 年龄: ${this.age}`);
  }
};

student1.print();
student2.print();

而面向对象呢, 我们的思路是将我们的数据和方法看成一个整体, 将本来零散在外层的数据和方法统一的管理起来。 此时封装的核心作用就是, 不用在跨多行在方法中找对应的数据,也不用再一堆代码中找我这个数据传到哪里去了。

os:
我不知道大家有没有get到这个点。 在我们的脚本开发中, 经常会出现的情况, 就是数据和方法混在一起的情况, 我们在程序的开头赋值了变量,在程序中间,结尾分别用两个方法使用了这些数据,就会导致我们无法准确的观察值的改变。 最常见的情况就是:

在一个py文件中, 上面是定义了很多的函数, 然后中间一个
if name == ‘main’:
然后main下面一堆的函数调用和项目逻辑, 一个文件几百几千行。

这样就是典型的面向过程开发, 我们将所有的数据、逻辑和方法都写在一个文件中, 如果是简单的业务逻辑还好。如果真的多批数据、多种方法在同一个逻辑代码中,后续维护的成本就非常的高。
此时,我们就需要将对应的数据和方法剧集在一起。 这就是我们“对象”的基本思路。

到这里,我们就应该明白了上述两段代码的核心区别了吧。 当然,这时肯定就有人说了。这两者看上去代码量没有多少区别,为什么会推崇面向对象编程呢。 我们继续看看下面这段代码:

// 用构造函数 / 类来复用结构
class Student {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  print() {
    console.log(`姓名: ${this.name}, 年龄: ${this.age}`);
  }
}

const student1 = new Student("Tom", 18);
const student2 = new Student("Lucy", 20);

student1.print();
student2.print();

我们将有固定逻辑结构的代码聚集起来,并用将它们捆绑在一起的思维构成一个对象,数据在对象内部由内部操作,这就是我们今天要学习的核心知识点,也是面向对象的核心之一:封装

2、封装

1- 解释

MDN的官方解释:

Encapsulation is the packing of data and functions into one component (for example, a class) and then controlling access to that component to make a “blackbox” out of the object.
Because of this, a user of that class only needs to know its interface (that is, the data and functions exposed outside the class), not the hidden implementation.

翻译:

封装是将数据和函数打包到一个组件中(例如,类),然后控制对该组件的访问,使对象成为一个“黑箱”。
因此,该类的用户只需要了解其接口(即类外部暴露的数据和功能),而不需要了解隐藏的实现。

口语OS:

就是我上述的前言。把同一结构下的数据和方法聚集起来,构成一个对象。我们只用使用这个对象的接口就能实现其方法。这个就叫做封装。

2- 封装的作用
信息隐藏:防止外部直接修改内部状态(降低错误概率)。

实现隔离:内部实现可以随时更改而不影响使用者(接口稳定)。

高内聚、低耦合:模块化,便于维护和测试。

安全性/一致性:通过 getter/setter 或方法验证数据,保证对象处于合法状态。

口语OS:

我们上面已经见过了高内聚,低耦合。剩下的就是我们封装的另一个核心:变量私有化

3- js中构建对象的几种方式

我们在学习变量私有化之前, 得先知道对象的属性和方法是什么

口语OS:

我们口语一点说:对象的属性就是对象中定义的变量、对象的方法,就是对象中的函数

那我们一般如何构建一个对象呢。 也就是定义对象属性和方法的方式。下面就是js中构建对象的几种方式

1. 对象字面量(object literal)

const user = {
  name: "Alice",
  age: 25,
  greet() { console.log("Hi, I'm " + this.name); }
};

对象字面量是 JavaScript 中最简单、最直接的对象构建方式。
它通过一对大括号 {} 将属性和方法直接写入对象中,形成一个最基础的“类的实例”结构。
他的使用场景也非常的基础,基本都是用于测试。 基本在 语法简洁,能快速构建,不重复使用的场景

2. 工厂函数(factory function)

工厂函数是一种用来创建对象的函数,它不需要使用 new 关键字,而是直接返回一个新对象。

function createUser(name, age) {
  return {
    name,
    age,
    greet() { console.log("Hi, I'm " + name); } // 闭包可做私有
  };
}
const u = createUser("Bob", 30);

工厂函数适合轻量级对象创建,当对象方法不多、复用性要求不高时非常实用。
对象结构固定,但不需要原型继承,同时工厂函数又可以动态创建对象。

口语os:

其实在js逆向中, 工厂函数是经常会遇到的。 工厂函数在逆向中出现频繁,它的优势是在工厂函数return对象或者函数的过程中会创建闭包。 
而闭包的核心作用就是创建私有变量,给外部一个可以操作内部变量的方法。 这就是工厂函数的核心, 他起到私有化变量和数据隔离的作用。 
那他的局限性也很明显,这种方式适合那种简单的,对象方法不多的场景。 

3. 构造函数 + prototype

构造函数想必大家已经不陌生了。 通过new来创建对象也是很常见的一个方法
我们标准的构造函数构应该如下,使用prototype将方法存入共享空间以节约每次新建对象时的空间浪费

function Person(name, age) {
    this.name = name;
    this.age = age;
}

// 把方法挂到 prototype 上
Person.prototype.greet = function() {
    console.log(`Hello, my name is ${this.name}`);
};

const alice = new Person("Alice", 25);
const bob = new Person("Bob", 30);

alice.greet(); // Hello, my name is Alice
bob.greet();   // Hello, my name is Bob

console.log(alice.greet === bob.greet); // true,方法共享

口语OS:

其实我个人觉得这才是标准的面向对象封装的意义。

4. Object.create(原型式继承/原型委托)

Object.create(proto) 创建一个新对象,并将新对象的内部 [[Prototype]](即 __ proto__)指向指定的 proto 对象。
这种继承方式被称为 原型式继承 或 原型委托。

const personProto = {
    greet() {
        console.log(`Hello, my name is ${this.name}`);
    }
};

// 原型式继承
const alice = Object.create(personProto);
alice.name = "Alice";
alice.greet(); // Hello, my name is Alice

const bob = Object.create(personProto);
bob.name = "Bob";
bob.greet(); // Hello, my name is Bob

console.log(alice.__proto__ === personProto); // true
console.log(bob.__proto__ === personProto);   // true

口语os:

我们可以直接这么理解, 我们直接创建了 personProto 这个原型对象。 然后我们使用 Object.create的方式,将personProto这个原型对象绑定在alice这个对象上。
alice.__ proto__ 被绑定为 personProto,也就是说 alice 会委托给 personProto 查找方法
这里听不懂的请查看我之前的关于 原型链的文章

5. ES6 class(语法糖)

这个是ES6的新的语法糖,暂未在逆向中遇见太多

class User {
  constructor(name) { this.name = name; }
  greet() { console.log(this.name); }
}
const u = new User("Amy");

口语os:

我个人觉得,在逆向的学习中。前三种会是我们最常见也是需要掌握的方式。 后面两种的使用频率并不高

4- 对象的属性和方法

封装的核心口语化讲就是:把数据和操作数据的方法绑在一起,让它们形成一个整体,同时可以控制谁能访问这些数据。

用三句话总结封装的精髓:
属性 + 方法 聚在一起 → 对象的整体。
控制访问 → 私有化 / 公有化。
提高复用 + 维护性 → 生成多个对象不重复写逻辑。

我们既然将数据和方法绑定在了对象内部,目的就是为了不要让外部胡乱使用对象的数据,否则这直接违背了我们意愿,有些关键数据我们只能对象内部使用,一些不重要的数据可以提供给外部使用。比如:对象名之类。 所以,就有了公有属性和私有属性的区别。

那我们如何定义私有属性呢
1. 约定命名

class C {
  constructor(){ this._secret = 123; }
}

口语os:

我们直接在命名上就传递了我们的意思。 _secret:使用_表示,_secret这个属性是我私有的,请不要在外部随便使用它。但是它并不会真正禁止访问,只是约定俗成,告诉大家「这是内部用的」。如果你一定要在外部使用,你非得这么想,那我也是没有办法

2. 闭包

function Counter() {
  let count = 0; // 私有
  return {
    inc() { count++; },
    get() { return count; }
  };
}
const c = Counter();

口语os:

相比于 约定命名 闭包就有了“强制私有属性”。count 是完全私有的,外部根本访问不到。 如果你在外部试图调用 count,JavaScript 会直接报错。
这样我们就真正把内部数据保护起来了,不会被随意篡改。
如果不太能理解,请查看本系列的闭包章节

5. Symbol(半私有)

const _name = Symbol('name');

class User {
  constructor(name) {
    this[_name] = name;
  }
  getName() { return this[_name]; }
}

const u = new User('Tom');
console.log(u.getName()); // Tom
// console.log(u._name); // undefined

口语os:

为什么说 Symbol 是“半私有”?
这是因为 Symbol 生成的属性键是独一无二的,不像普通字符串属性那么容易被访问(查看本系列数据类型章节)。
你不能通过普通方式 u._name 访问它,需要知道 Symbol 本身才行。
但是它仍然可以被 Object.getOwnPropertySymbols 拿到,所以不是真正严格的私有,算是半私有。

  1. Getter / Setter(控制访问)
class R {
  constructor(w, h) { 
    this._w = w; 
    this._h = h; 
  }
  
  get area() { 
    return this._w * this._h; 
  }
  
  set width(v) { 
    if (v > 0) this._w = v; 
    else throw Error('宽度必须大于0'); 
  }
}

const r = new R(3, 4);
console.log(r.area); // 12
r.width = 5;
console.log(r.area); // 20

口语os:

最后就是 Getter 和 Setter。
它们允许我们 通过公有接口访问私有属性,同时可以在访问时增加逻辑判断。
比如这里我们设置宽度时,不能小于 0,否则就报错。
这样就能保护对象内部状态,同时对外提供可控访问。

5- 总结

其实已经在文章中说过了封装的核心。我们最后在啰嗦一遍

封装的核心口语化讲就是:把数据和操作数据的方法绑在一起,让它们形成一个整体,同时可以控制谁能访问这些数据。

用三句话总结封装的精髓:
属性 + 方法 聚在一起 → 对象的整体。
控制访问 → 私有化 / 公有化。
提高复用 + 维护性 → 生成多个对象不重复写逻辑。

以上就是面向对象之封装的全部内容

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值