写《javascript 的设计模式》的一些总结 Nodejs 相关经验分享

发布于 1周前 作者 phonegap100 来自 nodejs/Nestjs

写《javascript 的设计模式》的一些总结 Nodejs 相关经验分享

最近复刻了一个《 javascript 的设计模式》。也再一次温习了 js 的一些看似不怎么用的知识点,但是在设计模式中又是非常重要的。对于这种容易遗忘的细节但却重要的东西,我觉得来记录这些知识点,以便需要的时候可以看看。

0x1 如何通过基类克隆一个子类对象

首先我们知道 JS 是基于设计模式中的原型模式设计原型链,那么类一定是有共通的原型在里面。那么在子类实例化之后,在基类的 this 也就变成了子类的 this,那么只需要通过新建一个对象,并把当前子类的 prototype 即实例化类的__proto__重现给予到新对象中,由于 this.__proto__是子类的 prototype,那么绑定新对象调用子类的 prototype 的 constructor 即可实现,这也是 new 的过程。

但是在 es6 中有一些变更就是如果使用了 class 关键字,是不可以被 call 调用的,但是还是可以通过 ES6 推出的 Reflect.construct 来达到相同效果,代码如下,下面也有详细说明。

// 这是基类
class Shape {
    // 代码省略...
    clone() {
        /**
        * 如果子类要改成 class 形式,这个方法要改写成下面形式
        * 因为主要是通过 JS 原型链帮助理解原型模式,所以子类不使用 class 形式
        * class 和 function 构造函数的区别是 class 的构造函数增加了只能作为构造函数使用的校验,比如 new
        * return Reflect.construct(
        * this.__proto__.constructor, 
        * [], 
        * this.__proto__.constructor
        * )
        */
       let clone = {};
       // 注意如果此类被继承,this 会变成子类的方法
       // 同时这里使用的是原型的指针,所以比直接创建对象性能损耗更低
       clone.__proto__ = this.__proto__;
       this.__proto__.constructor.call(clone);
       return clone;
    }
 }
// 这是子类
function Rectangle() {
    this.type = "Rectangle";
}
Rectangle.prototype.__proto__ = new Shape();
Rectangle.prototype.draw = function() {
    console.log("I'm a rectangle")
}

0x2 如何实现一个抽象方法

设计模式中有很多抽象方法,但是抽象方法是不能被初始化,只能被继承,那么抽象类要如何实现呢?

其实有两种方法,一种是通过判断 this 是否 instanceof 这个基类,二是用 ES6 的方法使用 new.target,如下:

class AbstractLogger {
    constructor() {
        if(new.target == AbstractLogger) {
            throw new Error('this class must be extends.')
        }
    }
    // 代码省略...
}

0x3 如何实现私有变量

实现私有变量有很多方法,比如 Symbol,但 Symbol 的实现需要通过作用域隔离,其次当访问私有属性的关键字只能返回 undefined,没有错误信息抛出,这是一种非常不好的设计或实践.

那么私有属性还没有推出,如何来更好的实现呢?可以通过数据定义的方式来做这件事情,即 defineProperty。

那么问题又来了,因为私有属性只允许自己调用,子类不能调用,那么如何保证是自己而不是子类或者其它类型呢?那么可以根据当前 this 的__proto__来判断,如果__proto__引用等于自己的 prototype 则为自己。因为如果是子类继承,那么 this 的__proto__等于继承者的 prototype。那么根据这一点,我们可以这样做,如下:

class Meal {
    constructor () {
        const items = [];
        /**
         * 为什么不用 Proxy 而使用 defineProperty
         * 因为 Proxy 虽然实现和 defineProperty 类似的功能
         * 但是在这个场景下,语意上是定义属性,而不是需要代理
         */
        Reflect.defineProperty(this, 'items', {
            get:()=>{
                if(this.__proto__ != Meal.prototype) {
                    throw new Error('items is private!');
                }
                return items;
            }
        })
}
// 省略代码... 

}

0x4 如何实现类的终态方法

在设计模式中,很多方法要实现终态化,即基类的方法不能被子类覆盖。

如何做到不被子类方法覆盖父类,貌似在 JS 中是个难题,但真的是难题吗?

并不是,因为当子类实例化的时候需要调用父类的构造函数,但此时父类的构造函数的 this 就是子类的方法,而 JS 对象构造又是基于原型的,那么如果子类自己实现了方法,那么子类实现的方法必然不等于父类原型中的方法,通过这个方法来实现父类方法的终态化。如下:

class Game {
    constructor() {
        if(this.play!= Game.prototype.play) {
            throw new Error("play mothed is final,can't be modify!");
        }
    }
    // 代码省略...
    play(){
        // 代码省略...
    }
}

结语

通过这些有意思的方法实现通过基类克隆一个子类对象,不可被初始化,私有化变量,终态方法的实现。即感叹 JS 的灵活性,但又对各种行为保有余地,这非常棒。同时在写《 javascript 的设计模式》的时候,发现 JS 本身就是一个设计模式的教科书,是很值得我们学习的。

以上例子出自于《 javascript 的设计模式》


22 回复

学习了赞


不知道楼主的整体是否很好地总结概括了这本书的风格。如果是的话,不推荐读这样的书,窜味儿,难受

你在 0x2 都用上 new.taget 了,在 0x1 用 Object.create(Object.getPrototypeOf(this))不好吗?
0x3 那个根本就不叫私有属性,私有和公有是按 instance 算的,不是 type
0x4 也是胡来,子类完全可以 super()以后再覆盖

最好还是……别误导新人吧


说的不错,那你能分别用更好的方式实现吗?


0x1 我已经说了
0x2 没这么麻烦,直接 throw 就行,子类覆盖后自然不 throw 了,这个 new.target 的判断是没用的
0x3 应该用函数+闭包定义方法来实现私有,当然这会导致 this.fn !== Object.getPrototypeOf(this).fn
0x4 终态方法的目的是能调用但不能覆写,所以只需要在 class 外面定义个函数然后去.call/.apply 就行,不需要变成方法

插眼。听灰大指导


每个人都是自己的实现和缺陷,其它的就不说了,但 0x2 你先试试看看直接 throw 行不行,我试了好像不行

什么样的情况下不行,你是怎么去调用的?


我觉得你说了要点,窜味儿确实存在,我这其实是有点搬 JAVA 那一套。
如果 JSer 要用的话,确实要根据实际情况根据 JS 特色使用。
在修正误导方面,可能还有很多工作要做。

啥东西 我咋都没用过。。。

子类 constructor()中要调用 super() ,不做 new.target 的判断 还是会 throw。

第一个根本不够成问题,对象复制是一个需要对象自己 aware 的事情,什么操作叫做“克隆”是由对象自己定义的,在对象自己定义这个概念之前,这个词无意义。

抽象方法的最简单的实现就是不在 JavaScript 层面实现,正确做法是用 TypeScript 做类型检查。运行时动态判断的开销是无意义的。而且 new 的目标不是说 AbstractBase 不代表它不是 AbstractIntermediate。

最终方法的实现是令实例冻结,并递归冻结类的构造器。然而这样做的意义其实也不大,因为 JavaScript 用户代码是白盒的,你总是可以强行派生。

所以这东西应该叫“抽象类”,而不是“抽象方法”……

既然是用 js,js 的动态扩展性支持直接在类上添加方法就摆明不适合 java 那套,设计模式不应该生搬硬套,灵活运用语言特性才是最关键的,什么抽象类抽象方法还是放在 typescript 上说吧

个人认为基于 js 的原型链闭包和函数作为参数传递能够开创一套区别于其他语言的设计模式,没必要完全照着其他语言的设计模式思路硬套,设计模式应该是完美利用语言特性的,你这里强行隔离原型链设置父类和子类有什么意义

在动态且支持函数式编程的语言中照搬 JAVA 那一套,真的是自费武功还看着费劲。
比如单例模式一个自执行函数就完事;迭代器还不如 forEach ;观察者注册一个回调函数就可以了,而不是对象

其实虽然话是这么说,但是再看别人的设计对自己多少也是有些帮助的。
我原本的设想是,做一个让 JSer 能看懂的设计模式。
1. 但 JS 界段确实没有特别成熟的设计模式的一套体系。
2. JS 的很多草案也越来越像 Java 靠拢,或者说就是一种抄袭。
迭代器 JS 也有 Symbol.iterator,其次迭代器和 forEach 的行为其实是不一样的。比如链表使用迭代器可以遍历,但是换 forEach 就不行了。同时在 ES6 中,你看到的 for…of 就已经对迭代器作出了实现。

其实叫类可能确实比叫方法好一些,但 JS 中类和方法的概念本来就相对模糊。

建议看曾探写的 javascript 设计模式与实践

关于《JavaScript的设计模式》在Node.js中的实践经验,以下是一些总结:

  1. 外观模式(Facade Pattern): 在Node.js中,外观模式通过提供统一的接口来简化复杂的子系统。例如:

    class ComplexModule {
        initialize() { /* 复杂的初始化逻辑 */ }
        operation1() { /* 复杂操作1 */ }
        operation2() { /* 复杂操作2 */ }
    }
    
    class ModuleFacade {
        constructor() {
            this.complexModule = new ComplexModule();
        }
        performOperations() {
            this.complexModule.initialize();
            this.complexModule.operation1();
            this.complexModule.operation2();
        }
    }
    
    const moduleFacade = new ModuleFacade();
    moduleFacade.performOperations();
    
  2. 单例模式(Singleton Pattern): 单例模式确保一个类只有一个实例,并提供一个全局访问点。这在Node.js中管理资源(如数据库连接)时非常有用。

    class Database {
        constructor() {
            this.connection = null;
        }
        static getInstance() {
            if (!Database.instance) {
                Database.instance = new Database();
            }
            return Database.instance;
        }
        connect() {
            this.connection = 'Connected';
        }
    }
    
    const db1 = Database.getInstance();
    const db2 = Database.getInstance();
    console.log(db1 === db2); // true
    
  3. 其他模式: 此外,工厂模式、观察者模式和中间件模式等也在Node.js中有广泛应用。这些模式增强了代码的可读性、可维护性和可扩展性。

通过运用这些设计模式,可以构建出更健壮、更易于维护的Node.js应用程序。

回到顶部