基础知识
一、原型模式
原型模式的实现的关键即克隆。
var plane =new Plane();var clonePlane = Object.create(plane);Object.create = Object.create || function (obj) {var F = function () {};F.prototype = obj;return new F();
要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它。
对象会记住它的原型。
如果对象无法响应某个请求,它会把这个请求委托给它自己的原型。
new Object & Object.create
- 创建对象的方式不同 ```javascript // new Object() 方式创建 var a = { rep : ‘apple’ } var b = new Object(a) console.log(b) // {rep: “apple”} console.log(b.proto) // {} console.log(b.rep) // {rep: “apple”}
// Object.create() 方式创建 var a = { rep: ‘apple’ } var b = Object.create(a) console.log(b) // {} console.log(b.proto) // {rep: “apple”} console.log(b.rep) // {rep: “apple”}
2. **创建对象属性的性质不同**`var o = Object.create({},{p:{value: 42}});`<br />_**p后跟的对象为p的属性描述,Object.create() 用第二个参数来创建非空对象的属性描述符默认是为false的,**_<br />_**而构造函数或字面量方法创建的对象属性的描述符默认为true。**_<br />**打印结果 --> {p: 42}**<br />_**所以属性p是不可写,不可枚举,不可配置的。**_<br />_****_3. **创建空对象时不同**<a name="Sg9Yy"></a>## [ ].slice.call(arguments) & [ ].shift.call(arguments);<br />**理解:**<br />_**slice 方法原理就是根据传入的参数(值)对原数组(或者类数组)进行遍历获取,赋给新数组然后返回。**_<br />_**如果没有参数便复制整个原数组(或者类数组),后赋给新数组然后返回。**_<br />_<br />_**因为slice内部实现是使用的this代表调用对象。那么当[].slice.call() 传入 arguments对象的时候,通过 call函数改变原来 slice方法的this指向, 使其指向arguments,并对arguments进行复制操作,而后返回一个新数组。**_<br />_**至此便是完成了arguments类数组转为数组的目的!**_<br />_<br />_**前面需要一个[]是用来调用方法。**_<br />_**除了slice还有其他的数组方法也能借用来遍历,获取新的结果。**_```javascript***计算对象中某个属性的数值总和***function countResources() {return Array.prototype.reduce.call($scope.resourcesList, function (prev, curr) {return prev + curr.resources.length;}, 0);}//prev是上次是结果的返回值,0是initialValue。若initialvalue为10,则最终结果会+10;***数组降维***var flattened = [[0, 1], [2, 3], [4, 5]].reduce(function(a, b) {return a.concat(b);}, []);// flattened is [0, 1, 2, 3, 4, 5]***按属性对Object分类***var people = [{ name: 'Alice', age: 21 },{ name: 'Max', age: 20 },{ name: 'Jane', age: 20 }];function groupBy(objectArray, property) {return objectArray.reduce(function (acc, obj) {var key = obj[property];if (!acc[key]) {acc[key] = [];}acc[key].push(obj);return acc;}, {});}var groupedPeople = groupBy(people, 'age');// groupedPeople is:// {// 20: [// { name: 'Max', age: 20 },// { name: 'Jane', age: 20 }// ],// 21: [{ name: 'Alice', age: 21 }]// }***将对象中某个属性的值整合在一起***var allbooks = friends.reduce(function(prev, curr) {return [...prev, ...curr.books];}, ['Alphabet']);// allbooks = [// 'Alphabet', 'Bible', 'Harry Potter', 'War and peace',// 'Romeo and Juliet', 'The Lord of the Rings',// 'The Shining'// ]***ES5使用reduce去重***let arr = [1,2,1,2,3,5,4,5,3,4,4,4,4];let result = arr.sort().reduce((init, current) => {if(init.length === 0 || init[init.length-1] !== current) {init.push(current);}return init;}, []);console.log(result); //[1,2,3,4,5]
forEach filter every some map等方法对空位的处理
tips:
- 遍历var a = [1,2,,3]时,
- forEach( ), filter( ), every( ) 和some( )都会跳过空位。
- map()会跳过空位,但会保留这个值。
- join( )和toString( )会将空位视为undefined,而undefined和null会被处理成空字符串。
- entries( )、keys( )、values( )、find( )和findIndex( )会将空位处理成undefined: ```javascript // forEach方法,跳过空位,无返回值,改变数组。 let arr = [, , 12, 2, 20, -1, , 17]; arr.forEach((item, index, array) => { array[index] = item +2; }); console.log(arr); // [empty × 2, 14, 4, 22, 1, empty, 19]
// filter方法,跳过空位,返回新数组。 [‘a’,,’b’].filter(x => true) // [‘a’,’b’]
// every方法,跳过空位,返回结果(布尔值)。 [,’a’].every(x => x===’a’) // true
// some方法,跳过空位,返回结果(布尔值)。 [,’a’].some(x => x !== ‘a’) // false
// map方法,跳过空位,返回新数组。 [,’a’].map(x => 1) // [,1]
// join方法 [,’a’,undefined,null].join(‘#’) // “#a##”
// toString方法 [,’a’,undefined,null].toString() // “,a,,”
// Array.from方法会将数组的空位转为undefined Array.from([‘a’,,’b’]) // [ “a”, undefined, “b” ]
// 扩展运算符(…)也会将空位转为undefined […[‘a’,,’b’]] // [ “a”, undefined, “b” ]
// for…of循环也会遍历空位
// entries() […[,’a’].entries()] // [[0,undefined], [1,”a”]]
// keys() […[,’a’].keys()] // [0,1]
// values() […[,’a’].values()] // [undefined,”a”]
// find() [,’a’].find(x => true) // undefined,return true时返回当前item。
// findIndex() [,’a’].findIndex(x => true) // 0,return true时返回当前的index。
_**map会跳过空位,但Array.from会将空位转为undefined,所以Array.from中的map不会跳过空位。**_```javascriptvar arr = [,1,,2];var b = arr.map(function(item,index) {return index;});var c = Array.from(new Array(100),function(item,index) {return index;})console.log(arr,b,c);// [empty, 1, empty, 2]// [empty, 1, empty, 3]// (100) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99]***附加内容******Array.from结合Set可实现去重***let arr = Array.from(new Set([1,2,3,1,2,3,4,5,4,3]));console.log(arr); // [ 1, 2, 3, 4, 5 ]
二、this、call、apply
this
this总是指向一个对象,具体指向哪个对象是在运行时基于函数的执行环境动态绑定的,
而非函数被声明时的环境。
this的指向
- 作为对象的方法调用。
- 作为普通函数调用。
- 构造器调用。
- Function.prototype.call 或 Function.prototype.apply 调用。
作为对象的方法调用。
P25
当函数作为对象的方法被调用时,this 指向该对象:调用时是谁的方法this就指向谁。
->obj.getname(); // a,指向objobj.b.getBname(); // a2,指向obj.bvar d = obj.getname;d();//此时作为普通函数调用,指向window
局部方法callback被作为普通函数调用时,内部指向window:
解决方法:
TIP:在严格模式下,上述情况调用时this不指向window,而是undefined。
构造器调用
当用 new 运算符调用函数时,该函数总会返回一个对象,通常情况下,构造器里的 this 就指向返回的这个对象。
function fun() {this.a='a1';this.getA = function () {var that =this;this.b='a2';console.log(this,that);}}var obj=new fun();obj.getA();//this皆指向fun()返回的值,即fun {a: "a1", b: "a2", getA: ƒ}
但用 new 调用构造器时,还要注意一个问题,如果构造器显式地返回了一个 object 类型的对象,
那么此次运算结果最终会返回这个对象,而不是我们之前期待的 this:
BUT如果return的对象里有使用this,那么仍然会返回期待的this
var MyClass = function () {this.name = 'sven';return { // 显式地返回一个对象name: this.name}};var obj = new MyClass();console.log(obj.name,obj); // sven {name: "sven"}
而且,若返回的为非对象类型的数据,就不会造成上述的this指向问题:
Function.prototype.call 或 Function.prototype.apply 调用
var obj1 = {name: 'sven',getName: function(){return this.name;}};var obj2 = {name: 'anne'};console.log( obj1.getName() ); // 输出: svenconsole.log( obj1.getName.call( obj2 ) ); // 输出:anne
丢失的this
var obj = {myName: 'sven',getName: function(){return this.myName;}};console.log( obj.getName() ); // this指向obj,输出:'sven'var getName2 = obj.getName;console.log( getName2() ); // this指向window,输出:undefined
call & apply
是Function的方法
fun.apply(obj,arguments);将arguments(数组)作为参数传入fun中。
fun.call(obj/null,1,2,3);fun.apply(obj/null,[1,2,3]);

修正document.getElementById内部丢失的this:
第一个参数为null时,this会指向默认宿主对象,浏览器中为null,在严格模式下,参数为null则this为null。
为null是表示使用不在于this指向,而是借用方法并且给函数传参。比如:
// 传参方式不同,apply第二个参数可传数组or类数组Math.max(1,2,3);//3Math.max.apply(null,[1,2,3]);//3,这种使用方式和this无关,[1,2,3]是传1,2,3为参数给Math.max实质函数。
实现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();
拓展:复杂一点的bind实现
借用其他对象的方法
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'(function(){Array.prototype.push.call( arguments, 3 );console.log ( arguments ); // 输出[1,2,3]})( 1, 2 );
借用Array.prototype.push等类似方法时有以下条件: 对象本身要可以存取属性;//var a = 1; Array.prototype.push.call( a, 'first' ); 不行 对象的 length 属性可写。//a为function也不行。
我们经常借用Array.prototype对象借用方法:
- 想把 arguments 转成真正的数组的时候,可以借用 Array.prototype.slice 方法;
- 想截去arguments 列表中的头一个元素时,又可以借用 Array.prototype.shift 方法。(返回头元素,原数组丢失头元素)
三、闭包和高阶函数
闭包
p38 ```javascript var func = function(){ var a = 1; // 退出函数后局部变量 a 将被销毁 alert ( a ); }; func(); //对于全局变量来说,全局变量的生存周期当然是永久的,除非我们主动销毁这个全局变量。
//对于在函数内用 var 关键字声明的局部变量来说,当退出函数时, //这些局部变量即失去了它们的价值,它们都会随着函数调用的结束而被销毁。
var func = function(){ var a = 1; return function(){ a++; alert ( a ); } }; var f = func(); f(); // 输出:2 f(); // 输出:3 f(); // 输出:4 f(); // 输出:5 当执行 var f = func();时,f 返回了一个匿名函数的引用, 它可以访问到 func()被调用时产生的环境,而局部变量 a 一直处在这个环境里。 *既然局部变量所在的环境还能被外界访问,这个局部变量就有了不被销毁的理由。
```javascriptfor ( var i = 0, len = nodes.length; i < len; i++ ){(function( i ){nodes[ i ].onclick = function(){console.log(i);}})( i )};

注:
var a = [1,2,3];Object.prototype.toString(a); // "[object Object]"Object.prototype.toString.call(a); // "[object Array]"
封装变量
var mult = (function () {var args = {};var calculate = function () {var a = 1;for (var i = 0; i < arguments.length; i++) {a = a * arguments[i];}console.log('a1', a)return a;}//calculate为提炼出来的代码,因为没必要放到return里去计算。return function () {var arg = Array.prototype.join.call(arguments, ',');if (arg in args) {return args[arg];//return里有args,延续了args的生命,使退出mult后args也不会消失。//此处若不使用闭包,那么否则args将和mult一样暴露在全局作用于下。}return args[arg] = calculate.apply(null, arguments);//此处若不使用apply,返回的是NaN。}})()mult(1, 2, 3, 4, 5);//a1 120//120mult(1, 2, 3, 4, 5);//120
延续局部变量寿命
report 函数的调用结束后,img 局部变量随即被销毁,而此时或许还没来得及发出 HTTP 请求,所以此次请求就会丢失掉。
var report = function( src ){var img = new Image();img.src = src;};report( 'http://xxx.com/getUserInfo' );
现在我们把 img 变量用闭包封闭起来,便能解决请求丢失的问题:
var report = (function(){var imgs = [];return function( src ){var img = new Image();imgs.push( img );img.src = src;}})();
闭包和面向对象设计
var extent = function () {var value = 0;return {call: function () {value++;console.log(value);}}};var extent = extent();extent.call(); // 输出:1extent.call(); // 输出:2extent.call(); // 输出:3
如果换成面向对象的写法, 就是:
var extent = {value: 0,call: function () {this.value++;console.log(this.value);}};extent.call(); // 输出:1extent.call(); // 输出:2extent.call(); // 输出:3
用闭包实现命令模式
<button id="execute">点击我执行命令</button><button id="undo">点击我执行命令</button>
var Tv = {open: function(){console.log( '打开电视机' );}};var OpenTvCommand = function( receiver ){this.receiver = receiver;};OpenTvCommand.prototype.execute = function(){this.receiver.open(); // 执行命令,打开电视机};var setCommand = function( command ){document.getElementById( 'execute' ).onclick = function(){command.execute(); // 输出:打开电视机}};setCommand( new OpenTvCommand( Tv ) );// new OpenTvCommand( Tv )为Object,// 有var b = new OpenTvCommand( Tv ),// setCommand(b),b.execute = function(){ this.receiver.open(); };
在面向对象版本的命令模式中,预先植入的命令接收者被当成对象的属性保存起来;
而在闭包版本的命令模式中,命令接收者会被封闭在闭包形成的环境中,代码如下:
var Tv = {open: function(){console.log( '打开电视机' );},close: function(){console.log( '关上电视机' );}};var createCommand = function( receiver ){var execute = function(){return receiver.open(); // 执行命令,打开电视机}var undo = function(){return receiver.close(); // 执行命令,关闭电视机}return {execute: execute,undo: undo}};var setCommand = function( command ){document.getElementById( 'execute' ).onclick = function(){command.execute(); // 输出:打开电视机}document.getElementById( 'undo' ).onclick = function(){command.undo(); // 输出:关闭电视机}};setCommand( createCommand( Tv ) );
闭包和内存管理
要解决循环引用带来的内存泄露问题,我们只需要把循环引用中的变量设为 null即可。
将变量设置为 null 意味着切断变量与它此前引用的值之间的连接。
当垃圾收集器下次运行时,就会删除这些值并回收它们占用的内存
高阶函数
高阶函数是指至少满足下列条件之一的函数。
函数可以作为参数被传递;
函数可以作为返回值输出。
函数可以作为参数被传递
1.回调函数
我们想在 ajax 请求返回之后做一些事情,但又并不知道请求返回的确切时间时,
最常见的方案就是把 callback 函数当作参数传入发起 ajax 请求的方法中,待请求完成之后执行 callback 函数。
var getUserInfo = function( userId, callback ){$.ajax( 'http://xxx.com/getUserInfo?' + userId, function( data ){if ( typeof callback === 'function' ){callback( data );}});}getUserInfo( 13157, function( data ){alert ( data.userName );});
2.Array.prototype.sort
//从大到小排列[ 1, 4, 3 ].sort( function( a, b ){return b - a;});// 输出: [ 4, 3, 1 ]
函数作为返回值输出
判断数据的类型
var Type = {};for(var i = 0,type; type = ['String','Number','Array'][i++];){(function(type){Type['is'+type] = function(obj){return Object.prototype.toString.call(obj) === '[object '+type+']'}})(type)}Type.isArray( [] ); // 输出:trueType.isString( "str" ); // 输出:true
高阶函数的其他应用
1.currying
currying 又称部分求值。一个 currying 的函数首先会接受一些参数,接受了这些参数之后,
该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。
待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。
var cost = (function(){var args = [];return function(){if(arguments.length === 0) {var sum = 0;for(var i = 0, l = args.length; i < l; i++) {sum += args[i]}return sum;} else {Array.prototype.push.apply(args,arguments);}}})()cost( 100 ); // 未真正求值cost( 200 ); // 未真正求值cost( 300 ); // 未真正求值console.log( cost() ); // 求值并输出:600
写一个能curring化函数的函数
var currying = function( fn ){var args = [];return function(){if ( arguments.length === 0 ){return fn.apply( this, args ); //this写成null结果一样,但不确定有什么别的差别。}else{[].push.apply( args, arguments );return arguments.callee; //返回了return后面的函数。}}};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 ); // 未真正求值,返回return后面的f()cost( 200 ); // 未真正求值cost( 300 ); // 未真正求值alert ( cost() ); // 求值并输出:600
2.uncurrying
for ( var i = 0, fn, ary = [ 'push', 'shift', 'forEach' ]; fn = ary[ i++ ]; ){Array[ fn ] = Array.prototype[ fn ].uncurrying();};var obj = {"length": 3,"0": 1,"1": 2,"2": 3};Array.push( obj, 4 ); // 向对象中添加一个元素console.log( obj.length ); // 输出:4var first = Array.shift( obj ); // 截取第一个元素console.log( first ); // 输出:1console.log( obj ); // 输出:{0: 2, 1: 3, 2: 4, length: 3}Array.forEach( obj, function( i, n ){console.log( n ); // 分别输出:0, 1, 2});
3.函数节流
(1)场景
- window.onresize 事件。
- mousemove 事件。
- 上传进度。
- ……频繁触发的事件
(2)原理
按时间段来忽略掉一些事件请求
(3)实现
function throttle (fn,interval) {const __self = fn,interVal = interval;// 不变的定值,使用const保存在闭包中。let timer,firstTime = true;// timer未定义,firstTime之后会变值,这俩用let。return function(){const __me = this,args = arguments;if(firstTime) {__self.apply(__me,args);// __me指向window,args为resize这个event,这句目的是调用__self,此处直接__self()效果相同。return firstTime = false;// 将firstTime设置为false,且使用return来阻断代码下行。}if(timer) {return false;// 如果设置了timer且还未结束,那么return false来阻止触发新setTimeout。}timer = setTimeout(function() {clearTimeout(timer);timer = null;// 定义timer需先清除timer。__self.apply(__me,args);},interval || 500);}}// 调用window.onresize = throttle(function(){console.log(1);},500);//调用后形同window.onresize = function(){} //throttle return回来的函数
4.分时函数
把 1 秒钟创建 1000 个节点,改为每隔 200 毫秒创建 8 个节点。
const timeChunk = function(arr,fn,limit) {let t,obj;let start = function(){for(var i = 0; i < Math.min(limit || 1, arr.length); i++){var obj = arr.shift();fn(obj);}}return function(){t = setInterval(function(){if(arr.length === 0) { // 如果全部节点都已经被创建好return clearInterval(t);}start();},200);}}// 调用let i = 0,arr = [];for (i; i < 1000; i++) {arr.push(i);}const render = timeChunk(arr,function(n){let newDiv = document.createElement('div');newDiv.innerHTML = n;document.body.appendChild(newDiv);},8);render();
5.惰性加载函数
普通写法,每次被调用的时候都会执行里面的 if 条件分支。
var addEvent = function( elem, type, handler ){if ( window.addEventListener ){return elem.addEventListener( type, handler, false );}if ( window.attachEvent ){return elem.attachEvent( 'on' + type, handler );}};
我们把嗅探浏览器的操作提前到代码加载的时候,在代码加载的时候就立刻进行一次判断,以便让 addEvent 返回一个包裹了正确逻辑的函数。
var addEvent = (function(){if ( window.addEventListener ){return function( elem, type, handler ){elem.addEventListener( type, handler, false );}}if ( window.attachEvent ){return function( elem, type, handler ){elem.attachEvent( 'on' + type, handler );}}})();
重写addEvent
var addEvent = function( elem, type, handler ){if ( window.addEventListener ){addEvent = function( elem, type, handler ){elem.addEventListener( type, handler, false );}}else if ( window.attachEvent ){addEvent = function( elem, type, handler ){elem.attachEvent( 'on' + type, handler );}}
设计模式
四、单例模式
用一个变量来标志当前是否已经为某个类创建过对象,
单例模式的核心是确保只有一个实例,并提供全局访问。
如果是,则在下一次获取该类的实例时,直接返回之前创建的对象。实现单例模式
```javascript var Singleton = function( name ){ this.name = name; this.instance = null; }; Singleton.prototype.getName = function(){ alert ( this.name ); }; Singleton.getInstance = function( name ){ if ( !this.instance ){ this.instance = new Singleton( name ); console.log(‘this’,this); // (1) } return this.instance; }; console.dir(Singleton); // (2) var a = Singleton.getInstance( ‘sven1’ ); var b = Singleton.getInstance( ‘sven2’ ); alert ( a === b ); // true
(1)console.log('this',this);<br /><br />(2)console.dir(Singleton);<br /><a name="DsVCQ"></a>## 透明的单例```javascriptvar CreateDiv = (function(){var instance;var CreateDiv = function( html ){if ( instance ){return instance;}this.html = html;this.init();console.log('this',this); // (1return instance = this;};CreateDiv.prototype.init = function(){var div = document.createElement( 'div' );div.innerHTML = this.html;document.body.appendChild( div );};return CreateDiv;})();var a = new CreateDiv( 'sven1' );var b = new CreateDiv( 'sven2' );alert ( a === b ); // true
用代理实现单例模式
var CreateDiv = function( html ){this.html = html;this.init();};CreateDiv.prototype.init = function(){var div = document.createElement( 'div' );div.innerHTML = this.html;document.body.appendChild( div );};var proxySingletonCreateDiv = (function(){var instance;return function( html ){if ( !instance ){instance = new CreateDiv( html );}return instance;}})();var a = new ProxySingletonCreateDiv( 'sven1' );var b = new ProxySingletonCreateDiv( 'sven2' );alert ( a === b );
JavaScript中的单例
在 JavaScript 开发中,我们经常会把全局变量当成单例来使用。var a = {};
当用这种方式创建对象 a 时,对象 a 确实是独一无二的。
如果 a 变量被声明在全局作用域下,则我们可以在代码中的任何位置使用这个变量,全局变量提供给全局访问是理所当然的。这样就满足了单例模式的两个条件。
我们有必要尽量减少全局变量的使用
1.使用命名空间减少全局变量
var namespace1 = {a: function(){alert (1);},b: function(){alert (2);}};
var MyApp = {};
MyApp.namespace = function( name ){
var parts = name.split( '.' );
var current = MyApp;
for ( var i in parts ){
if ( !current[ parts[ i ] ] ){
current[ parts[ i ] ] = {};
}
current = current[ parts[ i ] ]; console.log(current)}
};
MyApp.namespace( 'dom.style' );
console.dir( MyApp );
// 上述代码等价于:
var MyApp = {
event: {},
dom: {
style: {}
}
};
// current 被置为不同的值来实现嵌套
2.使用闭包封装私有变量
把一些变量封装在闭包的内部,只暴露一些接口跟外界通信:
var user = (function(){
var __name = 'sven',
__age = 29;
return {
getUserInfo: function(){
return __name + '-' + __age;
}
}
})();
惰性单例
var createLoginLayer = (function(){
var div;
return function(){
if ( !div ){
div = document.createElement( 'div' );
div.innerHTML = '我是登录浮窗';
div.style.display = 'none';
document.body.appendChild( div );
}
return div;
}
})();
document.getElementById( 'loginBtn' ).onclick = function(){
var loginLayer = createLoginLayer();
loginLayer.style.display = 'block';
};
// 只在点击登陆时创建div,节省不必要的dom节点。
// 单例,避免频繁地创建和删除节点。
不足:创建和管理的逻辑都在createLoginLayer函数中,可以进一步拆分职责。
通用的惰性单例
var getSingle = function( fn ){
var result;
return function(){
return result || ( result = fn .apply(this, arguments ) );
}
};
var createLoginLayer = function(){
var div = document.createElement( 'div' );
div.innerHTML = '我是登录浮窗';
div.style.display = 'none';
document.body.appendChild( div );
return div;
};
var createSingleLoginLayer = getSingle( createLoginLayer );
document.getElementById( 'loginBtn' ).onclick = function(){
var loginLayer = createSingleLoginLayer();
loginLayer.style.display = 'block';
};
五、策略模式
策略模式的定义是:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
使用策略模式计算奖金
写一个名为 calculateBonus 的函数来计算每个人的奖金数额,员工的工资数额和他的绩效考核等级相关。
var calculateBonus = function( performanceLevel, salary ){
if ( performanceLevel === 'S' ){
return salary * 4;
}
if ( performanceLevel === 'A' ){
return salary * 3;
}
if ( performanceLevel === 'B' ){
return salary * 2;
}
};
calculateBonus( 'B', 20000 ); // 输出:40000
calculateBonus( 'S', 6000 ); // 输出:24000
此段代码简单但缺乏弹性
使用策略模式重构代码
var performanceS = function(){};
performanceS.prototype.calculate = function( salary ){
return salary * 4;
};
var performanceA = function(){};
performanceA.prototype.calculate = function( salary ){
return salary * 3;
};
var performanceB = function(){};
performanceB.prototype.calculate = function( salary ){
return salary * 2;
};
接下来定义奖金类 Bonus:
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(){ // 取得奖金数额
return this.strategy.calculate( this.salary ); // 把计算奖金的操作委托给对应的策略对象
};
var bonus = new Bonus();
bonus.setSalary( 10000 );
bonus.setStrategy( new performanceS() ); // 设置策略对象
console.log( bonus.getBonus() ); // 输出:40000
bonus.setStrategy( new performanceA() ); // 设置策略对象
console.log( bonus.getBonus() ); // 输出:30000
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', 20000 ) ); // 输出:80000
console.log( calculateBonus( 'A', 10000 ) ); // 输出:30000
eg:给某个文本输入框添加多种校验规则
书上例子:往Cache中push校验规则,在start中逐个运行。
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 Validator = function () {
this.cache = [];
};
Validator.prototype.add = function (dom, rules) {
var self = this;
for (var i = 0, rule; rule = rules[i++];) {
(function (rule) {
var strategyAry = rule.strategy.split(':');
var errorMsg = rule.errorMsg;
self.cache.push(function () {
var strategy = strategyAry.shift();
strategyAry.unshift(dom.value);
strategyAry.push(errorMsg);
return strategies[strategy].apply(dom, strategyAry);
});
})(rule)
}
};
Validator.prototype.start = function () {
for (var i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
var errorMsg = validatorFunc();
if (errorMsg) {
return errorMsg;
}
}
};
var registerForm = document.getElementById('registerForm');
var validataFunc = function () {
var validator = new Validator();
validator.add(registerForm.userName, [{
strategy: 'isNonEmpty',
errorMsg: '用户名不能为空'
}, {
strategy: 'minLength:6',
errorMsg: '用户名长度不能小于 10 位'
}]);
validator.add(registerForm.password, [{
strategy: 'minLength:6',
errorMsg: '密码长度不能小于 6 位'
}]);
validator.add(registerForm.phoneNumber, [{
strategy: 'isMobile',
errorMsg: '手机号码格式不正确'
}]);
var errorMsg = validator.start();
return errorMsg;
}
registerForm.onsubmit = function () {
var errorMsg = validataFunc();
if (errorMsg) {
alert(errorMsg);
return false;
}
};
实操:往Cache中push错误信息,使用start输出错误信息。
var list = [{
strategy: 'isNonEmpty',
errorMsg: '用户名不能为空'
}, {
strategy: 'minLength:6',
errorMsg: '用户名长度不能小于 10 位'
}];
/**
* 策略
*/
var strategyList = {
isNonEmpty: function (value,msg) {
if(value.length === 0) {
return msg;
}
},
minLength: function (value,factor,msg) {
if(value.length <= factor) {
return msg;
}
}
}
/**
* 存储判断
* */
var ValidatorFun = function () {
this.cache = [];
}
/**
* 添加判断的类
*/
ValidatorFun.prototype.add = function (value, strategys) {
strategys.forEach(strategy => {
var arr = strategy.strategy.split(':');
var name = arr.shift();
arr.unshift(value.value);
arr.push(strategy.errorMsg);
var msg = strategyList[name].apply(value, arr);
console.log(name,arr,msg);
if(msg) {
this.cache.push(msg);
}
});
}
ValidatorFun.prototype.start = function () {
var msg;
console.log('cache', this.cache);
for(var i = 0;i<=this.cache.length;i++) {
msg = this.cache[i];
if(msg) {
return msg;
}
}
}
var registerForm = document.getElementById('registerForm');
var validataFunc = function () {
var validator = new ValidatorFun();
validator.add(registerForm.userName, [{
strategy: 'isNonEmpty',
errorMsg: '用户名不能为空'
}, {
strategy: 'minLength:6',
errorMsg: '用户名长度不能小于 10 位'
}]);
var errorMsg = validator.start();
return errorMsg;
}
registerForm.onsubmit = function () {
var errorMsg = validataFunc();
console.log('errorMsg', errorMsg);
if (errorMsg) {
alert(errorMsg);
return false;
}
return false;
}
策略模式的优缺点
优点
- 利用组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句。
- 提供了对开放—封闭原则的完美支持,将算法封装在独立的 strategy 中,使得它们易于切换,易于理解,易于扩展。
- 可以复用在系统的其他地方,从而避免许多重复的复制粘贴工作。
- 在策略模式中利用组合和委托来让 Context 拥有执行算法的能力,这也是继承的一种更轻
便的替代方案。
不使用 strategies 这个名字的策略模式。
var S = function( salary ){
return salary * 4;
};
var A = function( salary ){
return salary * 3;
};
var B = function( salary ){
return salary * 2;
};
var calculateBonus = function( func, salary ){
return func( salary );
};
calculateBonus( S, 10000 ); // 输出:40000
六、代理模式
代理的意义
图片预加载(预置一张默认图片)、
常见的预加载写法:(如果哪天不需要预加载了,将修改MyImage)
var MyImage = (function(){
var imgNode = document.createElement( 'img' );
document.body.appendChild( imgNode );
var img = new Image;
img.onload = function(){
imgNode.src = img.src;
};
return {
setSrc: function( src ){
imgNode.src = 'file:// /C:/Users/svenzeng/Desktop/loading.gif';
img.src = src;
}
}
})();
MyImage.setSrc( 'http:// imgcache.qq.com/music/photo/k/000GGDys0yA0Nk.jpg' );
代理写法:(如果不需要预加载,将调用时的proxyImage改成myImage就能正常使用)
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 ); // this.src即img.src
}
return {
setSrc: function( src ){
myImage.setSrc( 'file:// /C:/Users/svenzeng/Desktop/loading.gif' );
img.src = src;
}
}
})();
proxyImage.setSrc( 'http:// imgcache.qq.com/music/photo/k/000GGDys0yA0Nk.jpg' );
上述返回的方法名都为setSrc,这保持了代理和本体接口的一致性,代理和本体可以被替换使用。
BTW,如果代理对象和本体对象都为一个函数(函数也是对象),
函数必然都能被执行,则可以认为它们也具有一致的“接口”。
虚拟代理合并HTTP请求
var sentList = (function (id){
var cache = [],
timer;
return {
sent: function(id) {
cache.push(id);
if(timer) {
return;
}
timer = setInterval(() => {
var list = cache.join(',')
console.log(list);
cache.length = 0;
clearInterval(timer);
timer = null;
}, 2000);
}
}
})();
var checkbox = document.getElementsByTagName('input');
for(var i = 0,c;c = checkbox[i++];){
c.onclick = function(){
if (this.checked === true){
sentList.sent(this.id);
// 此处必须用this,因为在匿名函数中找不到c。
}
}
}
虚拟代理在惰性加载中的应用
(P99,118/317,有缘再看。
缓存代理
var mult = function(){
console.log( '开始计算乘积' );
var a = 1;
for ( var i = 0, l = arguments.length; i < l; i++ ){
a = a * arguments[i];
}
return a;
};
mult( 2, 3 ); // 输出:6
mult( 2, 3, 4 ); // 输出:24
现在加入缓存代理函数:
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
迭代器模式
迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,
而又不需要暴露该对象的内部表示。
实现自己的迭代器
var each = function( ary, callback ){
for ( var i = 0, l = ary.length; i < l; i++ ){
callback.call( ary[i], i, ary[ i ] ); // 把下标和元素当作参数传给 callback 函数
}
};
each( [ 1, 2, 3 ], function( i, n ){
alert ( [ i, n ] );
});
each 函数属于内部迭代器,each 函数的内部已经定义好了迭代规则,
它完全接手整个迭代过程,外部只需要一次初始调用,因此使用场景有限。
内部迭代器和外部迭代器
1.内部迭代器
var each = function(arr,fn) {
for(var i=0,len=arr.length;i<len;i++) {
fn.call(arr[i],i,arr[i]);
}
}
each([1,2,3],function(i,n,l){
alert([i,n]);
})
2.外部迭代器
var iterator = function(obj){
var current = 0;
var next = function(){
current += 1;
}
var isDone = function(){
return current > obj.length
}
var getItem = function() {
return obj[current];
}
return {
getItem: getItem,
next: next,
isDone: isDone
}
}
var compare = function(i1,i2) {
while (!i1.isDone() && !i2.isDone()) {
if(i1.getItem() !== i2.getItem()){
throw new Error('not equal');
}
i1.next();
i2.next();
}
}
var iterator1 = iterator([1,2,3]);
var iterator2 = iterator([4,5,6]);
compare(iterator1, iterator2);
迭代类数组对象和字面量对象
$.each = function (obj, callback) {
var value,
i = 0,
length = obj.length,
isArray = isArraylike(obj);
if (isArray) { // 迭代类数组
for (; i < length; i++) {
value = callback.call(obj[i], i, obj[i]);
if (value === false) {
break;
}
}
} else {
for (i in obj) { // 迭代 object 对象
value = callback.call(obj[i], i, obj[i]);
if (value === false) {
break;
}
}
}
return obj;
};
倒序迭代器
var reverseEach = function (ary, callback) {
for (var l = ary.length - 1; l >= 0; l--) {
callback(l, ary[l]);
}
};
reverseEach([0, 1, 2], function (i, n) {
console.log(n); // 分别输出:2, 1 ,0
});
中止迭代器
var each = function (ary, callback) {
for (var i = 0, l = ary.length; i < l; i++) {
if (callback(i, ary[i]) === false) { // callback 的执行结果返回 false,提前终止迭代
break;
}
}
};
each([1, 2, 3, 4, 5], function (i, n) {
if (n > 3) { // n 大于 3 的时候终止循环
return false;
}
console.log(n); // 分别输出:1, 2, 3
});
迭代器模式的应用
根据不同的浏览器获取相应的上传组件对象:<br />
``
var getUploadObj = function () {
try {
return new ActiveXObject("TXFTNActiveX.FTNUpload"); // IE 上传控件
} catch (e) {
if (supportFlash()) { // supportFlash 函数未提供
var str = '<object type="application/x-shockwave-flash"></object>';
return $(str).appendTo($('body'));
} else {
var str = '<input name="file" type="file"/>'; // 表单上
return $(str).appendTo($('body'));
}
}
};
可以发现,当我们对兼容性进行检查或者对某些数值进行校验时,若将验证函数放置在一个函数中,
会导致难以添加新校验规则or方法,违背了开闭原则,我们可以使用下面的这种方法:
(可以复习一下笔记中JavaScript设计原则——开闭原则的那篇文章)
var getActiveUploadObj = function(){
try{
return new ActiveXObject( "TXFTNActiveX.FTNUpload" ); // IE 上传控件
}catch(e){
return false;
}
};
var getFlashUploadObj = function(){
if ( supportFlash() ){ // supportFlash 函数未提供
var str = '<object type="application/x-shockwave-flash"></object>';
return $( str ).appendTo( $('body') );
}
return false;
};
var getFormUpladObj = function(){
var str = '<input name="file" type="file" class="ui-file"/>'; // 表单上传
return $( str ).appendTo( $('body') );
};
*迭代器代码*
var iteratorUploadObj = function(){
for ( var i = 0, fn; fn = arguments[ i++ ]; ){
var uploadObj = fn();
if ( uploadObj !== false ){
return uploadObj;
}
}
};
var uploadObj = iteratorUploadObj(getActiveUploadObj,getFlashUploadObj,getFormUpladObj );
增加分别获取 Webkit 控件上传对象和 HTML5 上传对象的函数:
var getWebkitUploadObj = function(){
// 具体代码略
};
var getHtml5UploadObj = function(){
// 具体代码略
};
依照优先级把它们添加进迭代器:
var uploadObj = iteratorUploadObj( getActiveUploadObj, getWebkitUploadObj,
getFlashUploadObj, getHtml5UploadObj, getFormUpladObj );
发布-订阅模式
发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在 JavaScript 开发中,我们一般用``_**事件模型**_``来替代传统的发布—订阅模式。
DOM事件
DOM节点绑定事件就是一种发布-订阅模式,在事件发生后再调用函数。
自定义事件
var salesOffices = {}; // 定义售楼处
salesOffices.clientList = {}; // 缓存列表,存放订阅者的回调函数
salesOffices.listen = function (key, fn) {
if (!this.clientList[key]) { // 如果还没有订阅过此类消息,给该类消息创建一个缓存列
this.clientList[key] = [];
}
this.clientList[key].push(fn); // 订阅的消息添加进消息缓存列表
};
salesOffices.trigger = function () { // 发布消息
var key = Array.prototype.shift.call(arguments), // 取出消息类型
fns = this.clientList[key]; // 取出该消息对应的回调函数集合
if (!fns || fns.length === 0) { // 如果没有订阅该消息,则返回
return false;
}
for (var i = 0, fn; fn = fns[i++];) {
fn.apply(this, arguments); // (2) // arguments 是发布消息时附送的参数
}
};
salesOffices.listen('squareMeter88', function (price) { // 小明订阅 88 平方米房子的消息
console.log('价格= ' + price); // 输出: 2000000
});
salesOffices.listen('squareMeter110', function (price) { // 小红订阅 110 平方米房子的消息
console.log('价格= ' + price); // 输出: 3000000
});
salesOffices.trigger('squareMeter88', 2000000); // 发布 88 平方米房子的价格
salesOffices.trigger('squareMeter110', 3000000); // 发布 110 平方米房子的价格
发布-订阅模式的通用实现
改写上面的代码,将发布-订阅功能提取出来。
var event = {
clientList: [],
// []或者{}都可以正常运行。
listen: function (key, fn) {
if (!this.clientList[key]) {
this.clientList[key] = [];
}
this.clientList[key].push(fn); // 订阅的消息添加进缓存列表
},
trigger: function () {
var key = Array.prototype.shift.call(arguments), // (1);
fns = this.clientList[key];
if (!fns || fns.length === 0) { // 如果没有绑定对应的消息
return false;
}
for (var i = 0, fn; fn = fns[i++];) {
fn.apply(this, arguments); // (2) // arguments 是 trigger 时带上的参数
}
}
};
再定义一个 installEvent 函数,这个函数可以给所有的对象都动态安装发布—订阅功能:
var installEvent = function (obj) {
for (var i in event) {
obj[i] = event[i];
}
};
再来测试一番,我们给售楼处对象 salesOffices 动态增加发布—订阅功能:
var salesOffices = {};
installEvent(salesOffices);
salesOffices.listen('squareMeter88', function (price) { // 小明订阅消息
console.log('价格= ' + price);
});
salesOffices.listen('squareMeter100', function (price) { // 小红订阅消息
console.log('价格= ' + price);
});
salesOffices.trigger('squareMeter88', 2000000); // 输出:2000000
salesOffices.trigger('squareMeter100', 3000000); // 输出:3000000
取消订阅的事件
event.remove = function (key, fn) {
var fns = this.clientList[key];
if (!fns) { // 如果 key 对应的消息没有被人订阅,则直接返回
return false;
}
if (!fn) { // 如果没有传入具体的回调函数,表示需要取消 key 对应消息的所有订阅
fns && (fns.length = 0);
} else {
for (var l = fns.length - 1; l >= 0; l--) { // 反向遍历订阅的回调函数列表
var _fn = fns[l];
if (_fn === fn) {
fns.splice(l, 1); // 删除订阅者的回调函数
}
}
}
};
var salesOffices = {};
var installEvent = function (obj) {
for (var i in event) {
obj[i] = event[i];
}
}
installEvent(salesOffices);
salesOffices.listen('squareMeter88', fn1 = function (price) { // 小明订阅消息
console.log('价格= ' + price);
});
salesOffices.listen('squareMeter88', fn2 = function (price) { // 小红订阅消息
console.log('价格= ' + price);
});
salesOffices.remove('squareMeter88', fn1); // 删除小明的订阅
salesOffices.trigger('squareMeter88', 2000000); // 输出:2000000
例子-网站登陆
登录之后需要添加一下刷新收货地址列表的代码。
Before
login.succ(function( data ){
header.setAvatar( data.avatar);
nav.setAvatar( data.avatar );
message.refresh();
cart.refresh();
address.refresh(); // 增加这行代码
});
After
$.ajax('http:// xxx.com?login', function (data) { // 登录成功
login.trigger('loginSucc', data); // 发布登录成功的消息
});
各模块监听登录成功的消息:
var header = (function () { // header 模块
login.listen('loginSucc', function (data) {
header.setAvatar(data.avatar);
});
return {
setAvatar: function (data) {
console.log('设置 header 模块的头像');
}
}
})();
var nav = (function () { // nav 模块
login.listen('loginSucc', function (data) {
nav.setAvatar(data.avatar);
});
return {
setAvatar: function (avatar) {
console.log('设置 nav 模块的头像');
}
}
})();
添加地址刷新
var address = (function () { // nav 模块
login.listen('loginSucc', function (obj) {
address.refresh(obj);
});
return {
refresh: function (avatar) {
console.log('刷新收货地址列表');
}
}
})();
全局的发布-订阅对象
发布—订阅模式可以用一个全局的 Event 对象来实现,订阅者不需要了解消息来自哪个发布者,发布者也不知道消息会推送给哪些订阅者,Event 作为一个类似“中介者”的角色,把订阅者和发布者联系起来。
var Event = (function () {
var clientList = {},
listen,
trigger,
remove;
listen = function (key, fn) {
if (!clientList[key]) {
clientList[key] = [];
}
clientList[key].push(fn);
};
trigger = function () {
var key = Array.prototype.shift.call(arguments),
fns = clientList[key];
if (!fns || fns.length === 0) {
return false;
}
for (var i = 0, fn; fn = fns[i++];) {
fn.apply(this, arguments);
}
};
remove = function (key, fn) {
var fns = clientList[key];
if (!fns) {
return false;
}
if (!fn) {
fns && (fns.length = 0);
} else {
for (var l = fns.length - 1; l >= 0; l--) {
var _fn = fns[l];
if (_fn === fn) {
fns.splice(l, 1);
}
}
}
};
return {
listen: listen,
trigger: trigger,
remove: remove
}
})();
Event.listen('squareMeter88', function (price) { // 小红订阅消息
console.log('价格= ' + price); // 输出:'价格=2000000'
});
Event.listen('squareMeter188', function (price) { // 小红订阅消息
console.log('价格= ' + price); // 输出:'价格=2000000'
});
Event.trigger('squareMeter88', 2000000); // 售楼处发布消息
Event.trigger('squareMeter188', 12000000); // 售楼处发布消息
模块间通信
<button id="count">点我</button>
<div id="show"></div>
var a = (function () {
var count = 0;
var button = document.getElementById('count');
button.onclick = function () {
Event.trigger('add', count++);
}
})();
var b = (function () {
var div = document.getElementById('show');
Event.listen('add', function (count) {
div.innerHTML = count;
});
})();
模块之间如果用了太多的全局发布—订阅模式来通信,那我们最终会搞不清楚消息来自哪个模块,或者消息
会流向哪些模块,这又会给我们的维护带来一些麻烦,也许某个模块的作用就是暴露一些接口给其他模块调用。
全局事件的命名冲突
全局的发布—订阅对象里只有一个 clinetList 来存放消息名和回调函数,大家都通过它来订阅和发布各种消息,久而久之,难免会出现事件名冲突的情况。
(P123,142/317)
命令模式
顾客预约,厨师按预约顺序做菜,记录着预约顺序的清单,就是命令模式中的命令对象。
1.命令模式的例子
<button id="button1">点击按钮 1</button>
<button id="button2">点击按钮 2</button>
<button id="button3">点击按钮 3</button>
var button1 = document.getElementById('button1'),
button2 = document.getElementById('button2'),
button3 = document.getElementById('button3');
var setCommand = function (button, command) {
button.onclick = function () {
command.execute();
}
};
var MenuBar = {
refresh: function () {
console.log('刷新菜单目录');
}
};
var SubMenu = {
add: function () {
console.log('增加子菜单');
},
del: function () {
console.log('删除子菜单');
}
};
var RefreshMenuBarCommand = function (receiver) {
this.receiver = receiver;
};
RefreshMenuBarCommand.prototype.execute = function () {
this.receiver.refresh();
};
var AddSubMenuCommand = function (receiver) {
this.receiver = receiver;
};
AddSubMenuCommand.prototype.execute = function () {
this.receiver.add();
// 此处为add(),虽然add执行后不会返回怎么值,
// 但是函数未执行时add也不执行,execute执行后add也执行,所以能间接console。
};
var DelSubMenuCommand = function (receiver) {
this.receiver = receiver;
};
DelSubMenuCommand.prototype.execute = function () {
console.log('删除子菜单');
};
console.log('AddSubMenuCommand', AddSubMenuCommand.prototype);
var refreshMenuBarCommand = new RefreshMenuBarCommand(MenuBar);
var addSubMenuCommand = new AddSubMenuCommand(SubMenu);
var delSubMenuCommand = new DelSubMenuCommand(SubMenu);
setCommand(button1, refreshMenuBarCommand);
setCommand(button2, addSubMenuCommand);
setCommand(button3, delSubMenuCommand);
2.JavaScript中的命令模式
var bindClick = function (button, func) {
button.onclick = func;
};
var MenuBar = {
refresh: function () {
console.log('刷新菜单界面');
}
};
var SubMenu = {
add: function () {
console.log('增加子菜单');
},
del: function () {
console.log('删除子菜单');
}
};
bindClick(button1, MenuBar.refresh);
bindClick(button2, SubMenu.add);
bindClick(button3, SubMenu.del);
上面两种命令模式的对比:
- 命令模式将过程式的请求调用封装在 command 对象的 execute 方法里,通过封装方法调用,我们可以把运算块包装成形。command 对象可以被四处传递,所以在调用命令的时候,客户(Client)不需要关心事情是如何进行的。
- 命令模式的由来,其实是回调(callback)函数的一个面向对象的替代品。
- 运算块不一定要封装在 command.execute 方法中,也可以封装在普通函数中。函数作为一等对象,本身就可以被四处传递。
在使用闭包的命令模式实现中,接收者被封闭在闭包产生的环境中,执行命令的操作可以更加简单,仅仅执行回调函数即可。无论接收者被保存为对象的属性,还是被封闭在闭包产生的环境中,在将来执行命令的时候,接收者都能被顺利访问。用闭包实现的命令模式如下代码所示:
var setCommand = function (button, func) {
button.onclick = function () {
func();
}
};
var MenuBar = {
refresh: function () {
console.log('刷新菜单界面');
}
};
var RefreshMenuBarCommand = function (receiver) {
return function () {
receiver.refresh();
}
};
var refreshMenuBarCommand = RefreshMenuBarCommand(MenuBar);
setCommand(button1, refreshMenuBarCommand);
撤销命令
*使用策略模式写一个 Animate 类*
var tween = {
linear: function (t, b, c, d) {
return c * t / d + b;
},
easeIn: function (t, b, c, d) {
return c * (t /= d) * t + b;
},
strongEaseIn: function (t, b, c, d) {
return c * (t /= d) * t * t * t * t + b;
},
strongEaseOut: function (t, b, c, d) {
return c * ((t = t / d - 1) * t * t * t * t + 1) + b;
},
sineaseIn: function (t, b, c, d) {
return c * (t /= d) * t * t + b;
},
sineaseOut: function (t, b, c, d) {
return c * ((t = t / d - 1) * t * t + 1) + b;
}
};
var Animate = function (dom) {
this.dom = dom; // 进行运动的 dom 节点
this.startTime = 0; // 动画开始时间
this.startPos = 0; // 动画开始时,dom 节点的位置,即 dom 的初始位置
this.endPos = 0; // 动画结束时,dom 节点的位置,即 dom 的目标位置
this.propertyName = null; // dom 节点需要被改变的 css 属性名
this.easing = null; // 缓动算法
this.duration = null; // 动画持续时间
};
Animate.prototype.start = function (propertyName, endPos, duration, easing) {
this.startTime = +new Date; // 动画启动时间
this.startPos = this.dom.getBoundingClientRect()[propertyName]; // dom 节点初始位置
this.propertyName = propertyName; // dom 节点需要被改变的 CSS 属性名
this.endPos = endPos; // dom 节点目标位置
this.duration = duration; // 动画持续事件
this.easing = tween[easing]; // 缓动算法
var self = this;
var timeId = setInterval(function () { // 启动定时器,开始执行动画
if (self.step() === false) { // 如果动画已结束,则清除定时
clearInterval(timeId);
}
}, 19);
};
Animate.prototype.step = function () {
var t = +new Date; // 取得当前时间
if (t >= this.startTime + this.duration) { // (1)
this.update(this.endPos); // 更新小球的 CSS 属性值
return false;
}
var pos = this.easing(t - this.startTime, this.startPos,
this.endPos - this.startPos, this.duration);
// pos 为小球当前位置
this.update(pos); // 更新小球的 CSS 属性值
};
Animate.prototype.update = function (pos) {
this.dom.style[this.propertyName] = pos + 'px';
};
var ball = document.getElementById('ball');
var pos = document.getElementById('pos');
var moveBtn = document.getElementById('moveBtn');
var cancelBtn = document.getElementById('cancelBtn');
var MoveCommand = function (receiver, pos) {
this.receiver = receiver;
this.pos = pos;
this.oldPos = null;
};
MoveCommand.prototype.execute = function () {
this.receiver.start('left', this.pos, 1000, 'strongEaseOut');
this.oldPos = this.receiver.dom.getBoundingClientRect()[this.receiver.propertyName];
// 记录小球开始移动前的位置
};
MoveCommand.prototype.undo = function () {
this.receiver.start('left', this.oldPos, 1000, 'strongEaseOut');
// 回到小球移动前记录的位置
};
var moveCommand;
moveBtn.onclick = function () {
var animate = new Animate(ball);
moveCommand = new MoveCommand(animate, pos.value);
moveCommand.execute();
};
cancelBtn.onclick = function () {
moveCommand.undo(); // 撤销命令
};
=
