JavaScript 设计模式(五)— 技巧型设计模式

104 阅读8分钟

专注于处理逻辑的内容,实现代码层面更好的解耦和性能优化。

链模式

通过在对象方法中将当前对象返回,实行对同一个对象多个方法的链式调用,简化对该对象多个方法的多次调用

// 参考jQuery链式调用
let A = function (selector) {
    return new A.fn.init(selector);
};
A.fn = A.prototype = {
    constructor: A,
    init: function (selector) {
        this[0] = document.getElementById(selector);
        this.length = 1;
        return this;
    },
    length: 0,
    size: function () {
        return this.length;
    },
    //增强数组
    //javascript是弱类型语言,函数、数组、对象都被看成对象的实例
    //浏览器引擎解析时判断对象是否数组时,不仅判断是否具备length属性、可否同过【索引值】访问元素,还会判断是否具备数组方法
    push: [].push,
    sort: [].sort,
    splice: [].splice,
};
A.fn.init.prototype = A.fn;
console.log(A('test')); // init [div#test]

委托模式

多个对象接受并处理同一个请求时,将请求委托给另外一个对象统一处理
委托者模式解决了请求与委托者之间的耦合,被委托者处理后接受的请求后,分发给相应的委托者去处理

// 点击单元格时背景色变成灰色
// js的垃圾回收机制是为了防止内存泄漏(不需要的某块内存还存在),垃圾回收机制就是不停歇的寻找这些不再使用的变量,并且释放掉它所指向的内存
// 利用事件冒泡机制,将多个子元素的请求委托给父元素
const ul = document.getElementById('list');
ul.onclick = function (e) {
    let event = e || window.event,
        target = e.target || e.srcElement;
    if (target) {
        tar.style.backgroundColor = 'gray';
    }
};
// 多个业务请求进行封装,分发给相应的委托者进行处理
let Deal = {
    one: function () {
        // ...
    },
    two: function () {
        // ...
    },
    three: function () {
        // ...
    },
};
getDealData.then((res) => {
    // 数据包拆分
    for (const i in res) {
        Deal[i] && Deal[i](res[i]);
    }
});

数据访问对象模式

抽象和封装对数据源的访问与存储,DAO(date access object)通过对数据源链接管理,方便对数据的访问与存储
方便了对于前端存储的的管理(事务型数据库),统一使用方式,并且使用时更加方便

// 封装一个本地数据库
let BaseLocalStorage = function (preId, timeSign) {
    // 本地数据库前缀
    this.preId = preId;
    // 时间戳和存储数据的连接符
    this.timeSign;
};
BaseLocalStorage.prototype = {
    // 操作状态
    status: {
        FAil: 0,
        SUCCESS: 1,
        OVERFLOW: 2,
        TIMEOUT: 3,
    },
    storage: localStorage || window.localStorage,
    getKey: function (key) {
        return this.preId + key;
    },
    set: function (key, value, callback, time) {
        let dataStatus = this.status.SUCCESS,
            dataKey = get(key);
        try {
            time = new Date().getTime() || time.getTime();
        } catch (e) {
            time = new Date().getTime() + 1000 * 60 * 60 * 24 * 31;
        }
        try {
            this.storage.setItem(dataKey, time + this.timeSign + value);
        } catch (e) {
            dataStatus = this.status.OVERFLOW;
        }
        callback && callback.call(this, dataStatus, dataKey, value);
    },
    get: function (key, callback) {
        let dataStatus = this.status.SUCCESS,
            dataKey = get(key),
            value = null,
            timeSignLen = this.timeSign.length,
            that = this,
            index,
            time;
        result;
        try {
            value = that.storage.getItem(key);
        } catch (e) {
            result = {
                status: that.status.FAIL,
                value: null,
            };
            callback && callback.call(this, result.status, result.value);
            return result;
        }
        if (value) {
            index = value.indexOf(that.timeSign);
            time = +value.slice(0, index);
            // 如果时间未过期
            if (new Date().getTime(time) > new Date().getTime() || time == 0) {
                value = value.slice(index + timeSigLen);
            } else {
                value = null;
                dataStatus = this.status.TIMEOUT;
                that.remover(key);
            }
        } else {
            dataStatus = this.status.FAIL;
        }
        result = {
            status: dataStatus,
            value: value,
        };
        callback && callback.call(this, result.status, result.value);
        return result;
    },
    remove: function (key, callback) {
        let dataStatus = this.status.SUCCESS,
            dataKey = get(key),
            value = null;
        try {
            value = that.storage.getItem(key);
        } catch (e) {}
        if (value) {
            try {
                this.storage.remove(key);
                dataStatus = this.status.SUCCESS;
            } catch (e) {}
        }
        callback &&
            callback.call(
                this,
                dataStatus,
                dataStatus > 0
                    ? null
                    : values.slice(value.indexOf(this.timeSign) + this.timeSign.length)
            );
    },
};
// 使用nodejs创建mongoDB数据库

节流模式

对重复的业务逻辑进行节流控制,执行最后一次操作并取消其他操作,以提高性能
构造节流器有两件事:能够清除将要执行的函数、能够延迟执行函数
节流器应用场景:时间段n内执行一次事件,即控制事件触发频率
类似的还有防抖:时间间隔n执行一次事件,即减少重复事件触发,可参考lodash

function extend(target, source) {
    // 遍历源对象属性
    for (const i in source) {
        target[i] = source[i];
    }
    return target;
}
// 节流器
const throttle = function () {
    // 可使用解构代替 或设置形参数代替
    let isClear = arguments[0],
        fn;
    if (typeof isClear === 'boolean') {
        fn = arguments[1];
        fn._throttleID && clearTimeout(fn._throttleID);
    } else {
        fn = isClear;
        let param = arguments[1];
        let p = extend({ const: null, args: [], time: 300 }, param);
        // 清除执行函数句柄
        arguments.callee(true, fn);
        fn._throttleID = setTimeout(() => {
            fn.apply(p.context, p.args);
        }, p.time);
    }
};
// 图片延迟加载,加载图片过多时占用线程,影响其他文件加载
// 图片篇幅过多时,监听scroll或resize事件加载可视范围内的图片
// 声明一个节流延迟加载图片类
function LazyLoad(id) {
    this.container = document.getElementById(id);
    this.imgs = this.getImgs();
    this.init();
}
LazyLoad.prototype = {
    // 初始化图片加载
    // 绑定窗口事件
    init: function () {
        this.update();
        this.binEvent();
    },
    // 获取延迟加载的图片
    getImgs: function () {
        const imgs = this.container.getElementsByTagName('img');
        const arr = Array.from(imgs);
        return arr;
    },
    update: function () {
        if (!this.imgs.length) {
            return;
        }
        let i = this.imgs.length;
        for (--i; i >= 0; i--) {
            if (this.shouldShow(i)) {
                // 加载图片
                this.imgs[i].src = this.imgs[i].getAttribute('data-src');
                // 清除缓存
                this.imgs.splice(i, 1);
                console.log(this.img);
            }
        }
    },
    shouldShow: function (i) {
        let img = this.imgs[i],
            scrollTop = document.documentElement.scrollTop || document.body.scrollTop,
            scrollBottom = scrollTop + document.documentElement.ClientHeight;
        imgTop = this.pageY(img);
        imgBottom = imgTop + img.offsetHeight;
        if (
            (imgBottom > scrollTop && imgBottom < scrollBottom) ||
            (imgTop > scrollTop && imgTop < scrollBottom)
        ) {
            return true;
        } else {
            return false;
        }
    },
    pageY: function (el) {
        return el.offsetParent ? el.offsetTop + this.pageY(el.offsetParent) : el.offsetTop;
    },
    on: function (el, type, fn) {
        // 兼容IE,事实上IE已经被微软废弃,以后类似例子应当不会出现了
        el.addEventListener
            ? el.addEventListener(type, fn, false)
            : el.attachEvent('on' + type, fn, false);
    },
    binEvent: function () {
        const that = this;
        this.on(window, 'resize', function () {
            throttle(that.update, { context: that });
        });
        this.on(window, 'scroll', function () {
            throttle(that.update, { context: that });
        });
    },
};

简单模板模式

通过格式化字符串拼凑出视图避免创建视图时大量节点操作,优化内存开销

// 创建一个模板生成器,利用es6的模板字符串
const templateGenerator = function (name,data) {
    const view = {
        code: `<pre><code>${data.code}</code><pre>`,
        img: `<img src="${data.src}" alt="${data.alt}" title="${data.title}>`,
        // 组合模板
        them: ['<div>',`<h1>${data.h1}</h1>`,`${data.content}`'</div>'].join()
    }
    return view[name]
}

惰性模式

减少每次代码的分支重复性判断,重定义对象来屏蔽分支判断(类似机器学习)

// 第一种,在页面加载时执行,确定时占用一定资源,降低页面渲染速度
// 第二种,在第一种的基础上做一次延迟执行(惰性执行)
/** 单体式命名空间 */
let A = {};
A.show = function (prefix, data) {
    if (prefix === '-webkit-') {
        A.show = function () {
            console.log('-webkit-' + data.name);
        };
    } else if (prefix === '-moz-') {
        A.show = function () {
            console.log('-moz-' + data.name);
        };
    } else if (prefix === '-o-') {
        A.show = function () {
            console.log('-o-' + data.name);
        };
    } else {
        A.show = function () {
            console.log('-any-' + data.name);
        };
    }
    A.show(prefix, data);
};

参与者模式

在特定的作用域中执行给定的函数,并将参数原封不动地传递

// 通过寄生简单实现函数绑定 bind
function bind(fn, context) {
    // 闭包返回新函数
    return function () {
        // 对 fn 装饰并返回
        return fn.apple(context, arguments);
    };
}
// 借助函数柯里化实现给运行的函数添加额外的自定义数据
// 函数柯里化:对函数参数进行分割,根据传递的参数不同,可以让一个函数存在多种状态(多态扩展)
function curry(fn) {
    let _slice = [].slice;
    // 从原参数第二个参数开始算起
    let args = _slice.call(arguments, 1);
    // 闭包返回新函数
    return function () {
        // 类数组转换为数组
        let addArgs = _slice.call(arguments),
            //拼接参数
            allArgs = args.concat(addArgs);
        // 返回新函数
        return fn.apply(null, allArgs);
    };
}
// 加法器
function add(num1, num2) {
    return num1 + num2;
}
// 加 5 加法器
function add5(num) {
    return add(5, num);
}
// 使用柯里化创建加 5 加法器
const _add5 = curry(add, 5);
// 增加了不固定的参数,扩大了使用范围,就是函数反柯里化
// 反柯里化函数
Function.prototype.uncurry = function () {
    let that = this;
    return function () {
        /**
           --> apply具有分解参数的作用,这很重要
                 apply把arguments分解成了args1, args2, ...
            --> that.call(args1, args2, args3, ...);
            --> args1.that(args2, args3, ...);
        */
        // 检验当前类型
        return Function.prototype.call.apply(that, arguments);
    };
};
// 保存数组 push 方法
let push = [].push.uncurry();
let testArray = {};
push(testArray, 'first data', 'second data');
console.log(testArray);
// 使用 object.prototype.toString 校验对象类型
let toString = Object.prototype.toString.uncurry();
console.log(toString(function () {}));

等待者模式

通过对多个异步进程监听,来触发未来发生的动作
等待者对象无需实时监听异步逻辑的完成,只需要在注册监听的异步逻辑状态发生改变时,对所有异步逻辑的状态迭代确认

// 等待者对象,类似es6的Promise.all,es7的async await
const Waiter = function () {
    let container = [],
        doneArr = [],
        failArr = [],
        slice = Array.prototype.slice,
        that = this;
    let PromiseData = function () {
        this.resolved = false;
        this.rejected = false;
    };
    PromiseData.prototype = {
        resolve: function () {
            this.resolved = true;
            if (!container.length) {
                return;
            }
            for (let i = container.length - 1; i >= 0; i--) {
                // 如果有任意一个监控对象没有 被解决 或 解决失败 则返回
                if ((container[i] && !container[i].resolved) || container[i].rejected) {
                    return;
                }
                container.splice(i, 1);
            }
            // 只执行一次
            _exec(doneArr);
        },
        reject: function () {
            this.rejected = true;
            // 如果没有监控对象则返回
            if (!container.length) {
                return;
            }
            // 清空所有监控对象
            container.splice(0);
            _exec(failArr);
        },
    };
    // 创建监控对象
    that.Deferred = function () {
        return new PromiseData();
    };
    // 执行回调函数
    function _exec(arr) {
        console.log('exec');
        for (const i of arr) {
            try {
                i && i();
            } catch (err) {}
        }
    }
    that.when = function () {
        container = slice.call(arguments);
        // 向前遍历对象
        for (let i = container.length - 1; i >= 0; i--) {
            // 如果有监控对象 不存在 或 被解决 或不是监控对象
            if (
                !container[i] ||
                container[i].resolved ||
                container[i].rejected ||
                !(container[i] instanceof PromiseData)
            ) {
                container.splice(i, 1);
            }
        }
        return that;
    };
    that.done = function () {
        doneArr = doneArr.concat(slice.call(arguments));
        return that;
    };
    that.fail = function () {
        failArr = failArr.concat(slice.call(arguments));
        return that;
    };
};
// example
const waiter = new Waiter();
const first = (function () {
    const p = waiter.Deferred();
    setTimeout(() => {
        console.log('first');
        p.resolve();
    });
    return p;
})();
const second = (function () {
    const p = waiter.Deferred();
    setTimeout(() => {
        console.log('second');
        p.resolve();
    });
    return p;
})();
const third = (function () {
    const p = waiter.Deferred();
    setTimeout(() => {
        console.log('third');
        p.resolve();
    });
    return p;
})();
// 可参考jQuery的 Deferred 对象: $.ajax('http://xxx').done(()=>{}).fail(()=>{})
waiter.when(first, second, third).done(function () {
    console.log('success');
});
// 简易 es6 Promise 实现
const PENDING = 'pending',
    FULFILLED = 'fulfilled',
    REJECTED = 'rejected';

function MyPromise(executor) {
    let self = this;
    self.value = null;
    self.error = null;
    self.status = PENDING;
    // 绑定多个同步回调
    self.onFulfilledCallbacks = [];
    self.onRejectedCallbacks = [];

    const resolve = (value) => {
        if (self.status != PENDING) return;
        setTimeout(() => {
            self.status = FULFILLED;
            self.value = value;
            self.onFulfilledCallbacks.forEach((callback) => callback(self.value));
        });
    };

    const reject = (value) => {
        if (self.status != PENDING) return;
        setTimeout(() => {
            self.status = REJECTED;
            self.error = value;
            self.onRejectedCallbacks.forEach((callback) => callback(self.value));
        });
    };

    executor(resolve, reject);
}

// 拆解 MyPromise 的过程,其中的【递归调用】传入的 resolve 和 reject 控制的是最开始传入的 bridgePromise 的状态
function resolvePromise(bridgePromise, x, resolve, reject) {
    if (x instanceof MyPromise) {
        if (x.status === PENDING) {
            x.then(
                (y) => {
                    resolvePromise(bridgePromise, y, resolve, reject);
                },
                (error) => {
                    reject(error);
                }
            );
        } else {
            x.then(resolve, reject);
        }
    } else {
        resolve(x);
    }
}

MyPromise.prototype.then = function (onFulfilled, onRejected) {
    // 处理 then 中的两个参数不传的情况
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : (value) => value;
    onRejected =
        typeof onRejected === 'function'
            ? onRejected
            : (error) => {
                  throw error;
              };
    let bridgePromise;
    let self = this;

    // 返回新的MyPromise对象——bridgePromise
    if (this.status === PENDING) {
        return (bridgePromise = new MyPromise((resolve, reject) => {
            self.onFulfilledCallbacks.push(() => {
                try {
                    // 拿到 then 返回的结果
                    let x = onFulfilled(self.value);
                    resolvePromise(bridgePromise, x, resolve, reject);
                } catch (e) {
                    reject(e);
                }
            });
            self.onRejectedCallbacks.push(() => {
                try {
                    let x = onRejected(self.error);
                    resolvePromise(bridgePromise, x, resolve, reject);
                } catch (e) {
                    reject(e);
                }
            });
        }));
    } else if (this.status === FULFILLED) {
        return (bridgePromise = new MyPromise((resolve, reject) => {
            try {
                // 状态变为成功,会有相应的 self.value
                let x = onFulfilled(self.value);
                // 暂时可以理解为 resolve(x),后面具体实现中有拆解的过程
                resolvePromise(bridgePromise, x, resolve, reject);
            } catch (e) {
                reject(e);
            }
        }));
    } else {
        return (bridgePromise = new MyPromise((resolve, reject) => {
            try {
                // 状态变为失败,会有相应的 self.error
                let x = onRejected(self.error);
                // 暂时可以理解为 resolve(x),后面具体实现中有拆解的过程
                resolvePromise(bridgePromise, x, resolve, reject);
            } catch (e) {
                reject(e);
            }
        }));
    }
};

// catch 语法糖
MyPromise.prototype.catch = function (onRejected) {
    return this.then(null, onRejected);
};

// 传参为一个 MyPromise, 则直接返回它
// 传参为一个 thenable 对象,返回的 MyPromise 会跟随这个对象,采用它的最终状态作为自己的状态
// 其他情况,直接返回以该值为成功状态的 MyPromise对象
MyPromise.resolve = function (param) {
    if (param instanceof MyPromise) return param;
    return new MyPromise((resolve, reject) => {
        if (param && param.then && typeof param.then === 'function') {
            // param 状态变为成功会调用 resolve,将新 MyPromise 的状态变为成功,反之亦然
            param.then(resolve, reject);
        } else {
            resolve(param);
        }
    });
};

// 传入的参数会作为一个 reason 原封不动地往下传
MyPromise.reject = function (reason) {
    return new MyPromise((resolve, reject) => {
        reject(reason);
    });
};

// 无论当前 MyPromise 是成功还是失败,调用finally之后都会执行 finally 中传入的函数,并且将值原封不动的往下传
MyPromise.prototype.finally = function (callback) {
    return this.then(
        (value) => {
            return MyPromise.resolve(callback()).then(() => {
                return value;
            });
        },
        (error) => {
            return MyPromise.resolve(callback()).then(() => {
                return error;
            });
        }
    );
};

// 使用node模块
let fs = require('fs');
let readFilePromise = (filename) => {
    return new MyPromise((resolve, reject) => {
        fs.readFile(filename, (err, data) => {
            if (!err) {
                resolve(data);
            } else {
                reject(err);
            }
        });
    });
};

readFilePromise('./001.txt')
    .then((data) => {
        console.log(data.toString());
        return readFilePromise('./002.txt');
    })
    .then((data) => {
        console.log(data.toString());
        throw 'err';
    })
    .catch((err) => {
        console.log('err:', err);
        return "it's end";
    })
    .finally(() => {
        console.log('finally');
    })
    .then((data) => {
        console.log(data.toString());
    });

new MyPromise((res1, rej) => {
    res1(
        new MyPromise((res2, rej) => {
            res2(
                new MyPromise((res3, rej) => {
                    res3('555');
                }).then((data) => {
                    console.log(data + '666');
                })
            );
        }).then((data) => {
            console.log(data + '777');
        })
    );
}).then((data) => {
    console.log(data + '888' + '\n');
});