JavaScript设计模式和开发实战

257 阅读14分钟

本文系《JavaScript设计模式和开发实战》读书笔记

第一部分:基础知识

第一章 面向对象的JavaScript

1. 动态类型语言和鸭子类型

静态类型语言在编译时便已确定变量的类型,而动态类型语言的变量类型要到程序运行的时候,待变量被赋予某个值之后,才会具有某种类型。

int a = 1 //静态类型语言
var a = 1 //动态类型语言

鸭子类型: 只关注对象的行为,而不关注对象本身。

2.多态

多态的含义: 同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。

var makeSound = function( animal ) {
    if( animal instanceof Duck ) {
        console.log("嘎嘎嘎")
    }else if( animal instanceof Chicken ) {
        console.log("咯咯咯")
    }
};

var Duck = function() {};
var Chicken = function() {};

makeSound( new Duck() );
makeSound( new Chicken() );

多态背后的思想是将“做什么”和“谁去做以及怎样去做”分离开来,也就是将“不变的事物”与“可能改变的事务”分离开来。

“一只麻雀在飞”或“一直喜鹊在飞” ---> “一只鸟在飞”

3.封装

JavaScript只能依赖变量的作用域来实现封装特性,而且只能模拟出public和private这两种封装性。

“透明” == “不可见”

4.原型模式和基于原型继承的JavaScript对象系统

在原型编程的思想中,类并不是必须的,对象未必需要从类中创建而来,一个对象是通过克隆另外一个对象所得到的。

原型编程规范至少包括以下基本规则:

  • 所有的数据都是对象。
  • 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它。
  • 对象会记住它的原型。
  • 如果对象无法响应某个请求,它会把这个请求委托给它自己的原型。

JavaScript中的根对象是Object.prototype对象。

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

Person.prototype.getName = function() {
    return this.name;
};

var a = new Person( 'sven' );

console.log( a.name ); //输出sven
console.log( a.getName() ); //输出:sven
console.log( Object.getPrototypeOf(a) === Person.prototype ); //输出:true

在这里Person并不是类,而是函数构造器,JavaScript的函数既可以作为普通函数被调用,也可以作为构造器被调用。

就JavaScript的真正实现来说,其实并不能说对象有原型,而只能说对象的构造器有原型。对于“对象把请求委托给它自己的原型”这句话,更好的说法是对象把请求委托给他的构造器的原型。

JavaScript给对象提供了一个名为 —proto— 的隐藏属性,某个对象的 —proto— 属性默认会指向它的构造器的原型对象,即{Constructor}.prototype

可以通过Object.create( null )可以创造出没有原型的对象

第二章 this, call 和 apply

1. this的指向

JavaScriptthis总是指向一个对象,而具体指向哪个对象是在运行时基于函数的执行环境动态绑定的,而非函数被声明时的环境。

this的指向大致可以分为以下四种:

  • 作为对象的方法调用
  • 作为普通函数的调用
  • 构造器调用
  • Function.prototype.callFunction.prototype.apply调用
//修正this
document.getElementById = (function(func) {
    return function() {
        return func.apply( document, arguments );
    }
})( document.getElementById );

2. callapply

2.1 callapply的区别

Math.max.apply(null, [1, 2, 3, 4, 5])

2.2 callapply的用途

  • 改变this的指向
  • Function.prototype.bind模拟实现
Function.prototype.bind = function() {
    var self = this; // 保存原函数
        context = [].shift.call(arguments); //需要绑定的this上下文
        args = [].slice.call(arguments); // 剩余的参数转成数组
    return function() { //返回一个新的函数
        return self.apply( context, [].concat.call(args, [].slice.call(arguments)) )
        // 执行新的函数的时候,会把之前传入的context当作新韩淑体内的this
        // 并且组合两次分别传入的参数,作为新函数的参数
    }
};

3. 借用其他对象的方法

3.1 借用构造函数 --- 实现类似继承的效果

var A = function( name ) {
    this.name = name
}

var B = function() {
    A.apply( this, arguments )
}

B.prototype.getName = function() {
    return this.name
}

var b = new B('sven');
console.log( b.getName() ); //'sven'

3.2 借用Array.prototype对象上的方法

(function(){
    Array.prototype.push.call( arguments, 3 );
    console.log( arguments ); // [1,2,3]
})(1, 2)

借用Array.prototype.push方法使用需要满足两个条件:

  • 对象本身要可以存取属性(number类型不行)
  • 对象的length属性要可读写(函数不行)

第三章 闭包与高阶函数

3.1 闭包

闭包的形成与 变量的作用域 以及 变量的生存周期 密切相关。

变量的生存周期:

var func = function() {
   var a = 1; //退出函数化局部变量a将被销毁
   alert ( a );
}

func(); 
var func = function() {
   var a = 1;
   return function() {
       a++; // a会一直被保存下来
       alert ( a );
   }
}
var f = func();

f(); //输出:2
f(); //输出:3
f(); //输出:4

判断变量类型方法

var Type = {};

for( var i = 0, type; type = [ 'String', 'Array', 'Number'][ i++ ]; ){
   (function( type ){
       Type[ 'is' + type ] = function( obj ){
           return Object.prototype.toString.call( obj ) === '[object' + type + ']';
       }
   })( type )
};

Type.isArray( [] ); //输出:true
Type.isString( 'str' ); //输出:true

闭包的更多作用

  • 封装变量(把一些不需要暴露在全局的变量封装成“私有变量”)
var mult = function() {
   var a = 1;
   for ( var i=0, l=arguments.length; i<l; i++){
       a = a * arguments[i];
   }
   return a;
}

第一步 加入缓存机制之后:

var cache = {};

var mult = function() {
   var args = Array.prototype.join.call( arguments, ',' );
   if( cache[args] ){
       return cache[ args ];
   }
   var a = 1;
   for( var i=0; l=arguments.length; i<l; i++){
       a = a * arguments[i];
   }
    
   return cache[ args ] = a;
};

alert ( mult(1,2,3) ); //输出:6
alert ( mult(1,2,3) ); //输出:6, 第二次不需要重新计算

第二步 将cache变量封闭在mult函数内:

var mult = (function(){
   var cache = {};
   return function(){
       var args = Array.prototype.join.call( arguments, ',' );
       if( args in cache ){
           return cache[ args ];
       }
       var a = 1;
       for( var i=0; l=arguments.length; i<l; i++){
           a = a * arguments[i];
       }
        
       return cache[ args ] = a;
   }
})()

第三步 提炼函数

var mult = (function(){
  var cache = {};
  var calculate = function(){ //封闭calculate函数
      var a = 1;
      for( var i=0; l=arguments.length; i<l; i++){
          a = a * arguments[i];
      }
       
      return a;
  }
  
  return function(){
      var args = Array.prototype.join.call( arguments, ',' );
      if( args in cache ){
          return cache[ args ];
      }
      return cache[ args ] = calculate.apply( null, arguments ); //传入null来代表某个对象
  }
})()
  • 延续局部变量的寿命
//img 对象经常用于进行数据上报
var report = function( src ){
   var img = new Image();
   img.src = src;
};

report( "http://xxx.com" );

以上数据上报存在数据丢失的现象,丢失数据的原因是img是report函数中的局部变量,当report函数的调用结束后,img局部变量随即被销毁,此时还来不及发送http请求。使用闭包解决请求丢失问题的方法:

var report = (function(){
   var imgs = [];
   return function( src ){
       var img = new Image();
       imgs.push( img );
       img.src = src;
   }
})();

闭包和面向对象设计

对象以方法的形式包含了过程,而闭包则是在过程中以环境的形式包含了数据。

3.2 高阶函数

高阶函数需要满足的条件:

  • 函数可以作为参数传递
  • 函数可以作为返回值输出
  1. 函数作为参数传递;
  2. 函数作为返回值输出;
  3. 高阶函数实现AOP(面向切面编程);

AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计,安全控制,异常处理等。把这些功能抽离出来之后,再通过“动态织入”的方式参入业务逻辑块中。好处:1.可以保持业务逻辑模块的纯净和高内聚性;2可以很方便地复用日志统计等模块。

动态织入,Function.prototype扩展

Function.prototype.before = function( beforefn ){
    var _self = this; //保存原函数地引用
    return function() { //返回包含了原函数和新函数的“代理”函数
        before.apply( this, arguments ); //执行新函数,修正this
        return _self.apply( this, arguments ); //执行原函数
    }
}

Function.prototype.after = function( afterfn ){
    var _self = this;
    return function(){
        var ret = _self.apply( this, arguments );
        afterfn.apply( this, arguments );
        return ret;
    }
}

var func = function() {
    console.log( 2 );
}

func = func.before(function(){
    console.log( 1 );
}).after(function(){
    console.log( 3 );
})

func();

高阶函数的其他应用

1.currying(部分求值)

currying 描述: 一个currying的函数首先会接受一些函数,接受了这些寒素之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。

遍历本月每天的开销并求出他们的总和:

var currying = function( fn ){
   var args = [];
   return function(){
       if( arguments.length === 0){
           return fn.apply( this, args )
       }else{
           [].push.apply( args, arguments );
           return arguments.callee;
           //callee是对象的一个属性,可以用于引用该函数体内当前正在执行的函数
           //此属性在ES5严格模式已删除
       }
   }
}

var cost = (function(){
   var money = 0;
   return function() {
       for(var i = 0, l = arguments.length; i < l; i++){
           money += arguments[ i ];
       }
       return money;
   }
})

var cost = currying( cost ); //转化成currying函数

cost( 100 );
cost( 200 );
cost( 300 );

cost(); //600

2.uncurring(提取泛化this的过程)

鸭子类型思想:只关注对象行为,不关注对象本身。

Array.prototype上的方法可以操作任何对象。

Function.prototype.uncurrying = function () {
   var self = this; // self此时是需要提取方法的对象
   return function() {
       var obj = Array.prototype.shift.call( arguments );
       // arguments 对象的第一个元素被截去
       return self.apply( obj, arguments );
   };
};
  1. 函数节流

函数被频繁调用的场景:

  • window.onresize事件
  • mousemove事件
  • 上传进度

代码实现:

var throttle = function( fn, interval ){
    var _self = fn, //保存需要被延迟执行的函数的引用
        timer, //定时器
        firstTime = true; //是否第一次调用
    
    return function () {
        var args = arguments,
            _me = this;
        if( firstTime ){ //如果是第一次调用,不需要延迟执行
            _self.apply(_me, args);
            return firstTime = false;
        }
        if( timer ){ //如果定时器还在,说明前一次延迟执行还没有完成
            return false
        }
        timer = setTimeout(function(){
            clearTimeout(timer);
            timer = null;
            _self.apply(_me, args);
        }, interval || 500) //延迟一段时间执行
    };
};

window.onresize = throttle(function(){
    console.log( 1 );
}, 500);
  1. 分时函数

5.惰性加载函数

第二部分:设计模式

第四章 单例模式

单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

4.1 JavaScript中的单例模式

全局变量不是单例模式,但在JavaScript开发中,我们经常会把全局变量当成单例来使用。

尽量减少全局变量的使用:

  • 使用命名空间

适当的使用命名空间,并不会杜绝全局变量,但可以减少全局变量的数量。

//动态创建命名空间
var MyApp = {};
MyApp.namespace = function( name ){
    var parts = name.split( '.' );
    var current = MyApp;
    for( var i in parts ){
        if( !current [ parts[i] ] ){
            currents[ parts[ i ] ] = {};
        }
        current = currents[ parts[ i ] ];
    }
}

MyApp.namespace( 'event' );
MyApp.namespace( 'dom.style' );

console.dir( MyApp );

<=>

var MyApp = {
    event: {},
    dom: {
        style: {}
    }
}
  • 使用闭包封装私有变量
var user = (function(){
    var _name = 'sven';
        _age = 29;
    return {
        getUserInfo: function() {
            return _name + '-' _age;
        }
    }
})

4.2 惰性单例(eg: QQ登录浮窗)

惰性单例是指在需要时才创建的对象实例。

4.3 通用的惰性单例

创建对象和管理单例的职责被分布在两个不同的方法中。

// fn为创建对象方法
var getSingle = function( fn ) {
    var result;
    return function() {
        return result || ( result = fn.apply(this, arguments ) );
    }
}

使用iframe实现跨域怎么做?

第五章 策略模式

将不变的部分和变化的部分隔开是每个设计模式的主题。

策略模式的思想: 定义一系列的算法,把它们各自封装成策略类,算法被封装在策略类内部的方法里。在客户对Context发起请求的时候,Context总是把请求委托给这些策略对象中间的某一个进行计算。

使用策略模式可以消除大量得条件分支语句

5.1 策略模式版本1 (模仿传统面向对象语音):

//计算规则
var performanceS = function(){};
performanceS.prototype.calculate = function( salary ){
    return salary * 4;
}

var performanceA = function(){};
performanceS.prototype.calculate = function( salary ){
    return salary * 3;
}

var performanceB = function(){};
performanceS.prototype.calculate = function( salary ){
    return salary * 2;
}

//定义奖金类
var Bonus = function(){
    this.salary = null; //工资
    this.strategy = null; //策略对象
}

Bonus.prototype.setSalary = function( salary ){
    this.salary = salary; //设置工资
}

Bonus.prototype.setStrategy = function( strategy ){
    this.strategy = strategy; //设置策略对象
}

Bonus.prototype.getBonus = function() {
    if (!this.strategy){
        throw new Error('未设置strategy属性')
    }
    return this.strategy.calculate( this.salary ); //把计算操作委托给策略对象
}

//使用
var bonus = new Bonus();

bonus.setSalary( 1000 );
bonus.setStrategy( new performanceS() ); //设置策略对象

console.log( bonus.getBonus() ); //输出:4000

5.2 JavaScript版本的策略模式:

var strategies = {
    "S": function( salary ){
        return salary * 4;
    },
    "A": function( salary ){
        return salary * 3;
    },
    "B": function( salary ){
        return salary * 2;
    }
}

var calculateBonus = function( level, salary ){
    return strategies[ level ]( salary );
};

console.log( calculateBonus('S', 2000) ); //80000
console.log( calculateBonus('A', 1000) ); //30000

5.3 使用策略模式实现缓动动画(略):

5.4 使用策略模式实现表单验证:

i 不使用策略模式

var registerForm = document.getElementById( 'registerForm' );

registerForm.onsubmit = function() {
    if (registerForm.userName.value === ''){
        alert('用户名不能为空')
        return false
    }
    if (registerForm.password.value.length < 6){
        alert('密码长度不能少于6位')
        return false
    }
    if ( !/(^1[3|5|8|][0-9]{9}$)/.test( registerForm.phoneNumber.value ){
        alert('手机号码格式不正确')
        return false
    }
}

ii 使用策略模式

var strategies = {
    isNonEmpty: function( value, errorMsg ){ //不为空
        if( value === '' ){
            return errorMsg;
        }
    },
    minLength: function( value, length, errorMsg ){ //限制最小长度
        if( value.length < length ){
            return errorMsg;
        }
    },
    isMobile: function( value, errorMsg ){
        if ( !/(^1[3|5|8|][0-9]{9}$)/.test( value )){
            return errorMsg;
        }
    }
}

var validataFunc = function() {
    var validator = new Validator(); // 创建一个validator对象
    
    /*****添加一些校验规则*****/
    validator.add( registerForm.userName, 'isNonEmpty', '用户名不能为空');
    validator.add( registerForm.password, 'minLength:6', '密码长度不能少于6位');
    validator.add( registerForm.userName, 'isMobile', '手机号码格式不正确');
    
    var errorMsg = validator.start(); //获得校验结果
    return errorMsg; // 返回校验结果
}

var Validator = function() {
    this.cache = [];
}

Validator.prototype.add = function( dom, rule, errorMsg ) {
    var ary = rule.split( ':' ); // 把strategy和参数分开
    this.cache.push(function(){ // 把校验的步骤用空函数包装起来,并且放入cache
        var strategy = ary.shift(); // 用户挑选的Strategy
        ary.unshift( dom.value ); // 把input的value添加进参数列表
        ary.push( errorMsg ); // 把errorMsg添加进参数列表
        return strategies[ strategy ].apply( dom, ary ); 
    })
}

Validator.prototype.start = function(){
    for( var i = 0, validatorFunc; validatorFunc = this.cache[ i++ ];){
        var msg = validatorFunc(); // 开始校验,并取得校验后的返回信息
        if(msg){ 
            return msg;
        }
    }
}

拓展: 给文本框输入添加多种验证规则。

疑问:目前这样只报一个错,如何实现点击提交时,错误全部显示出来?(类似饿了么表单验证组件)

第六章 代理模式

6.1 保护代理和虚拟代理

客户 ---> 代理 ---> 本体

代理可以帮助本体过滤掉一些请求,这被称为保护代理。

把一些开销很大的对象,延迟到真正需要它的时候才由代理去创建,这被称为虚拟代理。

6.2 虚拟代理实现图片预加载

图片过大时或者网络不佳,图片的位置会出现一片空白。那么常见的做法就是先用一张loading图片占位,然后用异步的方式加载图片,等图片好了再填充到img节点里,这种场景非常适用虚拟代理。

var myImage = (function(){
    var imgNode = document.createElement( 'img' );
    document.body.appendChild( imgNode );
    
    return {
        setSrc: function( src ){
            imgNode.src = src
        }
    }
})();

var proxyImage = (function(){
    var img = new Image;
    img.onload = function(){ //监听异步加载完成
        myImage.setSrc( this.src );
    }
    return {
        setSrc: function( src ){
            myImage.setSrc( 'file:// /C:/Users/XX/XX/loading.gif' ); //先用该图片代替
            img.src = src; //开始加载图片
        }
    }
})()

proxyImage.setSrc('file:// /C:/Users/XX/XX/need.jpg'')

6.3 代理的意义

单一职责原则

6.4 代理和本体接口的一致性(见94)

6.5 虚拟代理合并HTTP请求

解决方案:可以通过一个代理函数proxySynchronousFile来收集一段hijack之内的请求,最后一次性发给服务器。

var synchronousFile = function( id ){
    console.log( '开始同步文件, id为:' + id );
};

var proxySynchronousFile = (function(){
    var cache = [], //保存一段时间内需要同步的ID
        timer; //定时器
        
    return function( id ){
        cache.push(id);
        if( timer ){ //保证不会覆盖异价启动的定时器
            return;
        }
    }
    
    timer = setTimeOut(function(){
        synchronousFile( cache.join(',') ); //2秒后向本地发送需要同步的集合
        clearTimeout( timer ); //清空定时器
        timer = null;
        cache.length = 0; //清空ID集合
    }, 2000)
})()

var checkbox = document.getElementByTagName( 'input' );
for( var i=0; c; c=checkbox[i++] ){
    c.onclick = function(){
        if( this.checked === true ){
            proxySynchronousFile(this.id)
        }
    }
}

6.6 虚拟代理在惰性加载中的应用(见p99)

6.7 缓存代理

缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时。如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果。

var mult = function() {
    var a = 1;
    for(var i=0; l=arguments.length; i++){
        a = a * arguments[i]
    }
    return a
}

//加入代理缓存
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, 4 ); //24
proxyMult( 1, 2, 3, 4 ); //24

6.8 用高阶函数动态创建代理

var mult = function() {
    var a = 1;
    for(var i=0; l=arguments.length; i++){
        a = a * arguments[i]
    }
    return a
}

var plus = function() {
    var a = 0;
    for(var i=0; l=arguments.length; i++){
        a = a + arguments[i]
    }
    return a
}

var createProxyFactory = function( fn ){
    var cache = {};
    return function(){
        var args = Array.prototype.join.call( arguments, ',' );
        if( args in cache ){
            return cache[ args ];
        }
        return cache[ args ] = fn.apply( this, arguments );
    }
}

var proxyMult = createProxyFactory( mult );
var proxyPlus = createProxyFactory( plus );

proxyMult(1,2,3,4); //24
proxyMult(1,2,3,4); //24
proxyPlus(1,2,3,4); //10
proxyPlus(1,2,3,4); //10

第七章 迭代器模式

迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,二又不需要暴露该对象的内部表示。

7.1 内部迭代器

内部迭代器在调用的时候非常方便,外界不用担心迭代器的内部实现,跟迭代器的交互也仅仅是一次初始调用,这也是内部迭代器的缺点。

var each = ...

7.2 外部迭代器

外部迭代器必须显式的请求下一个元素。

var Iterator = function( obj ){
    var current = 0;
}
...

7.3 迭代类数组对象和字面量对象

jquery中的$.each实现方法

js中的forEach,for...in for...of实现方法

7.4 应用实例(上传组件的遍历)

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

定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。

8.1 真实例子---网站登录

需要登录之后设置各模块的头像以及刷新有关模块

使用发布-订阅模式之前伪代码:

login.succ(function(data){
    header.setAvatar( data.avatar ); //设置header模块的头像
    nav.setAvatar( data.avatar ); //设置导航模块的头像
    message.refresh(); //刷新消息列表
    cart.refresh(); //刷新购物车列表
    address.refresh(); //增加刷新收货地址列表(这是一种令人抓狂的添加方法)
})

使用发布-订阅模式之后伪代码:

$.ajax( 'http://xxx.com?login', function(data){ 
    login.trigger('loginSucc', data); //登录成功发布登录成功的消息
})

//header模块监听发布对象设置头像
var header = (function(){
    login.listen( 'loginSucc', function( data ){
        header.setAvatar( data.avatar )
    });
    return {
        setAvatar: function( data ){
            console.log('设置header模块的头像')
        }
    }
})

//navr模块监听发布对象设置头像
(...)

//监听发布对象刷新收货地址列表
var address = (function(){
    login.listen( 'loginSucc', function( data ){
        header.setAvatar( data.avatar )
    });
    return {
        refresh: function( data ) {
            console.log('刷新收货地址列表')
        }
    }
})

8.2 全局的发布-订阅对象(‘中介’)

8.3 模块间通信