«

笔记-11-JavaScript设计模式

codeez 发布于 阅读:1526 笔记


设计原则:

工厂模式

工厂模式是由一个方法来决定到底创建哪个类的实例,而这些类经常拥有相同的接口。这种模式主要用在所实例化的类型在编译期不能确定,而是在执行期决定的情况。

分为简单工厂和工厂方法
简单工厂是将创建对象的步骤放在父类进行,工厂方法是延迟到子类中进行,它们两者都可以总结为:根据传入的字符串来选择对应的类
简单工厂

var UserFactory = function(role) {
    function Admin() {
        this.name = "管理员";
        this.viewPage = ['首页', '查询', '权限管理'];
    }
    function User() {
        this.name = "普通用户";
        this.viewPage = ['首页', '查询'];
    }
    switch(role) {
    case 'admin':
        return new Admin();
        break;
    case 'user':
        return new User();
        break;
    default:
        throw new Error('参数错误,可选参数:admin、user');
    }
}
var admin = UserFactory('admin');
var user = UserFactory('user')

工厂方法

// 安全模式创建的工厂方法函数
var UserFactory = function(role) {
    if(this instanceof UserFactory) {
        return new this[role]();
    } else {
        return new UserFactory(role);
    }
}
// 工厂方法函数的原型中设置所有对象的构造函数
UserFactory.prototype = {
    Admin: function() {
        this.name = "管理员";
    },
    User: function() {
        this.name = "用户";
    }
}
// 调用
var admin = UserFactory('Admin');
var user = UserFactory('User');

构造器模式

在面向对象的编程语言中,构造器是一个类中用来初始化新对象的特殊方法。并且可以接受参数用来设定实例对象的属性的方法。
利用原型链上被继承的特性,实现了构造器:

function Car(model, year, miles) {
    this.model = model
    this.year = year
    this.miles = miles
}
// 覆盖原型对象上的toString
Car.prototype.toString = function() {
    return `${this.model} has done ${this.miles} miles`
}
// 使用
var civic = new Car("Honda Civic", 2009, 20000)
var mondeo = new Car("Ford Mondeo", 2010, 50000)

单例模式

保证一个类仅有一个实例,并提供一个访问它的全局访问点。例如页面中的登陆弹窗、全局缓存等。

案例:假设要设置一个管理员,多次调用也仅设置一次,我们可以使用闭包缓存一个内部变量来实现这个单例。

function SetManager(name) {
    this.manager = name
}
SetManager.prototype.getName = function() {
    console.log(this.manager)
}
var SingletonSetManager = (function() {
    var manager = null
    return function(name) {
        if(!manager) {
            manager = new SetManager(name)
        }
        return manager
    }
})()
// 调试
SingletonSetManager('a').getName() // a
SingletonSetManager('b').getName() // a

原型模式

用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象,在JS中实现原型模式是在ECMAScript5中,提出的Object.create方法,使用现有的对象来提供新创建的对象的隐式原型(proto
案例:使用现有的对象来提供创建的对象proto

var prototype = {
  name: "Jack",
  getName: function() {
    return this.name;
  },
};
var obj = Object.create(prototype, {
  job: {
    value: "IT",
  },
});
console.log(obj.getName()); // Jack
console.log(obj.job); // IT
console.log(obj.__proto__ === prototype); //true

发布-订阅模式

发布-订阅模式,它定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在JS中通常使用注册回调函数的形式来订阅
优点:一为时间上的解耦,二为对象间解耦,可以用在异步编程中。
缺点:创建订阅者本身要消耗一定的时间和内存,订阅的处理函数不一定会被执行,驻留内存有性能开销,弱化了对象之间的联系,复杂的情况下可能会增加代码的可维护性。

class EventEmitter {
  constructor() {
    this.events = {};
  }

  // 订阅事件
  on(eventName, callback) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    this.events[eventName].push(callback);
  }

  // 触发事件
  emit(eventName, ...args) {
    if (this.events[eventName]) {
      this.events[eventName].forEach((callback) => callback(...args));
    }
  }

  // 取消订阅事件
  off(eventName, callback) {
    if (this.events[eventName]) {
      this.events[eventName] = this.events[eventName].filter(
        (cb) => cb !== callback
      );
    }
  }
}

// 使用示例
const eventEmitter = new EventEmitter();

eventEmitter.on("message", (data) => {
  console.log("Received message:", data);
});

eventEmitter.emit("message", "Hello, World!"); // 输出: Received message: Hello, World!

适配器模式

它的作用是解决两个软件实体间的接口不兼容的问题。使用适配器模式后,原本由于接口不兼容而不能工作的两个软件实体可以一起工作。
适配器的别名是包装器(wrapper),这是一个相对简单的模式。在程序开发中有许多这样的场景:当我们试图调用模块或者对象的某个接口时,却发现这个接口的格式并不符合当前的需求。这时候有两种解决办法,第一种是修改原来的接口实现,但如果原来的模块很复杂,或者我们拿到的模块时一段别人编写的经过压缩的代码,修改原接口就显得太不现实了。第二种办法是创建一个适配器,将原接口转换为客户希望的另一个接口,客户只需要和适配器打交道。

实现一个简单的数据格式转换适配器:

// 渲染数据,格式限制为数组了
function renderData(data) {
    data.forEach(function(item) {
        console.log(item);
    });
}

// 对非数组的进行转换适配
function arrayAdapter(data) {
    if (typeof data !== 'object') {
        return [];
    }

    if (Object.prototype.toString.call(data) === '[object Array]') {
        return data;
    }

    var temp = [];

    for (var item in data) {
        if (data.hasOwnProperty(item)) {
            temp.push(data[item]);
        }
    }

    return temp;
}

var data = {
    0: 'A',
    1: 'B',
    2: 'C'
};

renderData(arrayAdapter(data)); // A B C

装饰器模式

以动态地给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象。
是一种“即用即付”的方式,能够在不改变对象自身的基础上,在程序运行期间给对象动态的添加职责。
为对象动态的加入行为,经过多重包装,可以形成一条装饰链。
最简单的装饰者,就是重写对象的属性

function Person() {}

Person.prototype.skill = function() {
    console.log('数学');
};

// 装饰器,还会音乐
function MusicDecorator(person) {
    this.person = person;
}

MusicDecorator.prototype.skill = function() {
    this.person.skill();
    console.log('音乐');
};

// 装饰器,还会跑步
function RunDecorator(person) {
    this.person = person;
}

RunDecorator.prototype.skill = function() {
    this.person.skill();
    console.log('跑步');
};

var person = new Person();

// 装饰一下
var person1 = new MusicDecorator(person);
person1 = new RunDecorator(person1);

person.skill(); // 数学
person1.skill(); // 数学 音乐 跑步

代理模式

当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求作出一些处理后,再把请求转交给本体对象。代理和本体的接口具有一致性,本体定义了关键功能,而代理是提供或拒绝对它的访问,或者在访问本体之前做一些额外的事情。

代理模式主要有三种:保护代理,虚拟代理,缓存代理
保护代理主要实现了访问主体的限制行为,以过滤字符作为简单的例子:

// 主体,发送消息
function sendMsg(msg) {
    console.log(msg);
}
// 代理,对消息进行过滤
function proxySendMsg(msg) {
    // 无消息则直接返回
    if (typeof msg === 'undefined') {
        console.log('deny');
        return;
    }
    // 有消息则进行过滤
    msg = ('' + msg).replace(/泥\s*煤/g, '');

    sendMsg(msg);
}
sendMsg('泥煤呀泥 煤呀'); // 泥煤呀泥 煤呀
proxySendMsg('泥煤呀泥 煤'); // 呀
proxySendMsg(); // deny

它的意图很明显,在访问主体之前进行控制,没有消息的时候直接在代理中返回了,拒绝访问主体,这属于保护代理的形式。有消息的时候对敏感字符进行了处理,这属于虚拟代理的模式。
虚拟代理在控制对主体的访问时,加入了一些额外的操作,如在滚动事件触发的时候,也许不需要频繁触发,我们可以引入函数节流,这是一种虚拟代理的实现。

// 函数防抖,频繁操作中不处理,直到操作完成之后(再过 delay 的时间)才一次性处理
function debounce(fn, delay) {
    delay = delay || 200;

    var timer = null;

    return function() {
        var arg = arguments;

        // 每次操作时,清除上次的定时器
        clearTimeout(timer);
        timer = null;

        // 定义新的定时器,一段时间后进行操作
        timer = setTimeout(function() {
            fn.apply(this, arg);
        }, delay);
    }
};

var count = 0;

// 主体
function scrollHandle(e) {
    console.log(e.type, ++count); // scroll
}

// 代理
var proxyScrollHandle = (function() {
    return debounce(scrollHandle, 500);
})();

window.onscroll = proxyScrollHandle;
缓存代理可以为一些开销大的运算结果提供暂时的缓存,提升效率。来个栗子——缓存加法操作:
// 主体
function add() {
    var arg = [].slice.call(arguments);

    return arg.reduce(function(a, b) {
        return a + b;
    });
}

// 代理
var proxyAdd = (function() {
    var cache = [];

    return function() {
        var arg = [].slice.call(arguments).join(',');

        // 如果有,则直接从缓存返回
        if (cache[arg]) {
            return cache[arg];
        } else {
            var ret = add.apply(this, arguments);
            return ret;
        }
    };
})();

console.log(
    add(1, 2, 3, 4),
    add(1, 2, 3, 4),

    proxyAdd(10, 20, 30, 40),
    proxyAdd(10, 20, 30, 40)
); // 10 10 100 100

外观模式

为子系统中的一组接口提供一个一致的页面,定义一个高层接口,这个接口使子系统更加容易使用。
可以通过请求外观接口来达到访问子系统,也可以选择越过外观来直接访问子系统。
外观模式在JS中,可以认为是一组函数的集合

// 三个处理函数
function start() {
    console.log('start');
}

function doing() {
    console.log('doing');
}

function end() {
    console.log('end');
}

// 外观函数,将一些处理统一起来,方便调用
function execute() {
    start();
    doing();
    end();
}

// 调用init开始执行
function init() {
    // 此处直接调用了高层函数,也可以选择越过它直接调用相关的函数
    execute();
}

init(); // start doing end

迭代器模式

迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素。
JS中数组的map、forEach已经内置了迭代器

[1, 2, 3].forEach(function(item, index, arr) {
    console.log(item, index, arr);
});

不过对于对象的遍历,往往不能与数组一样使用同一的遍历代码,我们可以封装一下:

function each(obj, cb) {
    var value;

    if (Array.isArray(obj)) {
        for (var i = 0; i < obj.length; ++i) {
            value = cb.call(obj[i], i, obj[i]);

            if (value === false) {
                break;
            }
        }
    } else {
        for (var i in obj) {
            value = cb.call(obj[i], i, obj[i]);

            if (value === false) {
                break;
            }
        }
    }
}

each([1, 2, 3], function(index, value) {
    console.log(index, value);
});

each({a: 1, b: 2}, function(index, value) {
    console.log(index, value);
});

// 0 1
// 1 2
// 2 3
// a 1
// b 2

前端

请先 登录 再评论