JavaScript 中的单例模式

单例模式,保证系统中只存在一个对象。全局变量不是一个好做法,容易被污染,即便是某些约定俗成的变量如前端的 jQuery,也免不了被修改,而且有时我们希望该对象只有在被使用的情况下才创建,即惰性创建或懒创建,全局变量无法满足,因此需另寻他法。

JavaScript 中的单例模式是用闭包实现的,将创建对象的逻辑和管理单例的逻辑分开,符合单一职责原则,即创建对象的逻辑可以单独使用,如果需要单例,可和单例逻辑组合使用;然后使用一个标识来记录单例对象,如果对象已经存在就直接返回,如果不存在就创建对象,并赋值给该标识。

function Singleton(fn, isNew) {
    let instance = null;

    return function(...args) {
        if (!instance) {
            // 根据 isNew 判断对象是使用 new 还是直接调用函数
            instance = isNew ? new fn(...args) : fn(...args);
        }
        return instance;
    }
}

function Cat(name) {
    this.name = name;
}

const getCat = Singleton(Cat, true);
console.log(getCat("miao") === getCat("miao")); // true

看了 JavaScript 设计模式与开发实践 关于单例模式的结尾部分,学到了一个技巧,或者说是解决方案。那就是单例模式重在 “单”,“单” 什么?可以是 “单个” 对象的单,也可以是 “单次” 执行的单,因此它还可以保证函数只执行一次。

怎么保证函数只执行一次呢?函数得返回一个真值,因为 Singleton 中通过判断 instance 是否为真决定是否调用函数。

function fn() {
    console.log("fn 执行");
    // 返回真值
    return true;
}

const getFn = Singleton(fn);
getFn();
getFn();
getFn();
// 只打印一次

emmm,说实话,如果要求业务代码为了实现未来可能会用到的单次执行逻辑而特意返回真值,是不符合单一职责原则的,最好再专门写一个用来处理这种情况的函数。

function SingleExec(fn) {
    let status = "none"; // "none", "doing", "done"
    let promise = null;

    return async function(...args) {
        // 已经执行过,返回结果即可
        if (status === "done") return promise;
        // 正在执行中了,请稍后
        if (status === "doing") return promise;
        // 没执行过,开始执行并返回结果
        promise = new Promise(async (resolve, reject) => {
            try {
                status = "doing";
                result = await fn(...args);
                resolve(result);
                status = "done";
            } catch (err) {
                reject(err);
                // 执行出错算作 none 还是 done 得看情况处理
                status = "none";
            }
        })
        return promise;
    }
}

async function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

function task1() {
    console.log("task1");
    return "task1";
}

async function task2() {
    await sleep(1000);
    // throw new Error("err");
    console.log("task2");
    return "task2";
}

// const singleTask1 = SingleExec(task1);
// // task1 只执行一次,所以只打印一次,但执行结果打印三次,且是同时打印
// singleTask1().then(val => console.log("执行结果: ", val));
// singleTask1().then(val => console.log("执行结果: ", val));
// singleTask1().then(val => console.log("执行结果: ", val));

const singleTask2 = SingleExec(task2);
// task2 只执行一次,所以只打印一次,但执行结果打印三次,且是同时打印
singleTask2().then(val => console.log("执行结果: ", val)).catch(err => console.log(err));
singleTask2().then(val => console.log("执行结果: ", val)).catch(err => console.log(err));
singleTask2().then(val => console.log("执行结果: ", val)).catch(err => console.log(err));

上面实现的函数把同步异步的情况都考虑了,还做了错误处理。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注