基础知识

一、原型模式

原型模式的实现的关键即克隆。

  1. var plane =new Plane();
  2. var clonePlane = Object.create(plane);
  3. Object.create = Object.create || function (obj) {
  4. var F = function () {};
  5. F.prototype = obj;
  6. return new F();

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


new Object & Object.create

  1. 创建对象的方式不同 ```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”}

  1. 2. **创建对象属性的性质不同**
  2. `var o = Object.create({},{p:{value: 42}});`<br />_**p后跟的对象为p的属性描述,Object.create() 用第二个参数来创建非空对象的属性描述符默认是为false的,**_<br />_**而构造函数或字面量方法创建的对象属性的描述符默认为true。**_<br />**打印结果 --> {p: 42}**<br />_**所以属性p是不可写,不可枚举,不可配置的。**_<br />_**![图片.png](https://cdn.nlark.com/yuque/0/2020/png/600287/1584361018370-6225f3fc-bc7c-42f7-aac1-66c8d912289e.png#align=left&display=inline&height=574&margin=%5Bobject%20Object%5D&name=%E5%9B%BE%E7%89%87.png&originHeight=574&originWidth=355&size=39686&status=done&style=shadow&width=355)**_
  3. 3. **创建空对象时不同**
  4. ![图片.png](https://cdn.nlark.com/yuque/0/2020/png/600287/1584361070759-68351547-65aa-43d0-8be2-c983e3080219.png#align=left&display=inline&height=312&margin=%5Bobject%20Object%5D&name=%E5%9B%BE%E7%89%87.png&originHeight=312&originWidth=598&size=37733&status=done&style=shadow&width=598)
  5. <a name="Sg9Yy"></a>
  6. ## [ ].slice.call(arguments) & [ ].shift.call(arguments);
  7. ![图片.png](https://cdn.nlark.com/yuque/0/2020/png/600287/1584361147756-e22e4bf3-dbec-4e2b-b0e0-05a59f9776cb.png#align=left&display=inline&height=295&margin=%5Bobject%20Object%5D&name=%E5%9B%BE%E7%89%87.png&originHeight=295&originWidth=264&size=23052&status=done&style=shadow&width=264)<br />**理解:**<br />_**slice 方法原理就是根据传入的参数(值)对原数组(或者类数组)进行遍历获取,赋给新数组然后返回。**_<br />_**如果没有参数便复制整个原数组(或者类数组),后赋给新数组然后返回。**_<br />_<br />_**因为slice内部实现是使用的this代表调用对象。那么当[].slice.call() 传入 arguments对象的时候,通过 call函数改变原来 slice方法的this指向, 使其指向arguments,并对arguments进行复制操作,而后返回一个新数组。**_<br />_**至此便是完成了arguments类数组转为数组的目的!**_<br />_<br />_**前面需要一个[]是用来调用方法。**_<br />_**除了slice还有其他的数组方法也能借用来遍历,获取新的结果。**_
  8. ```javascript
  9. ***计算对象中某个属性的数值总和***
  10. function countResources() {
  11. return Array.prototype.reduce.call($scope.resourcesList, function (prev, curr) {
  12. return prev + curr.resources.length;
  13. }, 0);
  14. }
  15. //prev是上次是结果的返回值,0是initialValue。若initialvalue为10,则最终结果会+10;
  16. ***数组降维***
  17. var flattened = [[0, 1], [2, 3], [4, 5]].reduce(
  18. function(a, b) {
  19. return a.concat(b);
  20. }, []);
  21. // flattened is [0, 1, 2, 3, 4, 5]
  22. ***按属性对Object分类***
  23. var people = [
  24. { name: 'Alice', age: 21 },
  25. { name: 'Max', age: 20 },
  26. { name: 'Jane', age: 20 }
  27. ];
  28. function groupBy(objectArray, property) {
  29. return objectArray.reduce(function (acc, obj) {
  30. var key = obj[property];
  31. if (!acc[key]) {
  32. acc[key] = [];
  33. }
  34. acc[key].push(obj);
  35. return acc;
  36. }, {});
  37. }
  38. var groupedPeople = groupBy(people, 'age');
  39. // groupedPeople is:
  40. // {
  41. // 20: [
  42. // { name: 'Max', age: 20 },
  43. // { name: 'Jane', age: 20 }
  44. // ],
  45. // 21: [{ name: 'Alice', age: 21 }]
  46. // }
  47. ***将对象中某个属性的值整合在一起***
  48. var allbooks = friends.reduce(function(prev, curr) {
  49. return [...prev, ...curr.books];
  50. }, ['Alphabet']);
  51. // allbooks = [
  52. // 'Alphabet', 'Bible', 'Harry Potter', 'War and peace',
  53. // 'Romeo and Juliet', 'The Lord of the Rings',
  54. // 'The Shining'
  55. // ]
  56. ***ES5使用reduce去重***
  57. let arr = [1,2,1,2,3,5,4,5,3,4,4,4,4];
  58. let result = arr.sort().reduce((init, current) => {
  59. if(init.length === 0 || init[init.length-1] !== current) {
  60. init.push(current);
  61. }
  62. return init;
  63. }, []);
  64. console.log(result); //[1,2,3,4,5]

forEach filter every some map等方法对空位的处理

tips:

  1. 遍历var a = [1,2,,3]时,
  2. forEach( ), filter( ), every( ) 和some( )都会跳过空位。
  3. map()会跳过空位,但会保留这个值。
  4. join( )和toString( )会将空位视为undefined,而undefined和null会被处理成空字符串。
  5. 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。

  1. _**map会跳过空位,但Array.from会将空位转为undefined,所以Array.from中的map不会跳过空位。**_
  2. ```javascript
  3. var arr = [,1,,2];
  4. var b = arr.map(function(item,index) {
  5. return index;
  6. });
  7. var c = Array.from(new Array(100),function(item,index) {
  8. return index;
  9. })
  10. console.log(arr,b,c);
  11. // [empty, 1, empty, 2]
  12. // [empty, 1, empty, 3]
  13. // (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]
  14. ***附加内容***
  15. ***Array.from结合Set可实现去重***
  16. let arr = Array.from(new Set([1,2,3,1,2,3,4,5,4,3]));
  17. console.log(arr); // [ 1, 2, 3, 4, 5 ]

二、this、call、apply

this

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

this的指向

  1. 作为对象的方法调用。
  2. 作为普通函数调用。
  3. 构造器调用。
  4. Function.prototype.call 或 Function.prototype.apply 调用。

    作为对象的方法调用。

    P25
    当函数作为对象的方法被调用时,this 指向该对象:
    调用时是谁的方法this就指向谁。
    截图.png
    ->
    1. obj.getname(); // a,指向obj
    2. obj.b.getBname(); // a2,指向obj.b
    3. var d = obj.getname;
    4. d();//此时作为普通函数调用,指向window

局部方法callback被作为普通函数调用时,内部指向window:
图片.png
解决方法:
图片.png
TIP:在严格模式下,上述情况调用时this不指向window,而是undefined。

构造器调用

当用 new 运算符调用函数时,该函数总会返回一个对象,通常情况下,构造器里的 this 就指向返回的这个对象。
image.png

  1. function fun() {
  2. this.a='a1';
  3. this.getA = function () {
  4. var that =this;
  5. this.b='a2';
  6. console.log(this,that);
  7. }
  8. }
  9. var obj=new fun();
  10. obj.getA();
  11. //this皆指向fun()返回的值,即fun {a: "a1", b: "a2", getA: ƒ}

但用 new 调用构造器时,还要注意一个问题,如果构造器显式地返回了一个 object 类型的对象,
那么此次运算结果最终会返回这个对象,而不是我们之前期待的 this:
image.png
BUT如果return的对象里有使用this,那么仍然会返回期待的this

  1. var MyClass = function () {
  2. this.name = 'sven';
  3. return { // 显式地返回一个对象
  4. name: this.name
  5. }
  6. };
  7. var obj = new MyClass();
  8. console.log(obj.name,obj); // sven {name: "sven"}

而且,若返回的为非对象类型的数据,就不会造成上述的this指向问题:

Function.prototype.call 或 Function.prototype.apply 调用

  1. var obj1 = {
  2. name: 'sven',
  3. getName: function(){
  4. return this.name;
  5. }
  6. };
  7. var obj2 = {
  8. name: 'anne'
  9. };
  10. console.log( obj1.getName() ); // 输出: sven
  11. console.log( obj1.getName.call( obj2 ) ); // 输出:anne

丢失的this

  1. var obj = {
  2. myName: 'sven',
  3. getName: function(){
  4. return this.myName;
  5. }
  6. };
  7. console.log( obj.getName() ); // this指向obj,输出:'sven'
  8. var getName2 = obj.getName;
  9. console.log( getName2() ); // this指向window,输出:undefined

call & apply

是Function的方法
fun.apply(obj,arguments);将arguments(数组)作为参数传入fun中。

  1. fun.call(obj/null,1,2,3);
  2. fun.apply(obj/null,[1,2,3]);

image.png
修正document.getElementById内部丢失的this:
image.png
第一个参数为null时,this会指向默认宿主对象,浏览器中为null,在严格模式下,参数为null则this为null。
为null是表示使用不在于this指向,而是借用方法并且给函数传参。比如:

  1. // 传参方式不同,apply第二个参数可传数组or类数组
  2. Math.max(1,2,3);//3
  3. Math.max.apply(null,[1,2,3]);
  4. //3,这种使用方式和this无关,[1,2,3]是传1,2,3为参数给Math.max实质函数。

实现Function.prototype.bind

  1. Function.prototype.bind = function( context ){
  2. var self = this; // 保存原函数
  3. return function(){ // 返回一个新的函数
  4. return self.apply( context, arguments );
  5. // 执行新的函数的时候,会把之前传入的 context,当作新函数体内的 this
  6. }
  7. };
  8. var obj = {
  9. name: 'sven'
  10. };
  11. var func = function(){
  12. alert ( this.name ); // 输出:sven
  13. }.bind( obj);
  14. func();

拓展:复杂一点的bind实现
image.png
借用其他对象的方法

  1. var A = function( name ){
  2. this.name = name;
  3. };
  4. var B = function(){
  5. A.apply( this, arguments );
  6. };
  7. B.prototype.getName = function(){
  8. return this.name;
  9. };
  10. var b = new B( 'sven' );
  11. console.log( b.getName() ); // 输出: 'sven'
  12. (function(){
  13. Array.prototype.push.call( arguments, 3 );
  14. console.log ( arguments ); // 输出[1,2,3]
  15. })( 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 一直处在这个环境里。 *既然局部变量所在的环境还能被外界访问,这个局部变量就有了不被销毁的理由。

  1. ```javascript
  2. for ( var i = 0, len = nodes.length; i < len; i++ ){
  3. (function( i ){
  4. nodes[ i ].onclick = function(){
  5. console.log(i);
  6. }
  7. })( i )
  8. };

image.png
注:

  1. var a = [1,2,3];
  2. Object.prototype.toString(a); // "[object Object]"
  3. Object.prototype.toString.call(a); // "[object Array]"

封装变量

  1. var mult = (function () {
  2. var args = {};
  3. var calculate = function () {
  4. var a = 1;
  5. for (var i = 0; i < arguments.length; i++) {
  6. a = a * arguments[i];
  7. }
  8. console.log('a1', a)
  9. return a;
  10. }//calculate为提炼出来的代码,因为没必要放到return里去计算。
  11. return function () {
  12. var arg = Array.prototype.join.call(arguments, ',');
  13. if (arg in args) {
  14. return args[arg];
  15. //return里有args,延续了args的生命,使退出mult后args也不会消失。
  16. //此处若不使用闭包,那么否则args将和mult一样暴露在全局作用于下。
  17. }
  18. return args[arg] = calculate.apply(null, arguments);//此处若不使用apply,返回的是NaN。
  19. }
  20. })()
  21. mult(1, 2, 3, 4, 5);
  22. //a1 120
  23. //120
  24. mult(1, 2, 3, 4, 5);
  25. //120

延续局部变量寿命

report 函数的调用结束后,img 局部变量随即被销毁,而此时或许还没来得及发出 HTTP 请求,所以此次请求就会丢失掉。

  1. var report = function( src ){
  2. var img = new Image();
  3. img.src = src;
  4. };
  5. report( 'http://xxx.com/getUserInfo' );

现在我们把 img 变量用闭包封闭起来,便能解决请求丢失的问题:

  1. var report = (function(){
  2. var imgs = [];
  3. return function( src ){
  4. var img = new Image();
  5. imgs.push( img );
  6. img.src = src;
  7. }
  8. })();

闭包和面向对象设计

  1. var extent = function () {
  2. var value = 0;
  3. return {
  4. call: function () {
  5. value++;
  6. console.log(value);
  7. }
  8. }
  9. };
  10. var extent = extent();
  11. extent.call(); // 输出:1
  12. extent.call(); // 输出:2
  13. extent.call(); // 输出:3

如果换成面向对象的写法, 就是:

  1. var extent = {
  2. value: 0,
  3. call: function () {
  4. this.value++;
  5. console.log(this.value);
  6. }
  7. };
  8. extent.call(); // 输出:1
  9. extent.call(); // 输出:2
  10. extent.call(); // 输出:3

用闭包实现命令模式

  1. <button id="execute">点击我执行命令</button>
  2. <button id="undo">点击我执行命令</button>
  1. var Tv = {
  2. open: function(){
  3. console.log( '打开电视机' );
  4. }
  5. };
  6. var OpenTvCommand = function( receiver ){
  7. this.receiver = receiver;
  8. };
  9. OpenTvCommand.prototype.execute = function(){
  10. this.receiver.open(); // 执行命令,打开电视机
  11. };
  12. var setCommand = function( command ){
  13. document.getElementById( 'execute' ).onclick = function(){
  14. command.execute(); // 输出:打开电视机
  15. }
  16. };
  17. setCommand( new OpenTvCommand( Tv ) );
  18. // new OpenTvCommand( Tv )为Object,
  19. // 有var b = new OpenTvCommand( Tv ),
  20. // setCommand(b),b.execute = function(){ this.receiver.open(); };

在面向对象版本的命令模式中,预先植入的命令接收者被当成对象的属性保存起来;
而在闭包版本的命令模式中,命令接收者会被封闭在闭包形成的环境中,代码如下:

  1. var Tv = {
  2. open: function(){
  3. console.log( '打开电视机' );
  4. },
  5. close: function(){
  6. console.log( '关上电视机' );
  7. }
  8. };
  9. var createCommand = function( receiver ){
  10. var execute = function(){
  11. return receiver.open(); // 执行命令,打开电视机
  12. }
  13. var undo = function(){
  14. return receiver.close(); // 执行命令,关闭电视机
  15. }
  16. return {
  17. execute: execute,
  18. undo: undo
  19. }
  20. };
  21. var setCommand = function( command ){
  22. document.getElementById( 'execute' ).onclick = function(){
  23. command.execute(); // 输出:打开电视机
  24. }
  25. document.getElementById( 'undo' ).onclick = function(){
  26. command.undo(); // 输出:关闭电视机
  27. }
  28. };
  29. setCommand( createCommand( Tv ) );

闭包和内存管理

要解决循环引用带来的内存泄露问题,我们只需要把循环引用中的变量设为 null即可。
将变量设置为 null 意味着切断变量与它此前引用的值之间的连接。
当垃圾收集器下次运行时,就会删除这些值并回收它们占用的内存

高阶函数

高阶函数是指至少满足下列条件之一的函数。
 函数可以作为参数被传递;
 函数可以作为返回值输出。

函数可以作为参数被传递

1.回调函数

我们想在 ajax 请求返回之后做一些事情,但又并不知道请求返回的确切时间时,
最常见的方案就是把 callback 函数当作参数传入发起 ajax 请求的方法中,待请求完成之后执行 callback 函数。

  1. var getUserInfo = function( userId, callback ){
  2. $.ajax( 'http://xxx.com/getUserInfo?' + userId, function( data ){
  3. if ( typeof callback === 'function' ){
  4. callback( data );
  5. }
  6. });
  7. }
  8. getUserInfo( 13157, function( data ){
  9. alert ( data.userName );
  10. });

2.Array.prototype.sort

  1. //从大到小排列
  2. [ 1, 4, 3 ].sort( function( a, b ){
  3. return b - a;
  4. });
  5. // 输出: [ 4, 3, 1 ]

函数作为返回值输出

判断数据的类型

  1. var Type = {};
  2. for(var i = 0,type; type = ['String','Number','Array'][i++];){
  3. (function(type){
  4. Type['is'+type] = function(obj){
  5. return Object.prototype.toString.call(obj) === '[object '+type+']'
  6. }
  7. })(type)
  8. }
  9. Type.isArray( [] ); // 输出:true
  10. Type.isString( "str" ); // 输出:true

高阶函数的其他应用

1.currying

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

  1. var cost = (function(){
  2. var args = [];
  3. return function(){
  4. if(arguments.length === 0) {
  5. var sum = 0;
  6. for(var i = 0, l = args.length; i < l; i++) {
  7. sum += args[i]
  8. }
  9. return sum;
  10. } else {
  11. Array.prototype.push.apply(args,arguments);
  12. }
  13. }
  14. })()
  15. cost( 100 ); // 未真正求值
  16. cost( 200 ); // 未真正求值
  17. cost( 300 ); // 未真正求值
  18. console.log( cost() ); // 求值并输出:600

写一个能curring化函数的函数

  1. var currying = function( fn ){
  2. var args = [];
  3. return function(){
  4. if ( arguments.length === 0 ){
  5. return fn.apply( this, args ); //this写成null结果一样,但不确定有什么别的差别。
  6. }else{
  7. [].push.apply( args, arguments );
  8. return arguments.callee; //返回了return后面的函数。
  9. }
  10. }
  11. };
  12. var cost = (function(){
  13. var money = 0;
  14. return function(){
  15. for ( var i = 0, l = arguments.length; i < l; i++ ){
  16. money += arguments[ i ];
  17. }
  18. return money;
  19. }
  20. })();
  21. var cost = currying( cost ); // 转化成 currying 函数
  22. cost( 100 ); // 未真正求值,返回return后面的f()
  23. cost( 200 ); // 未真正求值
  24. cost( 300 ); // 未真正求值
  25. alert ( cost() ); // 求值并输出:600

2.uncurrying

  1. for ( var i = 0, fn, ary = [ 'push', 'shift', 'forEach' ]; fn = ary[ i++ ]; ){
  2. Array[ fn ] = Array.prototype[ fn ].uncurrying();
  3. };
  4. var obj = {
  5. "length": 3,
  6. "0": 1,
  7. "1": 2,
  8. "2": 3
  9. };
  10. Array.push( obj, 4 ); // 向对象中添加一个元素
  11. console.log( obj.length ); // 输出:4
  12. var first = Array.shift( obj ); // 截取第一个元素
  13. console.log( first ); // 输出:1
  14. console.log( obj ); // 输出:{0: 2, 1: 3, 2: 4, length: 3}
  15. Array.forEach( obj, function( i, n ){
  16. console.log( n ); // 分别输出:0, 1, 2
  17. });

3.函数节流

(1)场景

  • window.onresize 事件。
  • mousemove 事件。
  • 上传进度。
  • ……频繁触发的事件

(2)原理
按时间段来忽略掉一些事件请求
(3)实现

  1. function throttle (fn,interval) {
  2. const __self = fn,
  3. interVal = interval;
  4. // 不变的定值,使用const保存在闭包中。
  5. let timer,
  6. firstTime = true;
  7. // timer未定义,firstTime之后会变值,这俩用let。
  8. return function(){
  9. const __me = this,
  10. args = arguments;
  11. if(firstTime) {
  12. __self.apply(__me,args);
  13. // __me指向window,args为resize这个event,这句目的是调用__self,此处直接__self()效果相同。
  14. return firstTime = false;
  15. // 将firstTime设置为false,且使用return来阻断代码下行。
  16. }
  17. if(timer) {
  18. return false;
  19. // 如果设置了timer且还未结束,那么return false来阻止触发新setTimeout。
  20. }
  21. timer = setTimeout(function() {
  22. clearTimeout(timer);
  23. timer = null;
  24. // 定义timer需先清除timer。
  25. __self.apply(__me,args);
  26. },interval || 500);
  27. }
  28. }
  29. // 调用
  30. window.onresize = throttle(function(){
  31. console.log(1);
  32. },500);//调用后形同window.onresize = function(){} //throttle return回来的函数

4.分时函数

把 1 秒钟创建 1000 个节点,改为每隔 200 毫秒创建 8 个节点。

  1. const timeChunk = function(arr,fn,limit) {
  2. let t,
  3. obj;
  4. let start = function(){
  5. for(var i = 0; i < Math.min(limit || 1, arr.length); i++){
  6. var obj = arr.shift();
  7. fn(obj);
  8. }
  9. }
  10. return function(){
  11. t = setInterval(function(){
  12. if(arr.length === 0) { // 如果全部节点都已经被创建好
  13. return clearInterval(t);
  14. }
  15. start();
  16. },200);
  17. }
  18. }
  19. // 调用
  20. let i = 0,
  21. arr = [];
  22. for (i; i < 1000; i++) {
  23. arr.push(i);
  24. }
  25. const render = timeChunk(arr,function(n){
  26. let newDiv = document.createElement('div');
  27. newDiv.innerHTML = n;
  28. document.body.appendChild(newDiv);
  29. },8);
  30. render();

5.惰性加载函数

  • 普通写法,每次被调用的时候都会执行里面的 if 条件分支。

    1. var addEvent = function( elem, type, handler ){
    2. if ( window.addEventListener ){
    3. return elem.addEventListener( type, handler, false );
    4. }
    5. if ( window.attachEvent ){
    6. return elem.attachEvent( 'on' + type, handler );
    7. }
    8. };
  • 我们把嗅探浏览器的操作提前到代码加载的时候,在代码加载的时候就立刻进行一次判断,以便让 addEvent 返回一个包裹了正确逻辑的函数。

    1. var addEvent = (function(){
    2. if ( window.addEventListener ){
    3. return function( elem, type, handler ){
    4. elem.addEventListener( type, handler, false );
    5. }
    6. }
    7. if ( window.attachEvent ){
    8. return function( elem, type, handler ){
    9. elem.attachEvent( 'on' + type, handler );
    10. }
    11. }
    12. })();
  • 重写addEvent

    1. var addEvent = function( elem, type, handler ){
    2. if ( window.addEventListener ){
    3. addEvent = function( elem, type, handler ){
    4. elem.addEventListener( type, handler, false );
    5. }
    6. }else if ( window.attachEvent ){
    7. addEvent = function( elem, type, handler ){
    8. elem.attachEvent( 'on' + type, handler );
    9. }
    10. }

    设计模式

    四、单例模式

    用一个变量来标志当前是否已经为某个类创建过对象
    单例模式的核心是确保只有一个实例,并提供全局访问。
    如果是,则在下一次获取该类的实例时,直接返回之前创建的对象。

    实现单例模式

    ```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. (1)console.log('this',this);<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/600287/1590581322049-ac1d3c0a-e2ba-4e8a-8bab-b1543be7b020.png#align=left&display=inline&height=164&margin=%5Bobject%20Object%5D&name=image.png&originHeight=134&originWidth=539&size=10177&status=done&style=shadow&width=661)<br />(2)console.dir(Singleton);<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/600287/1591607533289-5454226a-bbd4-4d09-a814-fb45681f17db.png#align=left&display=inline&height=501&margin=%5Bobject%20Object%5D&name=image.png&originHeight=412&originWidth=568&size=28032&status=done&style=none&width=691)
  2. <a name="DsVCQ"></a>
  3. ## 透明的单例
  4. ```javascript
  5. var CreateDiv = (function(){
  6. var instance;
  7. var CreateDiv = function( html ){
  8. if ( instance ){
  9. return instance;
  10. }
  11. this.html = html;
  12. this.init();
  13. console.log('this',this); // (1
  14. return instance = this;
  15. };
  16. CreateDiv.prototype.init = function(){
  17. var div = document.createElement( 'div' );
  18. div.innerHTML = this.html;
  19. document.body.appendChild( div );
  20. };
  21. return CreateDiv;
  22. })();
  23. var a = new CreateDiv( 'sven1' );
  24. var b = new CreateDiv( 'sven2' );
  25. alert ( a === b ); // true

image.png

用代理实现单例模式

  1. var CreateDiv = function( html ){
  2. this.html = html;
  3. this.init();
  4. };
  5. CreateDiv.prototype.init = function(){
  6. var div = document.createElement( 'div' );
  7. div.innerHTML = this.html;
  8. document.body.appendChild( div );
  9. };
  10. var proxySingletonCreateDiv = (function(){
  11. var instance;
  12. return function( html ){
  13. if ( !instance ){
  14. instance = new CreateDiv( html );
  15. }
  16. return instance;
  17. }
  18. })();
  19. var a = new ProxySingletonCreateDiv( 'sven1' );
  20. var b = new ProxySingletonCreateDiv( 'sven2' );
  21. alert ( a === b );

JavaScript中的单例

在 JavaScript 开发中,我们经常会把全局变量当成单例来使用。
var a = {};
当用这种方式创建对象 a 时,对象 a 确实是独一无二的。
如果 a 变量被声明在全局作用域下,则我们可以在代码中的任何位置使用这个变量,全局变量提供给全局访问是理所当然的。这样就满足了单例模式的两个条件。
我们有必要尽量减少全局变量的使用

1.使用命名空间减少全局变量

  1. var namespace1 = {
  2. a: function(){
  3. alert (1);
  4. },
  5. b: function(){
  6. alert (2);
  7. }
  8. };
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 被置为不同的值来实现嵌套

image.png

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);

上面两种命令模式的对比:

  1. 命令模式将过程式的请求调用封装在 command 对象的 execute 方法里,通过封装方法调用,我们可以把运算块包装成形。command 对象可以被四处传递,所以在调用命令的时候,客户(Client)不需要关心事情是如何进行的。
  2. 命令模式的由来,其实是回调(callback)函数的一个面向对象的替代品。
  3. 运算块不一定要封装在 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(); // 撤销命令
};

=