JavaScript常用的设计模式

104 阅读8分钟

原型模式

场景:根据对象来复制对象。

原型模式实现关键是语言是否提供了clone方法。ES5提供了Object.create方法,可以用来克隆对象。不支持create的浏览器可以使用如下代码:

Object.create = Object.create || function (obj) {

var F = function () {}

F.prototype = obj;

return new F();

}

当然在 JavaScript这种类型模糊的语言中,创建对象非常容易,也不存在类型耦合的问题。 从设计模式的角度来讲,原型模式的意义并不算大 。但 JavaScript本身是一门基于原型的面向对 象语言,它的对象系统就是使用原型模式来搭建的,在这里称之为原型编程范型也许更合适。

原型编程范型的一些规则

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

JavaScript是如何在这些规则中构造对象系统的?

  1. 所有数据都是对象?

    JavaScript在设计的时候,模仿 Java 引入了两套类型机制:基本类型和对象类型。基本类型 包括 undefined 、 number 、 boolean 、 string 、 function 、 object 。

    JavaScript中所有对象的跟对象就是Object.prototype对象。但在 JavaScript语言里,我们并不需要关心克隆的细节,因为这是引擎内部负责实现的。我们所需要做的只是显式地调用 var obj1 = new Object() 或者 var obj2 = {} 。此时,引擎内部会从Object.prototype 上面克隆一个对象出来,我们最终得到的就是这个对象。

    当使用 new 运算符来调用函数时,此时的函数就是一个构造器。 用new 运算符来创建对象的过程,实际上也只是先克隆 Object.prototype 对象,再进行一些其他额外操作的过程。

    可以利用 ECMAScript 5提供的 Object.getPrototypeOf 来查看对象的原型.

  2. 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它

    在 Chrome和 Firefox等向外暴露了对象 proto 属性的浏览器下,我们可以通过下面这段代码来理解 new 运算的过程:

    function(){
        return this.name;
    };
    var objectFactory = function(){
    var obj = new Object(), // 从 Object.prototype 上克隆一个空的对象
    Constructor = [].shift.call( arguments ); // 取得外部传入的构造器,此例是 Person
    obj.__proto__ = Constructor.prototype; // 指向正确的原型
    var ret = Constructor.apply( obj, arguments ); // 借用外部传入的构造器给 obj 设置属性
    return typeof ret === 'object' ? ret : obj; // 确保构造器总是会返回一个对象
    };
    var a = objectFactory( Person, 'sven' );
    console.log( a.name ); // 输出:sven
    console.log( a.getName() ); // 输出:sven
    console.log( Object.getPrototypeOf( a ) === Person.prototype ); // 输出:true
    
  3. 对象会记住它的原型

    目前我们一直在讨论“对象的原型”,就 JavaScript 的真正实现来说,其实并不能说对象有原型,而只能说对象的构造器有原型。对于 “对象把请求委托给它自己的原型” 这句话,更好的说法是对象把请求委托给它的构造器的原型。那么对象如何把请求顺利地转交给它的构造器的原型呢?

    JavaScript 给对象提供了一个名为 proto 的隐藏属性,某个对象的 proto 属性默认会指 向它的构造器的原型对象,即 {Constructor}.prototype 。在一些浏览器中, proto 被公开出来, 实际上, proto就是对象跟“对象构造器的原型”联系起来的纽带.

  4. 如果对象无法响应某个请求,它会把这个请求委托给它的构造器的原型

    在 JavaScript 中,每个对象都是从 Object.prototype 对象克隆而来的,如果是这样的话,我们只能得到单一的继承关系,即每个对象都继承自 Object.prototype 对象,这样的对象系统显然是非常受限的。

    实际上,虽然 JavaScript 的对象最初都是由 Object.prototype 对象克隆而来的,但对象构造器的原型并不仅限于 Object.prototype 上,而是可以动态指向其他对象。这样一来,当对象 a 需要借用对象 b 的能力时,可以有选择性地把对象 a 的构造器的原型指向对象 b ,从而达到继承的效果。下面的代码是我们最常用的原型继承方式:

    var obj = { name: 'sven' };
    var A = function(){};
    A.prototype = obj;
    var a = new A();
    console.log( a.name ); // 输出:sven
    

    当我们期望得到一个 “类”继承自另外一个“类” 的效果时,往往会用下面的代码来模拟实现:

    var A = function(){};
    A.prototype = { name: 'sven' };
    var B = function(){};
    B.prototype = new A();
    var b = new B();
    console.log( b.name ); // 输出:sven
    

JavaScript中函数的this指向

当使用 call 或者 apply 的时候,如果我们传入的第一个参数为 null ,函数体内的 this 会指 向默认的宿主对象,在浏览器中则是 window.

var func = function( a, b, c ){
    alert ( this === window ); // 输出 true
};
func.apply( null, [ 1, 2, 3 ] );
// 但如果是在严格模式下,函数体内的 this 还是为 null :
var func = function( a, b, c ){
    "use strict";
    alert ( this === null ); // 输出 true
}
func.apply( null, [ 1, 2, 3 ] );

有时候我们使用 call 或者 apply 的目的不在于指定 this 指向,而是另有用途,比如借用其 他对象的方法。那么我们可以传入 null 来代替某个具体的对象:

Math.max.apply( null, [ 1, 2, 5, 3, 4 ] ) // 输出:5

call和apply在实际开发中的用途

  • 改变this指向

    在实际开发中,经常会遇到 this 指向被不经意改变的场景,比如有一个 div 节点, div 节点的 onclick 事件中的 this 本来是指向这个 div 的:

     document.getElementById( 'div1' ).onclick = function(){
        alert( this.id ); // 输出:div1
    };
    

    假如该事件函数中有一个内部函数 func ,在事件内部调用 func 函数时, func 函数体内的 this 就指向了 window ,而不是我们预期的 div ,见如下代码:

    document.getElementById( 'div1' ).onclick = function(){
        alert( this.id ); // 输出:div1
        var func = function(){
            alert ( this.id ); // 输出:undefined
        }
        func();
    };
    

    这时候我们用 call 来修正 func 函数内的 this ,使其依然指向 div :

    document.getElementById( 'div1' ).onclick = function(){
        var func = function(){
            alert ( this.id ); // 输出:div1
        }
        func.call( this );
    };
    
  • Function.prototype.bind

    大部分高级浏览器都实现了内置的 Function.prototype.bind ,用来指定函数内部的 this 指向,即使没有原生的 Function.prototype.bind 实现,我们来模拟一个也不是难事.

    Function.prototype.bind = function( context ){
        var self = this; // 保存原函数
        return function(){ // 返回一个新的函数
            return self.apply( context, arguments ); // 执行新的函数的时候,会把之前传入的 context
        // 当作新函数体内的 this
        }
    };
    var obj = {
        name: 'sven'
    };
    var func = function(){
        alert ( this.name ); // 输出:sven
    }.bind( obj);
    func();
    
  • 借用其他对象的方法

    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'
      
    2. 借用内置对象的方法,比如arguments对象借用数组的方法,如下:

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

      在操作 arguments 的时候,我们经常非常频繁地找 Array.prototype 对象借用方法。想把 arguments 转成真正的数组的时候,可以借用 Array.prototype.slice 方法;想截去arguments 列表中的头一个元素时,又可以借用 Array.prototype.shift 方法。

      Array.prototype.push 实际上是一个属性复制的过程,把参数按照下标依次添加到被 push 的对象上面,顺便修改了这个对象的 length 属性。至于被修改的对象是谁,到底是数组还是类数组对象,这一点并不重要。

      由此可以推断,我们可以把“任意”对象传入 Array.prototype.pushvar a = {};
      Array.prototype.push.call( a, 'first' );
      alert ( a.length ); // 输出:1
      alert ( a[ 0 ] ); // first
      

      前面我们之所以把“任意”两字加了双引号,是因为可以借用 Array.prototype.push 方法的对 象还要满足以下两个条件,从 ArrayPush 函数的(1)处和(2)处也可以猜到,这个对象至少还要满足:

      • 对象本身要可以存取属性;
      • 对象的 length 属性可读写。

高阶函数应用

单例模式

var getSingle = function (fn) {
	var ret;
	return function() {
		return ret || (ret = fn.apply(this,arguments))
	}
}

高阶函数实现AOP

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

在 Java 语言中,可以通过反射和动态代理机制来实现 AOP 技术。而在 JavaScript 这种动态语言中,AOP的实现更加简单,这是 JavaScript与生俱来的能力。通常,在 JavaScript中实现 AOP,都是指把一个函数“动态织入”到另外一个函数之中,具体的实现技术有很多。

Function.prototype.before = function( beforefn ){
	var __self = this; // 保存原函数的引用
	return function(){ // 返回包含了原函数和新函数的"代理"函数
    	beforefn.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();

\