不知不觉断更三个月了。。阿里云外包的工作没有想象中的难适应,多页面vue+jQuery的技术栈,大部分时间都像在写jQuery和原生JS,实现的需求虽然算是过关了,但是觉得代码质量和代码规范还是一团糟。。从一周前每天看几章《JavaScript设计模式与开发实践》,写了点笔记和感想,趁着周末总结出来。

关于面向对象的JavaScript

传统面向对象语言的几个重要特征,比如继承,封装,多态,在JavaScript动态类型脚本语言中都用迂回的方式实现。比如封装不再依赖private,protected之类的关键字,而是靠函数作用域实现;继承靠原型链实现;多态靠检测参数的值或者给传进来的对象定义一个相同方法名的不同方法。

动态类型

对变量赋值时,不需考虑它的类型

——可以尝试调用任何对象的任意方法,而不需要考虑它原本是否被设计为拥有该方法

只关注对象的行为,不去关注对象本身。例如一个对象如果有pop和push方法并且它们有正确的实现,那么就可以当作一个栈来使用(鸭子类型)。

动态类型赋予JavaScript极大的灵活性,也是它能够简化实现各种静态语言设计模式的重要原因。

多态

同一操作作用于不同的对象上可以产生不同的解释和执行结果。

——把“做什么”和“谁来做”相分离。

1,检测参数,用if分支实现多态。

var student = {
    say:funtion(){
    	console.log('am student')
	}
}
var teacher = {
    say:funtion(){
    	console.log('am teacher')
	}
}
var introduce = function(position){
    if(position === 'student'){            // 检测传进来的参数值选择采取的操作
        student.say()
    }else if(position === 'teacher'){
        teacher.say()
    }
}
introduce('teacher')  // am teacher

introduce函数通过检测传进来的参数值来执行不同的操作,但如果想再增加一个职位,那么就只能在introduce函数里继续堆砌if语句。

这里可以注意到,student和teacher有同一个方法名的方法say(),虽然他们执行结果不同,但是调用过程是一样的,因此还可以直接把不同的对象传进去,调用同一个方法名的方法实现多态。

var introduce = function(position){
    position.say()
}
introduce(student);  // am student
introduce(teacher);  // am teacher

原型模式

创建一个对象有两种思路:

a,先指定类型,通过实例化这个类获得一个对象。

b,不关心具体类型,找到一个对象克隆得到一个对象。

很明显JavaScript使用第二种思路创建对象。具体的实现过程是那个经典的问题,new操作都干了啥。。

这种创建对象的思路有两个重要的特征,一是对象能够记住它的原型,二是如果对象无法响应一个请求,会委托给自己的原型去处理。

JavaScript的对象最初都是由Object.prototype克隆得到的,所以所有对象最初的原型都指向Object.prototype,但是JavaScript允许对象动态的改变对象原型的指向,因此才有各种灵活的继承实现。

单例模式

保证一个类仅有一个实例,并提供一个访问这个实例的全局访问点。

思路:用一个变量标识这个类有没有创建过对象,如果创建过则直接返回之前创建的对象,否则创建一个实例并返回。

实现方式

普通实现:

Singleton.getInstance = (function(){
    var instance = null;                // 是否创建过对象的标识
    return function(name){
        if(!instance){
            instance = new Singleton(name);
        }
        return instance;                // 访问点
    }
})()

缺点:使用者必须用Singleton.getInstance创建单例,而不能直接使用new Singleton()的方式。

透明实现:

var Singleton = (function(){
    var instance;                     // 是否创建过对象的标识
    var Singleton = function(props){
        if(instance){
            return instance;
        }
        this.name = props;
        this.init();				 // 为单例执行某些操作
        return instance = this;
    }
    Singleton.prototype.init = function(){
    	// some code...
	}
    return Singleton;
})()
var a = new Singleton('xcola')
a   // Singleton {name: "xcola"}

缺点:违反单一职责原则,代码复杂。

代理实现:将管理单一实例的代码和创建实例的代码相拆分,使得创建不同的对象也可以使用单例模式

var CreateObj = function(props){			// 创建单例
    this.name = props;
    this.init();
}
CreateObj.prototype.init = function(){       // 执行构建单例的操作
	// some code...
}
var proxyCreateObj = (function(){			// 代理实现控制单一实例
    var instance;
    return function(props){
        if(!instance){
            instance = new CreateObj(props);
        }
        return instance;
    }
})()
var a = proxyCreateObj('emm')
a     // CreateObj {name: "emm"}

BB了这么多,JavaScript最简单的单例实现是创建全局变量。/笑哭

var a = {name:'emm'}
//首先它独一无二,其次它能全局都访问到

大量使用全局变量总是不好的,引入命名空间。

var namespace = {
    a:{},
    b:function(){}
}

关于惰性单例

在需要的时候才创建单例实例(事件触发)。将创建实例的职责和管理单例的职责写在两个方法里,他们相互之间可以独立变化而互不影响,结合在一起使用就完成了一个单例模式。

策略模式

定义一系列的算法,把他们封装起来,使他们可以相互替换。

思路:一个基于策略模式的程序由至少两部分组成,一组策略类,存放具体的算法,一组环境类,接受客户的请求,委托给某一个策略类处理。

做法:告诉程序,谁来做,做什么。

var strategies = {					// 一组策略类
    "s":function(salary){
        return salary*4;
    },
    "a":function(salary){
        return salary*3;
    },
	"b":function(salary){
        return salary*2;
    }
}
var caculate = function(level,salary){	// 选取策略类,执行对应操作
    return strategies[level](salary);
}
caculate('s',1000)      // 4000

应用场景:某个元素的不同动态效果,不断变更的表单验证方式

代理模式

为一个对象提供一个代用品或者占位符,以便控制对它的访问。

思路:当不方便直接访问一个对象或者当前不满足条件的时候,提供一个替身对象来控制这个对象。

虚拟代理

创建代理舰艇某个对象的状态变化,在合适的时机由代理对象执行操作。

example:在图片尚未加载完成时显示loading.gif图。

var newImg = (function(){
    var imgNode = document.createElement('img');
    document.body.appendChild(imgNode);
    return{
        setSrc:function(src){
            imgNode.src = src;
        }
    }
})()
var proxySetImg = (function(){
    var img = new Image;
    img.onload = function(){
        newImg.setSrc(this.src);            // 3,加载完毕时将img节点的loading图替换成真正的图片
    }
    return {
        setSrc:function(src){
            newImg.setSrc('loading.gif');   // 2,先显示loading图
            img.src = src                   // 1,接受真正的图片地址
        }
    }
})()
proxySetImg.setSrc('http://imgurl')

代理对象和被代理对象都提供setSrc方法,所以任何使用本体对象的场景都可以无缝切换成代理对象。

应用场景:合并http请求,惰性加载文件。

缓存代理

为一些开销大的运算缓存结果,下次运算时如果参数一致,直接从缓存池中返回之前计算的结果。

example:缓存乘积运算

var mult = function(){
    var result = 1;
    for(let i = 0; i < arguments.length; i++){
        result = result * arguments[i];
    }
    return result;
}
var proxyMult = (function(){
    var cache = {};
    return function(){
        var args = Array.prototype.join.call(arguments,',');
        if(args in cache){
            return cache[args];
        }
        return cache[args] = mult.apply(this.arguments)
    }
})()
proxyMult(1,2,3)     // 6 mult函数计算得出
proxyMult(1,2,3)     // 6 proxyMult从缓存中取出之前的结果

发布-订阅(观察者)模式

定义对象间一种一对多的依赖关系,当一个对象(发布者)的状态发生改变时,所有依赖于它的对象(订阅者)都将得到通知。在js中事件模型即是。

思路:1,指定发布者。2,为发布者添加缓存列表,存放订阅者的回调。3,发布消息时遍历缓存列表。

简单订阅-发布实现:

var publisher = {};						// 定义发布者
publisher.clientList = [];				 // 定义缓存池
publisher.listen = function(callback){    // 订阅方法
    this.clientList.push(callback);
}
publisher.trigger = function(){			  // 发布消息
    for(let i = 0,fn; fn = this.clientList[i++]; ){
        fn.apply(this,arguments)
    }
}
var subscriber = {							// 定义订阅者
    name:'xcola',
    subscribe:function(info){				 // 订阅的回调
        console.log(info);
    }
}
publisher.listen(subscriber.subscribe);		  // 将订阅者的回调传给listen函数,加入发布者的缓存池中
publisher.trigger('emmm')                     // subscriber.subscribe输出“emmm”

在此种实现下,订阅者会收到所有发布者的消息,若添加一个标识,指定订阅哪种类型的消息,这样就不会给订阅者带来不必要的困扰。

指定订阅类型

publisher.listen = function(key,callback){
    if(!this.clientList[key]){			// 如果先前不存在这种标识的发布类型则新建一个
        this.clientList[key] = []
    }
    this.clientList[key].push(callback);	// 将回调存进对应key标识的缓存池中
}
publisher.trigger = function(){
    var key = Array.prototype.shift.call(arguments),  // 取出arguments中第一个参数,即key
        fns = this.clientList[key];		 // 取出对应key的缓存池
    if(!fns || fns.length === 0){			// 如果没有回调返回false
        return false;
    }
    for(let i = 0,fn; fn = this.clientList[i++]; ){
        fn.apply(this,arguments)		// 遍历缓存池,调用回调函数传递消息
    }
}
publisher.listen('xcola',subscriber.subscribe);
publisher.trigger('xcola','emmm')              // 只有标识为xcola的回调会收到emmm

添加取消订阅功能

publiser.remove = function(key,callback){
    var fns = this.clientList[key];        // 取出对应key的缓存池
    if(!fns){                             // 如果缓存池不存在,则返回false
        return false;
    }
    if(!callback){					// 如果没有指定对应key的callback,则清除对应key的所有订阅回调
        fns && (fns.length === 0);		// 只有fns不为空才会执行fns.length === 0
    }else{
        for(let i = fns.length - 1; i >= 0;i--){
            var _callback = fns[i];
            if(_callback == callback){
                fns.splice(i,1);		// 删除缓存池中找到的对应的订阅callback
            }
        }
    }
}

给对象动态的添加订阅发布功能

var event = {                        // 将所有方法封装在一个对象中
    clientLish:[],
    listen:function(key,callback){
                if(!this.clientList[key]){
                    this.clientList[key] = []
                }
                this.clientList[key].push(callback);
            },
    trigger:function(){
                var key = Array.prototype.shift.call(arguments),
                    fns = this.clientList[key];
                if(!fns || fns.length === 0){
                    return false;
                }
                for(let i = 0,fn; fn = this.clientList[i++]; ){
                    fn.apply(this,arguments)
                }
            },
    remove:function(key,callback){
            var fns = this.clientList[key];
            if(!fns){
                return false;
            }
            if(!callback){
                fns && (fns.length === 0);
            }else{
                for(let i = fns.length - 1; i >= 0;i--){
                    var _callback = fns[i];
                    if(_callback == callback){
                        fns.splice(i,1);
                    }
                }
            }
        }
}
var installEvent = function(obj){			// 为某个对象安装订阅发布功能
    for(let i in event){					// 遍历event的方法,为传入的对象添加event中存放的方法
        obj[i] = event[i]
    }
}

​ 于2018/10/14 夜