JavaScript设计模式与开发实践

《JavaScript设计模式与开发时间》

前言

开发过程中,我们经常会有这种感觉:这个问题发生的场景很相似,之前好像遇到过很多次,但是如果要跟他人描述,那么就需要大量的篇幅去描述遇到的这个问题。我们就非常希望给这个场景一个名字,当其他开发听到这个名字的时候,就会有恍然大明白的感觉。大家对这个名字有了共识之后,沟通成本就会显著的降低。这也是各种设计模式被挖掘的原因,因为他确实是我们非常需要的。

第一部分 基础知识

面向对象的JavaScript

Js设计模式思想的主要组成

动态类型语言

编程语言按照数据类型大体可以分为两类,一类是静态类型语言,另一类是动态类型语言。

静态类型语言的优点:首先是在编译时就能发现类型不匹配的错误,编辑器可以帮助我们提前避免程序在运行期间有可能发生的一些错误。如果在程序中明确地规定了数据类型,编译器还可以针对这些信息对程序进行一些优化工作,提高程序执行速度。

静态类型语言的缺点:需要迫使程序员依照强契约来编写程序,为每个变量规定数据类型,辅助我们编写可靠性高的一种手段,而不是编写程序的目的。类型的生命也会增加更多的代码,编写过程中,这些细节会让程序员的精力从思考业务逻辑上分散开来。

动态类型语言的优点:编写的代码数量更少,看起来也更加简介,程序员可以吧精力更多地放在业务逻辑上面。

动态类型语言的缺点:无法保证变量的类型,从而在程序的运行期有可能发生跟类型相关的错误。

多态

多态:同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果,换句话说,给不同的对象发送同一个消息的时候,这些对象会根据这个消息分别给出不同的反馈。

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

在JavaScript这种将函数作为一等对象的语言中,函数本身也是对象,函数用来封装行为并且能够被四川传递。当我们对一些函数发出“调用”的消息时,这些函数会返回不同的执行结果,这是“多态性”的一种体现,也是很多设计模式在JavaScript中可以用高阶函数来代替实现的原因。

封装

在更多场景下的开发中,我们为了简化代码,实现程序的易维护性和可扩展性,我们多会选择功能封装,实现公用。封装的目的是将信息隐藏

封装数据

在Js中我们可以依赖变量的作用域来实现封装特性

var myObject = (function() {
    var _name = 'seven';
    return {
        getName:function(){
            return _name;
        }
    }
})()
console.log(myObject.getName()); // 'seven';
console.log(myObject._name); // undefined
// 通过symbol的方式来实现
const Exam = (function() {
    var _name = Symbol('seven');
    class Exam {
        constructor() {
            this[_name] = 'seven';
        }
        getName() {
            return this[_name];
        }
    }
    return Exam;
})()
var ex = new Exam();
console.log(ex.getName()); // seven
console.log(ex._name); // undefined;
封装实现

封装目的是将信息隐藏,封装应该被视为“任何形式的封装”。

封装使得对象内部的变化对其他对象而言都是透明的,也就是不可见的。对象对它自己的行为负责。其他对象或者用户都不关心它的内部实现。封装使得对象之间的耦合变松散,对象之间通过暴露的API接口来通信。当我们修改一个对象时,可以随意的修改它的内部实现,只要对外的接口没有变化,就不会影响到程序的其他功能。

封装类型

封装类型是静态类型中一种重要的封装方式。JavaScript本身也是一门类型模糊的语言。在封装类型方面,JavaScript没有能力,也没有必要做得更多。对于JavaScript的设计模式而言,不缺分类型则是一种解脱。

封装变化

通过封装变化的方式,把系统中稳定不变的部分和容易变化的部分隔离开来,在系统的演变过程中,我们呢只需要替换哪些容易变化的部分,如果这些部分是已经封装好的,替换起来也相对容易。这可以很大程度的保证程序的稳定性和可扩展性。

原型模式

原型模式不单是一个设计模式,也被称为一种编程泛型。

如果需要一个跟某个对象一模一样的对象,就可以使用原型模式。

对Js而言,原型模式则是构建了JavaScript的对象系统。不过区别是JavaScript拥有一个跟对象(Object.prototype对象),Object.prototype对象是一个空对象。

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

用new运算符来创建对象的过程,实际上也只是先克隆Object.prototype对象,再进行一些其他额外操作的过程。

js的原型继承

function Person(name) {
    this.name = name;
}
Person.prototype.getName = function() {
    return this.name;
}
var a = new Person('seven');
console.log(a.name); // seven
console.log(a.getName()); // seven
console.log(Object.getPrototypeOf(a) === Person.prototype); // true

模拟new关键字**

function Person(name) {
    this.name = name;
}
Person.prototype.getName = 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,'seven');
console.log(a.name); // seven
console.log(a.getName()); // seven
console.log(Object.getPrototypeOf(a) === Person.prototype); // true

// 执行一下代码时结果一样

var a = objectFactory(A,'seven');
var a = new A('seven');

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

某个对象的__proto__属性默认会指向它的构造器的原型对象。__proto__就是对象跟“对象构造器的原型”联系起来的纽带。

JavaScript的对象最初都是有Object.prototype对象克隆而来的,但对象构造器的原型并不仅限于Object.prototype上,而是可以动态指向其他对象。

关于原型的知识点详见JavaScript基础捡漏之原型与原型链

this,call,apply

this,call,apply

this

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

this指向

出去不常用的with和eval的情况,this指向大致可分为4中:

  • 作为对象的方法调用
  • 作为普通函数调用
  • 构造器调用
  • Function.prototype.call或Function.prototype.apply调用

作为对象的方法调用时,this总是指向该对象

var obj = {
  name: 'seven'
  getName() {
    alert(this.name); // seven
  }
}
obj.getName();

作为普通函数调用时,this总是指向全局对象window

var name = 'globalName';
var getName = function() {
  return this.name;
}
console.log(getName()); // globalName

构造器调用时,this总是指向返回的这个对象

var MyClass = {
  this.name = 'seven';
}
var obj = new MyClass();
console.log(obj.name); // seven

特殊情况:**当构造器主动的返回了一个对象,那么此次运算结果会最终显示这个对象,而不是我们期待的this**

var myClass = {
  this.name = 'seven';
  return {
  	name: 'six'
	}
}
var obj = new MyClass();
console.log(obj.name); // six

如果对象不显示的返回任何数据,或者是返回一个非对象类型的数据,就不会出现上述问题。

var myClass = {
  this.name = 'seven';
  return 'six';
}
var obj = new MyClass();
console.log(obj.name); // seven;
丢失的this
var obj = {
  myName: 'seven',
  getName: function() {
    return this.myName;
  }
}
console.log(obj.getName()); // seven
var getName2 = obj.getName();
console.log(getName2()) // undefined

第一个输入seven是因为getName是在obj的环境下运行,this.myName指向了obj内部的myName

第二个输出undefined,是因为使用了getName2来引用obj.getName,当运行getName2的时候,这个方法内的this指向的全局的对象window。而window下并没有myName这个变量,所以返回undefined;

call和apply和bind

三者都是通过this指向来实现方法的灵活使用!不过不同的是,call和apply会立即执行函数,而bind则不会,bind只是更改this指向,并不会执行函数。

call和apply的区别

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

apply接收两个参数,一个参数指定了函数体内this对象的指向,第二个参数为一个带下标的集合(数据/类数组)

var func = function(a,b,c) {
  alert([a,b,c]); // 输出[1,2,3];
}
func.apply(null,[1,2,3]);

call接收多个参数,一个参数指定了函数体内this对象的指向,其余参数其实就是将apply中第二个参数拆分成多个参数。当我们明确的知道函数接收多少个参数,而且想一目了然的表达形ca参和实参的对应关系时,我们可以选择call来传送参数!

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

Math.max.apply(null,[1,2,5,4,8]); // 8;
call和apply的用途
  • 改变this指向
var obj1 = {
  name: 'seven'
}
var obj2 = {
  name: 'six'
}
window.name = 'eight';
var getName = function() {
  alert(this.name);
}
getName(); // eight
getName.call(obj1); // seven
getName.call(obj2); // six;
  • 模拟bind的实现
Function.prototype.bind = function() {
  var self = this,
      context = [].shift.call(arguments), // 需要绑定的this上下文
      args = [].slice.call(arguments); // 剩余的参数转成数组
  return function() {
    return self.apply(context, [].concat.call(args,[].slice.call(arguments)));
    // 执行新的函数的时候,会把之前掺入的context当做新函数体内的this
    // 并且合并分两次分别传入的参数,作为新函数的参数
  }
};
var obj = {
  name: 'seven'
}
var func = function(a,b,c,d) {
  alert(this.name);
  alert([a,b,c,d]);
}.bind(obj,1,2,3,4);
func(3,4);
  • 借用其他对象的方法

函数的参数列表arguments是一个类数组对象,并非真正的数组,如果我们想在arguments中国添加一个新的元素,通常会借用Array.prototype.push

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

如果需要把arguments转换成真正的数组可以借用Array.prototype.slice方法;想截去arguments的头一个元素时,可以借用Array.prototype.shift

闭包

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

变量的作用域

变量的作用域就是说变量的有效范围。与闭包挂钩的则是我们常在函数内声明的变量。

var func = function() {
  var name = 'seven';
  console.log(name); // seven
}
func();
console.log(name); // Uncaught ReferenceError: name is not defined

JavaScript中函数可以用来创建函数作用域,函数就像汽车的车窗膜一样,里面可以看到外面定义的变量,但是外面访问不到里面的变量,除非你把窗拉下来才可以。

JavaScript在函数中搜索一个变量的时候,如果该函数内部没有变量,那么此次的搜索过程会随着代码执行环境创建的作用域链往外层逐层查找,一直搜索到全局对象为止。变量的搜索是从内到外的。

关于作用域的思考

console.log(a); // undeinfed
var a = 1;
console.log(a); // 1

b(); // b is not a function
var b = function() {
  return 2;
}
b(); // 2

c(); // 10
function c() {
  console.log(10);
}
c(); // 10

console.log(test); // function test() { console.log(19) }
var test = 'seven';
function test() {
  console.log(19);
}
console.log(test); // seven
变量的生存周期

函数内生命的局部变量,当退出函数时,这些局部变量就失去了意义,他们都会随着函数调用的结束而销毁。但是当函数被另一个变量引用时,则闭包缓存的变量则会永久存在,造成内存的占用。局部变量所在的环境还能被外界访问,这个局部变量就有了不被销毁的理由

var Type = {};
for(var i = 0,type;type = ['String','Array','Number','Object'][i++]) {
  (function(type) {
    Type['is' + type] = function(obj) {
      return Object.prototype.toString.call(obj) === '[object ' + type + ']';
    }
  })(type)
}
console.log(Type.isArray([])); // trye
console.log(Type.isObject({})); // true
console.log(Type.isString('str')); // true

闭包的更多作用

  1. 封装变量
// one
var mult = function() {
  var a = 1;
  for (var i = 0, l = arguments.length;i<l;i++) {
    a = a * arguments[i];
  }
  return a;
};

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

// three
var mult = (function() {
  var cache = {};
  return function() {
    var args = Array.ptototye.join.call(arguments, ',');
    if(cache[args]) {
			return cache[args];
    }
    for(var i = 0,l = arguments.length;i< l;i++) {
			a = a * arguments[i];
    }
    return cache[args] = a;
  }
})();
// four
var mult = (function() {
	var cache = {};
  return function() {
    var calculate = function() {
      var a = 1;
      for (var i = 0,l = arguments.length;i<l;i++) {
        a = a * arguments[i];
      }
      return a;
    }
    var args = Array.prototype.join.call(arguments,',');
    if(cache[args]) {
      return cache[args];
    }
    return cache[args] = calculate.apply(null,arguments);
  }
})()
  1. 延续局部变量的寿命
// 数据埋点,报表等场景 解决数据丢失的问题
var report = (function(){
  var imgs = [];
  return function(src) {
    var img = new Image();
    imgs.push(img);
    img.src = src;
  }
})
report('http://xxx.com/getUserInfo');

闭包和面向对象的设计

过程与数据的结合是形容面向对象中“对象”时经常使用的表达。对象以方法的形式包含了过程,而闭包则是在过程中以环境包含了数据。

通常用面向对象实现的功能,用闭包也能实现。反之亦然。

// 闭包
var extent = function() {
  var val = 0;
  return {
    call: function() {
      val++;
      console.log(val);
    }
  }
}
var extent = extent();
extent.call(); // 1
extent.call(); // 2
extent.call(); // 3
// 面向对象
var extend = {
  val: 0,
  call: function() {
    val++;
    console.log(this.val);
  }
};
extend.call(); // 1
extend.call(); // 2
extend.call(); // 3
// 面向过程
var Extend = function() {
  this.val = 0;
}
Extend.prototype.call = function() {
  this.val++;
  console.log(this.val);
}
var extend = new Extend();
extend.call(); // 1
extend.call(); // 2
extend.call(); // 3

闭包与内存管理

闭包是的功能十分强大,在复杂的设计中承担了重要角色。但是有一种耸人听闻的说法是,闭包会造成内存泄漏,其实造成内存泄漏的是你的不合理的代码块,并不是闭包造成的!

使用闭包的一部分原因是我们选择主动把一些变量封闭在闭包中,因为可能在以后还需要使用这些变量,如果把这些变量放在全局作用域中,对内存方面的影响是一致的,这里并不能说是内存泄漏。如果在将来需要回收这些变量,我们可以手动把这些变量设为null。

跟闭包和内存泄漏有关的点是,使用闭包的同时比较容易形成循环引用,如果闭包的作用域链中保存着一些DOM节点,这时候就有可能造成内存泄漏。但这并非闭包本身的问题,也并非Js的问题。在IE浏览器中,由于BOM和DOM中的对象是通过C++以COM对象的方式实现的,而COM对象的垃圾回收机制是引用计数策略。在基于引用技术策略的垃圾回收机制中,对象的循环引用是无法被回收的。是IE浏览器的垃圾回收机制的问题和不良好的代码块造成的。

高阶函数

函数可以作为参数被传递

函数可以作为返回值输出

函数作为参数传递

一般我们可以抽离出一部分容易变化的业务逻辑,把这部分逻辑放在函数参数中,让函数返回另外一个函数。

  • 回调函数
  • Array.prototype.sort
  • ……
函数作为返回值输出

让函数返回一个可执行的函数,意味着运算过程是可延续的。

  • 判断数据的类型

    • ```javascript
      var Type = {};
      for (var i = 0,type;type = [‘String’,’Array’,’Number’][i++]) {
      (function(type) {
      Type['is' + type] = function(obj) {
          return Object.prototype.toString.call(obj) === '[object ' + type + '']';
        }
      
      })(type)
      };
      Type.isArray([]); // true
      Type.isString(‘str’); // true
      
      * 单例模式
      
        * ```javascript
          var getSingle = function(fn) {
            var ret;
            return function() {
              return ret || (ret = fn.apply)(this,arguments));
            }
          }
          var getScript = getSingle(function() {
            return document.createElement('script');
          });
          var script1 = getScript();
          var script2 = getScript();
          alert(script1 === script2); // true

高阶函数实现AOP

AOP(面向切面变成):主要作用是把一些跟核心业务逻辑谋爱无关的功能抽离出来,通常包括日志统计,安全控制,异常处理。把这些功能抽离出来后,再通过“动态织入”的方式掺入业务逻辑模块中。

AOP多是通过把一个函数动态织入另外一个函数之中来实现。有种装饰这模式的感觉

Function.prototype.before = function(beforefn) {
  var _self = this;
  return function() {
    beforefn.apply(this,arguments);
    return _self.apply(this,arguments);
  }
}
Function.prototype.after = function(afterfn) {
  var _self = this;
  return function() {
    var ret = _self.apply(this,arguments);
    afterfn.apply(this, arguments);
    return ret;
  }
};
var func = function() {
  console.log(2);
}
func = func.before(function() {
  console.log(1);
}).after(function() {
  console.log(3);
})
func(); // 1  2  3

这种使用AOP的方式给函数添加职责,也是js中一种非常巧妙的装饰这模式实现。

高阶函数的其他应用

currying(科里化)

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

用JavaScript 实现一个add(1,2,3)(4)(5)(6,7)(8)() 返回结果为这些数字的和:36。

var calculate = (function() {
  var sum = 0;
  return function curried() {
    if(arguments.length === 0) {
      return sum;
    } else {
      for(var i = 0,l = arguments.length;i<l;i++) {
        sum += arguments[i];
      }
      return curried;
    }
  }
})()
calculate(1,2,3);
calculate(4,5);
console.log(calculate()); // 15
// console.log(calculate(1,2,3)(4,5)()); // 15
// 进一步封装
function currying(fn) {
  var args = [];
  return function curried() {
    if(arguments.length === 0) {
      return fn.apply(this,args);
    } else {
      [].push.apply(args,arguments);
      return curried;
    }
  }
};
var sum = function() {
  return [].slice.call(arguments).reduce((acc,cur) => acc + cur);
};
var minus = function() {
  return [].slice.call(arguments).reduce((acc,cur) => acc - cur);
};
var multiple = function() {
  return [].slice.call(arguments).reduce((acc,cur) => acc * cur);
}
var divide = function() {
  return [].slice.call(arguments).reduce((acc,cur) => acc / cur);
}
var result = currying(sum);
console.log(result(1,2,3)(4,5)()); // 15
uncurrying(科里化对偶)

科里化对偶就是将this泛化的过程提取出来。让对象得以使用原本不属于它的方法。Array.prototype的方法只能用来操作array对象,但是用call和apply可以把任意对象当做this传入某个方法,这样一来,方法中用到this的地方就不再局限于原来规定的对象,而是加以泛化并得到更光的适用性。

常见的就是类数组对象借用Array.prototype的方法

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

封装

Function.prototype.uncurrying = function() {
  var self = this; // self此时是Array.prototype.push
  return function() {
    var obj = Array.prototype.shift.call(arguments);// obj是 {"length": 1,"0": 1} 对象的第一个元素
   	// 截去第一个元素后剩下[2]:
    return self.apply(obj,arguments);
    // 相当于是Array.prototype.push.apply(obj, 2);
  }
}
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); // 用数组方法给对象中添加元素4
console.log(obj.length); // 4

var first = Array.shift(obj); // 用数组方法截取对象的第一个元素
console.log(first); // 1
console.log(obj); // {0:2,1:3,2:4,length:3}
Array.forEach(obj,function(i,n) {
  console.log(n); // 0,1,2
})
函数节流

事件的高频率触发导致浏览器假死,引发很大的性能问题。区别于函数防抖的是节流是间隔时间执行

  1. 函数节流场景:
  • Window.onresize事件,浏览器窗口大小被拖动而改变的时候,这个事件高频触发,如果同时我们加入了DOM的操作,那么浏览器很大肯能会出现卡顿现象
  • mousemove事件
  • scroll是否滑动到底部加载更多
  1. 函数节流原理

比如我们在window.onresize事件中要打印当前的浏览器窗口大小,一秒钟会执行10次,但是我们只需要执行2次或者3次,这就需要忽略掉一些事件请求,比如确保在500ms内只打印一次。我们可以借用setTimeout来实现。

  1. 函数节流的实现

这里的代码的原理是,将即将被执行的函数用setTimeout延迟一段时间执行。如果该次延迟执行还没有完成,则忽略接下来调用该函数的请求。

var throttle = function(fn, interval) {
  var _self = fn,  // 延迟后即将执行的方法
      timer, // 定时器
      firstTime = true; // 是否是第一次调用
  return function() {
    var args = arguments,
        _me = this;
    if(firstTime) {
      _self.apply(_me,args); // 如果是第一次执行则不需要延迟执行,立即执行
      return firstTime = false;
    }
    if(timer){
      return false; // 如果定时器还在则忽略即将发起的事件请求
    }
    timer = setTimeout(function() {
      clearTimeout(timer);
      timer = null;
      _self.apply(_me, args);
    }, interval || 500);
  }
};
window.onresize = throttle(function() {
  console.log(1);
},500);
函数防抖

函数防抖知识点截取自segmentfault苏格拉没有底

  1. 函数防抖场景:
  • 搜索框输入
  • 表单校验
  • 浏览器窗口大小的监听
  1. 函数防抖的原理

函数防抖其实是分为 “立即执行版” 和 “非立即执行版” 的,根据字面意思就可以发现他们的差别,所谓立即执行版就是 触发事件后函数不会立即执行,而是在 n 秒后执行,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。 而 “非立即执行版” 指的是 触发事件后函数会立即执行,然后 n 秒内不触发事件才能继续执行函数的效果。

  1. 函数防抖的实现
/**
 * @desc  函数防抖---“立即执行版本” 和 “非立即执行版本” 的组合版本
 * @param  func 需要执行的函数
 * @param  wait 延迟执行时间(毫秒)
 * @param  immediate---true 表立即执行,false 表非立即执行
 **/
function debounce(func,wait,immediate) {
    let timer;

    return function () {
        let context = this;
        let args = arguments;

        if (timer) clearTimeout(timer);
        if (immediate) {
            var callNow = !timer;
            timer = setTimeout(() => {
                timer = null;
            }, wait)
            if (callNow) func.apply(context, args)
        } else {
            timer = setTimeout(function(){
                func.apply(context, args)
            }, wait);
        }
    }
}

function handle(){
    console.log(Math.random());
}

// window.addEventListener("mousemove",debounce(handle,1000,true)); // 调用立即执行版本
window.addEventListener("mousemove",debounce(handle,1000,false)); // 调用非立即执行版本
分时函数

分时函数一般用来解决用户主动调用触发导致的性能问题

假如我们又1000个好友列表需要渲染,那么就要往页面中添加1000个DOM,很大可能造成浏览器的卡顿。这时我们就可以通过SetInterval固定时间段往页面中添加DOM,比如把1s创建1000个节点改为每隔200毫秒创建8个节点。这就是分时函数的主要思想

var timeChunk = function(ary,dn,count) {
  var obj,
      t;
  var len = ar.length;
  var start = function() {
    for (var i = 0;i < Math.min(count || 1, ary.length); i++) {
      var obj = ary.shift();
      fn(obj);
    }
  };
  return function() {
    t = setInterval(function() {
      if(ary.length === 0) { // 如果节点已经被全部创建好了
        return clearInterval(t);
      };
      start();
    }, 200); // 分批执行的时间间隔,也可以用参数的形式传入
  }
}
// use
 var ary = [];
for(var i = 1;i<= 10000;i++) {
  ary.push(i);
}
var renderFriendList = timeChunk(ary,function(n) {
  var div = document.createElement('div');
  div.innerHTML = n;
  document.body.appendChild(div);
}, 8);
renderFriendList();
惰性加载函数

这里的知识点其实与《高性能JavaScript》中的某一章节很类似,通过惰性加载函数的方式进行性能优化

前端开发因为浏览器的实现差异,一些嗅探工作不可避免。比如我们需要一个在各个浏览器中能够通过事件绑定函数addEvent,常见的写法为:

// bad 每次执行的时候我们都需要进行一次判断
var addEvent = function(elem, type, handler) {
  if(window.addEventListener) {
    return elem.addEventListener(type, handler,false);
  }
  if(window.attachEvent) {
    return elem.attachEvent('on' + type, handler);
  }
}

我们可以进一步优化将判断的这些嗅探工作提前

// good 自运行函数在我们还没有执行的时候就已经判断好了,再执行的时候,直接使用就ok
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);
    }
  }
})()

但是如果我们并没有使用这个方法,那么代码在加载的时候,这个判断其实也是先前执行了,我们可以进一步优化

// best // 通过重置的方法在函数第一次执行时,将函数重写避免以后的每次的判断
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);
    }
  }
  addEvent(elem, type, handler);
}

使用

var div = document.getElementById('div1');
addEvent(div, 'click', function() {
  console.log(1);
})
addEvent(div, 'click',function() {
  console.log(2);
})

很多设计模式都是通过闭包和高阶函数来实现的,其实可以把闭包和高阶函数理解为一种思想,因为这种思想将js变得更加灵活。我觉得这本书读到这里,其实我感觉的出来,我仍需要对高阶函数有过多的练习,为以后学习算法做好铺垫!


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!