JavaScript 设计模式(三)— 结构型模式

95 阅读6分钟

更关注于如何将类或对象组合成更大、更复杂的结构,简化设计。

外观模式

为一组复杂的子系统接口提供更高一级的统一接口,通过该接口使得对子系统的访问更容易。
在 javascript 中有时也用于对底层结构兼容性做统一封装简化用户操作。

// 对元素绑定事件做处理
function addEvent(dom, type, fn) {
    // 支持DOM2事件
    if (dom.addEventListener) {
        dom.addEventListener(type, fn, false);
    } else if (dom.attachEvent) {
        // IE已经被微软宣布废弃,除非维护旧项目,否则无需考虑兼容问题
        dom.attachEvent('on' + type, fn);
    } else {
        dom['on' + type] = fn;
    }
}
let divDom = document.createElementById('div');
divDom.style = 'width: 100px; height:50px; background: yellow;';
document.body.append(divDom);
addEvent(divDom, 'click', function () {
    console.log('click me !!!');
});

适配器模式

传统模式中,适配器将一个类(对象)的接口(属性和方法)转化成另外一个接口,解决不同类之间的接口兼容性问题。
javascript 中适配器应用范围更加广泛,比如适配两个代码库、适配前后端数据等等。

// 适配框架A和jQuery
let A  = A || {};
A.get = function (id) {
    // $(jQuery对象简写)
    return $(id).get(0);
};
// 参数适配器
function doSomething(obj) {
    let _adapter {
        name: 'test',
        sort: '001',
        type: 'test'
    };
    for(const i in obj) {
        _adapter[i] in obj[i] || _adapter[i];
    }
}
// 后端数据适配
function ajaxAdapter(data) {
    return [data['key1'],data['key2'],data['key3']];
}
// 使用jQuery
$.ajax({
    url: 'xxx.php',
    success: function (data, status) {
        if(data) {
            ajaxAdapter(doSomething(data))
        }
    }
})

代理模式

由于一个对象不能直接引用另外一个对象,通过代理对象(对象的替代品或其占位符)在两个对象之间起到代理的作用。
代理控制着对于原对象的访问, 并允许在将请求提交给对象前后进行一些处理。
经典案列,可参考前端跨域解决方案。

/* 受限于浏览器的同源策略,javascript无法调用其他域的内容
 * 跨域方案:包含src属性的标签、JSON、代理模板,window.postMessage方法
 */
/* 代理模板
 * 解决思路:自己域中的两个页面相互之间是可以调用的,即代理页面B可以调用被代理页面A中的对象
 * 实现思路:在被访问的域中,请求返回的Header重定向到代理页面,并在代理页面中处理被代理页面A
 */

装饰者模式

在不改变原有对象的基础上,通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为,以满足更复杂的需求。
相比于适配器模式,可以在不了解原有对象功能的基础上进行拓展。

// 输入框装饰器
let decorator = function (input, fn) {
    let input = document.getElementById(input);
    // 若输入框已绑定事件
    if (typeof input.onclick === 'function') {
        let oldClickEvent = input.onclick;
        input.onclick = function () {
            oldClickEvent();
            fn();
        };
    } else {
        // 绑定事件
        input.onclick = fn;
    }
};
// 电话输入框
decorator('tel_input', function () {
    console.log('tel');
});
// 地址输入框
decorator('address_input', function () {
    console.log('address');
});

桥接模式

在系统沿着多个维度变化时,不增加复杂度并且解耦。
将一个大类或一系列紧密相关的类拆分为抽象和实现两个独立的层次结构, 从而能在开发时分别使用。
通过桥接(方法),我们可以将创建的对象(实现层)和业务逻辑(抽象层)进行解耦。

// 使用canvas实现游戏人物(具备说话、跑步等动作单元)
// 运动单元
function Speed(x, y) {
    this.x = x;
    this.y = y;
};
Speed.prototype.run = function () {
    console.log('run start');
};
// 着色单元
function Color(cl) {
    this.color = cl;
};
Color.prototype.draw = function () {
    console.log('draw color');
};
// 创建人物类,有颜色、可以说话
function Person(x, y, cl) {
    this.speed = new Speed(x, y);
    this.color = new Color(cl);
};
// 使用桥接方法(匿名方法)解耦
Person.prototype.init() = function () {
    this.speed.run();
    this.color.draw();
}
let p = new Person(10, 25, 'yellow');
p.init();

组合模式

又称部分-整体模式,将对象组合成树形结构以表示‘部分整体’的层次结构。
对单个对象和组合对象的使用具有一致性。

// 使用组合寄生式继承
function inheritObject(obj) {
    // 声明过渡对象
    function Fn() {}
    // 过渡对象继承父对象
    Fn.prototype = obj;
    // 返回过渡对象实例
    return new Fn();
}
function inheritPrototype(subClass, superClass) {
    // 复制一份父类原型的副本
    let p = inheritObject(superClass.prototype);
    // 设置子类的原型
    subClass.prototype = p;
    // 修正因为重写子类原型,导致子类的constructor属性被修改
    p.constructor = subClass;
}
// 创建一个注册页面
// 虚拟父类,方便统一管理接口及简化子类设计
let Form = function () {
    this.children = [];
    this.element = null;
};
Form.prototype = {
    init: function () {
        throw new Error('请重写你的方法');
    },
    add: function () {
        throw new Error('请重写你的方法');
    },
    getElement: function () {
        throw new Error('请重写你的方法');
    },
};
// 各个成员类
// 表单类
let FormItem = function (id, parent) {
    Form.call(this);
    this.id = id;
    this.parent = parent;
    this.init();
};
inheritPrototype(FormItem, Form);
FormItem.prototype.init = function () {
    this.element = document.createElement('form');
    this.element.className = 'new-form-item';
};
FormItem.prototype.add = function (child) {
    this.children.push(child);
    this.element.appendChild(child.getElement());
    return this;
};
FormItem.prototype.getElement = function () {
    return this.element;
};
FormItem.prototype.show = function () {
    this.parent.appendChild(this.element);
};
// 区域类
let FieldsetItem = function (id, title) {
    Form.call(this);
    this.id = id;
    this.init(title);
};
inheritPrototype(FieldsetItem, Form);
FieldsetItem.prototype.init = function (title) {
    this.element = document.createElement('div');
    this.element.id = this.id;
    this.element.className = 'new-fieldset-item';
    let titleDom = document.createElement('div');
    titleDom.innerText = title;
    titleDom.className = 'new-fieldset-item-title';
    this.element.appendChild(titleDom);
};
FieldsetItem.prototype.add = function (child) {
    this.children.push(child);
    this.element.appendChild(child.getElement());
    return this;
};
FieldsetItem.prototype.getElement = function () {
    return this.element;
};
// 条目类
let Group = function () {
    Form.call(this);
    this.init();
};
inheritPrototype(Group, Form);
Group.prototype.init = function () {
    this.element = document.createElement('div');
    this.element.className = 'new-group';
};
Group.prototype.add = function (child) {
    this.children.push(child);
    this.element.appendChild(child.getElement());
    return this;
};
Group.prototype.getElement = function () {
    return this.element;
};
// 底层成员类无add方法
// 标签类
let Label = function (label, text) {
    Form.call(this);
    this.init(label, text);
};
inheritPrototype(Label, Form);
Label.prototype.init = function (label, text) {
    this.element = document.createElement('label');
    this.element.for = label;
    this.element.innerHTML = text;
    this.element.className = 'new-label';
};
Label.prototype.getElement = function () {
    return this.element;
};
// 输入框类
let Input = function (name) {
    Form.call(this);
    this.init(name);
};
inheritPrototype(Input, Form);
Input.prototype.init = function (name) {
    this.element = document.createElement('Input');
    this.element.name = name;
    this.element.className = 'new-input';
};
Input.prototype.getElement = function () {
    return this.element;
};
// 输入框类
let SpanItem = function (text) {
    Form.call(this);
    this.init(text);
};
inheritPrototype(SpanItem, Form);
SpanItem.prototype.init = function (text) {
    this.element = document.createElement('span');
    this.element.innerText = text;
    this.element.className = 'new-span-item';
};
SpanItem.prototype.getElement = function () {
    return this.element;
};
let form = new FormItem('FormItem', document.body);
form.add(
    new FieldsetItem('account', '账号')
        .add(
            new Group()
                .add(new Label('user_name', '用户名:'))
                .add(new Input('user_name'))
                .add(new SpanItem('4到6位数字或字母'))
        )
        .add(
            new Group()
                .add(new Label('password', '密 码:'))
                .add(new Input('password'))
                .add(new SpanItem('6到12位数字为密码'))
        )
)
    .add(
        new FieldsetItem('info', '信息').add(
            new Group()
                .add(new Label('nick', '昵 称:'))
                .add(new Input('nick'))
                .add(new SpanItem('4到6位数字或字母'))
        )
    )
    .show();

享元模式

运用共享技术有效支持大量的细粒度对象,避免对象间因为拥有相同内容造成多余开销。
主要是对数据、方法共享分离,将数据和方法分为内部数据、内部方法和外部数据、外部方方法,目的在于提升程序的执行效率和系统的性能。
摒弃了在每个对象中保存所有数据的方式,通过共享多个对象所共有的相同状态,在有限的内存容量中载入更多对象。

// 创建一个分页新闻列表
let article = [
        '新闻列表1',
        '新闻列表2',
        '新闻列表3',
        '新闻列表4',
        '新闻列表5',
        '新闻列表6',
        '新闻列表7',
        '新闻列表8',
        '新闻列表9',
    ],
    dom = null,
    p = 0,
    i = 0,
    num = 2,
    len = article.length;
// 冗余做法
let long = () => {
    for (; i < len; i++) {
        dom = document.createElement('div');
        dom.innerHTML = article[i];
        if (i >= num) {
            dom.style.display = 'none';
        }
        document.getElementById('container').appendChild(dom);
    }
    document.getElementById('next_page').onclick = function () {
        let div = document.getElementById('container').getElementsByTagName('div'),
            j = (k = n = 0);
        // 很棒的思路,利用取余,计算出列表第一条项目索引
        n = (++p % Math.ceil(len / num)) * num;
        for (; j < len; j++) {
            div[j].style.display = 'none';
        }
        console.log(p, n);
        for (; k < num; k++) {
            if (div[n + k]) {
                div[n + k].style.display = 'block';
            }
        }
    };
};
// 享元模式,分离数据层和视图层,抽出内部方法(getDiv)和外部方法(getNextPage)
let domWrapper = (function () {
    let created = [];
    function create() {
        let dom = document.createElement('div');
        document.getElementById('container').appendChild(dom);
        created.push(dom);
        return dom;
    }
    return {
        getDiv: () => {
            if (created.length < num) {
                return create();
            } else {
                let div = created.shift();
                created.push(div);
                return div;
            }
        },
    };
})();
let short = () => {
    for (; i < num; i++) {
        if (article[i]) domWrapper.getDiv().innerHTML = article[i];
    }
    document.getElementById('next_page').onclick = function () {
        if (article.length < num) return;
        j = n = 0;
        n = (++p % Math.ceil(len / num)) * num;
        console.log(p, n);
        for (; j < num; j++) {
            if (article[n + j]) {
                domWrapper.getDiv().innerHTML = article[n + j];
            }
            // else if (article[n + j - len]) {
            //     domWrapper.getDiv().innerHTML = article[n + j - len];
            // }
            else {
                domWrapper.getDiv().innerHTML = '';
            }
        }
    };
};