JavaScript设计模式与开发实践(零)基础知识

从多态到开放封闭原则

假设现在有一个第三方登录接口,他可以调用支付宝,微信,Tik Tok的登录接口,按照多态的原则我们可以这么实现

1
2
3
4
5
6
7
8
9
10
11
12
enum LoginType {
WeChat,
TaoBao,
TikTok,
}
function Login(API){
if(API.name === LoginType.WeChat){
// do something
}else if(API.name === LoginType.TaoBao){
// do something
}
}

但是这样当我们要新增一个登录接口时,就需要改动Login函数里面的代码,多增加一个if else,这显然不太优雅,也不利于维护,我们希望能将具体的实现放在函数之外,Login函数内部只负责方法的调用,而外部实现具体的方法,这样新增接口将会变得更易于维护和扩展。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
enum LoginType {
WeChat,
TaoBao,
TikTok,
}

class WeChatHandle{
handler(){
console.log(LoginType.WeChat)
}
}

class TaoBaoHandle{
handler(){
console.log(LoginType.TaoBao)
}
}

class TikTokHandle{
handler(){
console.log(LoginType.TikTok)
}
}

function Login(API){
API.handler()
}

Login(new TikTokHandle())

由于JavaScript的特殊性( 鸭子类型 ),可以使用这种方式来实现多态,而在传统的面向对象语言(静态类型语言)中,通常通过继承的方式来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public abstract class API {
abstract void handler()
}

public class WeChat extends API {
public void handler(){
// do something
}
}

public class TaoBao extends API {
public void handler(){
// do something
}
}

public class Login {
public void login(API api){
api.handler()
}
}

public class Test{
public void main(String args[]){
Login login = new Login();
TaoBao taobao = new TaoBao();
login.login(taobao);
}
}

这种方式在Java中也叫做 向上转型, 即将接受参数的类型定义为需要类以及类的超类(基类),通过继承基类来实现具体的方法。

这也是多态的作用: 通过把过程化的条件分支语句转化为对象的多态性,从而消除这些条件分支语句

封装——封装变化

从设计模式的角度出发,封装在更重要的层面体现为封装变化。

《设计模式》一书曾提到如下文字:

“考虑你的设计中哪些地方可能变化,这种方式与关注会导致重新设计的原因相反。 它不是考虑什么时候会迫使你的设计改变,而是考虑你怎样才能够在不重新设计的情况 下进行改变。这里的关键在于封装发生变化的概念,这是许多设计模式的主题。”
这段文字即是《设计模式》提到的“找到变化并封装之”。

《设计模式》一书中共归纳总结了 23 种设计模式。从意图上区分,这 23种设计模式分别被划分为创建型模式、结构型模式和行为型模式。

拿创建型模式来说,要创建一个对象,是一种抽象行为,而具体创建什么对象则是可以变化的,创建型模式的目的就是 封装创建对象的变化 。而结构型模式 封装的是对象之间的组合关系 。 行为型模式封装的是 对象的行为变化

通过封装变化的方式, 把系统中稳定不变的部分和容易变化的部分隔离开来 ,在系统的演变过程中,我们只需要替换那些容易变化的部分,如果这些部分是已经封装好的,替换起来也相对容易。这可以最大程度地保证程序的稳定性和可扩展性。

原型模式和基于原型模式的JavaScript原型系统

  • 所有的数据都是对象
  • 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它。
  • 对象会记住它的原型
  • 如果对象无法响应某个请求,它会把这个请求委托给它自己的原型

JavaScript中的数据存储

  • 全局变量和被捕获变量(闭包)储存在堆中(全局中的基本类型的值是存在堆中,它的引用地址是存在全局执行上下文的栈内存中)。
    • 被捕获变量

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      function test () {
      let num = 1;
      let string = '前端';
      let bool = true;
      let obj = {
      attr1: 1,
      attr2: '收割机',
      attr3: true,
      attr4: 'something'
      }
      return function log() {
      console.log(num, string, bool, obj);
      }
      }

    • 全局变量

  • 局部变量:如果是基础类型,那栈中储存的是数据本身。如果是对象类型,那栈中存储的是堆中对象的引用(对象本身储存在堆中)。

至于为什么被捕获的变量也存在堆中,个人觉得是由于在一个作用域中被捕获的变量可能不止一个,需要把这些变量聚合起来,类似于对象,因此也被放在了堆中。

AOP编程

AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些 跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Function.prototype.before = function( beforefn ){ 
var __self = this; // 保存原函数的引用
return function(){ // 返回包含了原函数和新函数的"代理"函数
beforefn.apply( this, arguments ); // 执行新函数,且保证 this 不被劫持,新函数接受的参数也会被原封不动地传入原函数,新函数在原函数之前执行
return __self.apply( this, arguments ); // 执行原函数并返回原函数的执行结果,
// 并且保证 this 不被劫持
}
}

Function.prototype.after = function( afterfn ){
var __self = this;
return function(){
var ret = __self.apply( this, arguments );
afterfn.apply( this, arguments );
return ret;
}
};

var func = function(){
console.log( 2 );
};
func = func.before(function(){
console.log( 1 );
}).after(function(){
console.log( 3 );
});
func();

惰性载入函数

1
2
3
4
5
6
7
8
9
10
11
12
var addEvent = function (elem, type, handler) {
if (window.addEventListener) {
addEvent = function (elem, type, handler) {
elem.addEventListener(type, handler, false);
};
} else if (window.attachEvent) {
addEvent = function (elem, type, handler) {
elem.attachEvent("on" + type, handler);
};
}
addEvent(elem, type, handler);
};

JavaScript设计模式与开发实践(零)基础知识
https://jing-jiu.github.io/jing-jiu/2023/01/03/notebooks/JavaScript设计模式与实践/设计模式(零)/
作者
Jing-Jiu
发布于
2023年1月3日
许可协议