原型模式
场景:根据对象来复制对象。
原型模式实现关键是语言是否提供了clone方法。ES5提供了Object.create方法,可以用来克隆对象。不支持create的浏览器可以使用如下代码:
Object.create = Object.create || function (obj) {
var F = function () {}
F.prototype = obj;
return new F();
}
当然在 JavaScript这种类型模糊的语言中,创建对象非常容易,也不存在类型耦合的问题。 从设计模式的角度来讲,原型模式的意义并不算大 。但 JavaScript本身是一门基于原型的面向对 象语言,它的对象系统就是使用原型模式来搭建的,在这里称之为原型编程范型也许更合适。
原型编程范型的一些规则
- 所有的数据都是对象。
- 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它。
- 对象会记住它的原型。
- 如果对象无法响应某个请求,它会把这个请求委托给它自己的原型。
JavaScript是如何在这些规则中构造对象系统的?
-
所有数据都是对象?
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 来查看对象的原型.
-
要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它
在 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 -
对象会记住它的原型
目前我们一直在讨论“对象的原型”,就 JavaScript 的真正实现来说,其实并不能说对象有原型,而只能说对象的构造器有原型。对于 “对象把请求委托给它自己的原型” 这句话,更好的说法是对象把请求委托给它的构造器的原型。那么对象如何把请求顺利地转交给它的构造器的原型呢?
JavaScript 给对象提供了一个名为 proto 的隐藏属性,某个对象的 proto 属性默认会指 向它的构造器的原型对象,即 {Constructor}.prototype 。在一些浏览器中, proto 被公开出来, 实际上, proto就是对象跟“对象构造器的原型”联系起来的纽带.
-
如果对象无法响应某个请求,它会把这个请求委托给它的构造器的原型
在 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(); -
借用其他对象的方法
-
借用方法的第一种场景是“借用构造函数”,通过这种技术,可以实现一些类似继承的效果:
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' -
借用内置对象的方法,比如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.push : var 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();
\