百木园-与人分享,
就是让自己快乐。

《javascript设计模式与开发实践》——第三章(闭包和高阶函数)学习记录

3.1闭包

闭包的形成与变量的作用域以及变量的生存周期密切相关。我们先来看下这两个前置技能

变量的作用域:就是指变量的有效范围,我们在声明一个变量的时候,如果该变量前面没有关键字var,这个变量就是全局变量,这是一种容易造成命名冲突的做法。
另一种情况是用var在函数中声明变量,这个时候的变量就是局部变量,只有在该函数内部才能访问这个变量,如下:

  var func = function(){
        var a = 1;
        alert(a)//1
    }
    func();
    alert(a)

在函数外alert(a),浏览器就会报错
在这里插入图片描述
在JavaScript中,函数可以用来创造函数作用域,此时的函数就像一层半透明的玻璃,在函数里面可以看到外面的变量,但是在函数外面就无法看到函数里面的变量。在函数里面搜索一个变量的时候,如果没有找到这个变量,就会随着代码执行环境创建的作用域链往外层逐层搜索,一只搜索到全局对象为止。注意这个搜索过程是由内而外的,并且不能反过来。

看一下下面这段包含了嵌套函数的代码:

var a = 1;
    var func1 = function(){
      var b = 2;
      var func2 = function(){
        var c = 3;
        alert(b)//2
        alert(a)//1
      }
      func2();
      alert(c)//报错:prograDesign_3.html:26 Uncaught ReferenceError: c is not defined
    }
    func1()

变量的生存周期:对于全局变量来说,生存周期当然是永久的,除非我们主动销毁这个全局变量,而对于局部变量来说,退出函数的时候,局部变量就会被销毁,但是有些函数,退出函数后,变量并没有销毁:

 var func = function(){
      var a = 1;
      return function(){
        a++;
        alert(a);
      }
    }
    var f = func()
    f()//2
    f()//3
    f()//4
    f()//5

可以看出a的值一直在++,退出函数后,a并没有销毁。这是因为当var f = func()执行时,f返回了一个匿名函数的引用,它可以访问到func被调用时产生的环境,a也在这个环境里。因为这个环境一直能被外界访问,所以a也有了不被销毁的理由。这里就产生了
闭包结构,局部变量的生命看起来就被延续了。
利用闭包我们可以完成许多奇妙的工作,下面就有一个经典应用。假设页面上有五个div节点,我们通过循环给每个div绑定click事件,希望点击第一个div的时候弹出1,点击第二个弹出2,以此类推。代码如下:

<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
var nodes = document.getElementsByTagName(\'div\');
      for(var i = 0,len = nodes.length;i<len;i++){
        nodes[i].onclick = function(){
          alert(i)
        }
      }

运行代码我们发现,不管点哪一个div都会弹出5,这是因为div节点的onclick事件时被异步触发的,当事件触发的时候,for循环早已结束,所以i的值都变成了5。
该怎么解决呢?使用闭包,把每次循环的i值都封闭起来,当在事件函数中,顺着作用域链中从内到外查找变量i时,会先找到被封闭在闭包环境中的i,如果有5个div,这里的i的值分别是0,1,2,3,4,如下:

var nodes = document.getElementsByTagName(\'div\');
      for(var i = 0,len = nodes.length;i<len;i++){
        (function(i){
          nodes[i].onclick = function(){
            alert(i)
          }
        })(i)
      }

同样的道理,我们还可以写一段检测类型的代码:

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)


    }
   console.log(Type.isArray([])) //true
   console.log(Type.isString(\"str\"))//true
   console.log(Type.isString([]))//false

这里的for循环只有两个条件,如果不太明白可以看下下面的链接
https://blog.csdn.net/weixin_39647035/article/details/103196019

3.1.1闭包的更多作用

1.封装变量
闭包可以帮助把一些不需要暴露在全局的变量封装成“私有变量”。假设有一个计算乘积的简单函数:

 var mult = function(){
    var a = 1;
    for(var i = 0,len = arguments.length;i<len;i++){
      a = a*arguments[i]
    }
    return a;
  }
  var multResult = mult(1,2,3,4,5)
  console.log(multResult)//120

mult函数接受一些number类型的参数,并返回这些参数的乘积。如果传入了相同的参数,我们可以加入缓存机制来提高这个函数的性能:

 var cache = {};
  var mult = function(){
    var args = Array.prototype.join.call(arguments,\',\');
    if(cache[args]){
      return \"从缓存中获取\"+cache[args]
    }
    var a = 1;
    for(var i = 0,len = arguments.length;i<len;i++){
      a = a*arguments[i]
    }
    return cache[args] = a;
  }
  console.log(mult(1,2,3))//6
  console.log(mult(1,2,3))//从缓存中获取6
  console.log(mult(1,2,3,6,4))//144

(上边缓存还有待优化。。。)
cache这个变量仅仅在mult函数中被使用,与其让cache变量跟mult函数一起平行地暴露在全局作用域下,不如把它封闭在mult函数内部,这样做可以减少页面中的全局变量,可以避免这个变量在其他地方被不小心修改而引发错误

var mult = (function(){
    var cache = {};
    return function(){
      var args = Array.prototype.join.call(arguments,\',\');
      if(args in cache){
        return \"从缓存中获取\"+cache[args]
      }
      var a = 1;
      for(var i = 0,len = arguments.length;i<len;i++){
      a = a*arguments[i]
    }
    return cache[args] = a;
    }
  })()
  console.log(mult(1,2,3))//6
  console.log(mult(1,2,3))//从缓存中获取6

提炼函数是代码重构中的一种常见技巧。如果在一个大函数中有一些代码块能够独立出来,我们常常把这些代码块封装在独立的小函数里面。独立出来的小函数有助于代码复用,如果这些小函数不需要在程序的其他地方使用,最好是把它们用闭包封闭起来:

var mult = (function(){
    var cache = {};
    var calculate = function(){
      var a = 1;
      for(var i = 0,len = arguments.length;i<len;i++){
        a = a*arguments[i]
      }
      return a;
    }
    return function(){
      var args = Array.prototype.join.call(arguments,\',\');
      if(args in cache){
        return \"从缓存中获取\"+cache[args]
      }
    return cache[args] = calculate.apply(null,arguments);
    }
  })()
  console.log(mult(1,2,3))//6
  console.log(mult(1,2,3))//从缓存中获取6

2.延续局部变量的寿命
在讲变量生存周期的时候,已经举过一个例子是可以延续局部变量的寿命的,接下来再看这个例子,img对象经常用于进行数据上报。

var report = function(src){
    var img = new Image();
    img.src = src
  }
  report(\'http://xxx.com/getUserInfo\')

但是通过查询后台的记录我们得知,因为一些低版本浏览器的实现存在bug,在这些浏览器下使用report函数进行数据上报会丢失30%左右的数据,也就是说,report并不是每一次都成功发起了HTTP请求。丢失数据的原因是img是report函数中的局部变量,当退出report以后,img可能会随即被销毁,而此时还没来得及发出HTTP请求,所以请求就会丢失掉。
现在我们把img变量用闭包封闭起来,便能解决请求丢失的问题:

var report = (function(){
    var imgs = [];
    return function(src){
      var img = new Image();
      imgs.push(img)
      console.log(imgs)
      img.src = src
    } 
  })()

3.1.2闭包和面向对象设计
过程与数据的结合时形容面向对象中的“对象”时经常使用的表达。对象以方法的形式包含了过程,而闭包则是在过程中以环境的形式包含了数据。通常面向对象思想能实现的功能,用闭包也能实现。看下下面这段跟闭包相关的代码:

var extent = function(){
    var value = 0;
    return {
      call:function(){
        value++;
        console.log(value)
      }
    }
  }
  var extent = extent();
  extent.call();//1
  extent.call();//2
  extent.call();//3

换成面向对象的写法:

  var extent = {
    value:0,
    call:function(){
      this.value++;
      console.log(this.value)
    }
  }
  extent.call();//1
  extent.call();//2
  extent.call();//3

或者

var Extent = function(){
  this.value = 0;
}
Extent.prototype.call = function(){
  this.value++;
  console.log(this.value)
}
var extent = new Extent();
extent.call();//1
extent.call();//2
extent.call();//3

3.1.3用闭包实现命令模式
先来看一段用面向对象的方式编写的命令模式的代码:

<button id=\"execute\">执行命令-打开电视</button>
<button id=\"undo\">执行命令-关闭电视</button>
 var Tv = {
      open:function(){
        console.log(\"open TV\")
      },
      close:function(){
        console.log(\"close TV\")
      }
    }
    var OpenTvCommand = function(receiver){
      this.receiver = receiver;
    }
    OpenTvCommand.prototype.excute = function(){
      this.receiver.open()
    }
    OpenTvCommand.prototype.undo = function(){
      this.receiver.close()
    }
    var setCommand = function(command){
      document.getElementById(\'execute\').onclick = function(){
        command.excute()
      }
      document.getElementById(\'undo\').onclick = function(){
        command.undo()
      }
    }
    setCommand(new OpenTvCommand(Tv))

命令模式的意图是把请求封装为对象,从而分离请求的发起者和请求的接收者(执行者)之间的耦合关系。在命令被执行之前,可以预先往命令对象中植入命令的接受者。
如果需要往函数对象中预先植入命令的接收者,那么闭包可以完成这个工作。在面向对象版本的命令模式中,预先植入的命令接收者被当成对象的属性保存起来;而在闭包版本的命令模式中,命令接收者会被封闭在闭包形成的环境中,代码如下:

var Tv = {
      open:function(){
        console.log(\"open TV\")
      },
      close:function(){
        console.log(\"close TV\")
      }
    }
    var createCommand = function(receiver){
      var excute = function(){
        return receiver.open();
      }
      var undo = function(){
        return receiver.close();
      }
      return {
        excute:excute,
        undo:undo
      }
    }
    var setCommand = function(command){
      document.getElementById(\'execute\').onclick = function(){
        command.excute()
      }
      document.getElementById(\'undo\').onclick = function(){
        command.undo()
      }
    }
    setCommand(new createCommand(Tv))

3.1.3闭包与内存管理
我们经常听到一种耸人听闻的说法“闭包会造成内存泄漏,所以要尽量减少闭包的使用”
使用闭包可以让局部变量一直生存下去,从这个意义上看,闭包的确会使一些数据无法被及时销毁。使用闭包的一部分原因是我们选择主动把一些变量封闭在闭包中,因为可能在以后还需要使用这些变量,把这些变量放在闭包中和放在全局作用域里,对内存的影响是一致的,这里不能说成是内存泄露。如果我们想回收这些变量,我们可以手动把这些变量设为null。
跟闭包和内存泄露有关系的地方是,使用闭包的同时比较容易形成循环引用,如果闭包作用域链中保存着一些DOM节点,这时候就有可能造成内存泄露。
同样,如果要解决循环引用带来的内存泄露问题,我们只需要把循环引用中的变量设为null。这意味着切断变量与它此前引用的值之间的连接。当垃圾收集器下次运行时,就会删除这些值并回收它们占用的内存。

3.2高阶函数

高阶函数是指至少满足下列条件之一的函数

  1. 函数可以作为参数被传递
  2. 函数可以作为返回值输出

在实际开发中,这两种情形都有很多应用场景,来看一下下面的应用场景。

3.2.1函数作为参数传递
把函数当作参数传递,这代表我们可以抽离出一部分容易变化的业务逻辑,把这部分业务逻辑放在函数参数中,这样一来可以分离业务代码中变化与不变的部分。其中一个重要的场景就是回调函数。

1.回调函数
在ajax异步请求的应用中,回调函数使用非常频繁:

 var getUserInfo = function(userId,callback){
  $.ajax(\'http://xxx.com/getUserInfo?\'+userId,function(data){
    if(typeof callback === \'function\'){
      callback(data)
    }
  })
}
getUserInfo(\"1001011\",function(data){
  alert(data.userName)
})

当然,回调函数不只能用在异步请求中,当一个函数不适合执行一些请求时,我们也可以把这些请求封装成一个函数。比如我们想在页面创建100个div节点,然后把这些div节点都设置为隐藏,可以用下面这种方式:

var appendDiv = function(){
  for(var i = 0;i<100;i++){
    var div = document.createElement(\'div\');
    div.innerHTML = i;
    document.body.appendChild(div);
    div.style.display = \'none\';
  }
}
appendDiv()

显然,把隐藏div的逻辑硬编码在appendDiv有些太个性化了,成为了一个难以复用的函数,所以我们要把div.style.display = \'none\';这行代码抽出来,用回调函数的形式传入appendDiv:

var appendDiv = function(callback){
  for(var i = 0;i<100;i++){
    var div = document.createElement(\'div\');
    div.innerHTML = i;
    document.body.appendChild(div);
    if(typeof callback === \'function\'){
      callback(div)
    }
  }
}
appendDiv(function(node){
  node.style.display = \'none\';
})

2.Array.prototype.sort
Array.prototype.sort有排序的功能,它可以接收一个函数当作参数,在函数里封装了排序的规则,从Array.prototype.sort中可以看出,我们的目的是对数组进行排序,这是不变的部分,而要用什么规则排序,这是可变的部分。我们把可变的部分封装在函数里,动态传入Array.prototype.sort,这样这个方法就变得非常灵活。

// 从小到大排序
var sort1 = [1,4,6,2].sort(function(a,b){
  return a - b;
})
// 从大到小排序
var sort2 = [1,4,6,2].sort(function(a,b){
  return b - a;
})
console.log(sort1);//[1, 2, 4, 6]
console.log(sort2);//[6, 4, 2, 1]

3.2.2函数作为返回值输出
相比把函数当作参数传递,函数当作返回值输出的应用场景也许更多,让函数继续返回一个可执行的函数,意味着运算过程是可延续。

1.判断数据的类型
判断一个数据是否是数组,在以往的实现中,可以给予鸭子类型的概念来判断,比如判断这个数据有没有length属性,有咩有sort方法或者slice方法等。更好的方式是用Object.prototype.toString来计算,Object.prototype.toString.call(obj)返回一个字符串,比如Object.prototype.toString.call([1,2,3])会返回\"[object Array]\",而Object.prototype.toString.call(“str”)总是返回\"[object String]\"。我们可以编写一系列的isType函数。代码如下:

 var isString = function(obj){
  return Object.prototype.toString.call(obj) === \'[object String]\'
}
var isArray = function(obj){
  return Object.prototype.toString.call(obj) === \'[object Array]\'
}
var isNumber = function(obj){
  return Object.prototype.toString.call(obj) === \'[object Number]\'
}

可以看出上面的代码中,大部分代码都是相同的,不同的只是Object.prototype.toString.call(obj)返回的字符串。为了避免多余的代码,我们尝试把这些字符串作为参数提前植入isType函数:

var isType = function(type){
  return function(obj){
    return Object.prototype.toString.call(obj) === \'[object \'+type+\']\'
  }
}
var isString = isType(\'String\');
var isArray = isType(\'Array\');
var isNumber = isType(\'Number\');
console.log(isString([1,2,3]));//false
console.log(isArray([1,2,3]));//true

或者可以写一个循环来批量注册isType函数:

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)
}
console.log(Type.isArray([]));//true
console.log(Type.isArray(\"str\"));//false

2.getSingle
下面是一个单例模式的例子:

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

这个高阶函数的例子,即把函数当作参数传递,又让函数执行后返回了另一个函数,可以看看getSingle函数的效果。

var getScript = getSingle(function(){
  return document.createElement(\'script\');
});
var script1 = getScript();
var script2 = getScript();

alert(script1 === script2)//true

3.2.3高阶函数实现AOP
AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,例如日志统计、安全控制、异常处理等。把这些功能抽离出来以后,再通过“动态织入”的方式掺入业务逻辑模块中。这样做,可以保持业务逻辑模块的纯净和高内聚性,其次是可以很方便地复用日志统计等功能模块。
我们通过扩展Function.prototype来做这一点:

 Function.prototype.before = function(beforefn){
  var _self = this;//保存原函数的引用
  return function(){//返回包含了原函数和新函数的“代理”函数
    beforefn.apply(this,arguments);//执行新函数,修正this
    return _self.apply(this,arguments);//执行原函数
  }
}
Function.prototype.after = function(afterfn){
  var _self = this;//保存原函数的引用
  return function(){//返回包含了原函数和新函数的“代理”函数
    afterfn.apply(this,arguments);//执行新函数,修正this
    return _self.apply(this,arguments);//执行原函数
  }
}
var func = function(){
  console.log(2);
}
func = func.before(function(){
  console.log(1)
}).after(function(){
  console.log(3)
})
func();

我们把负责打印数字1和打印数字3的两个函数通过AOP的方式动态植入func函数。通过执行func(),控制台顺利地返回了执行结果
在这里插入图片描述

3.2.3高阶函数其他应用

1.currying
currying又称为部分求值,一个currying的函数首先会接受一些参数,接受了参数以后并不会立即求值,而是继续返回另一个函数,刚才传入的参数在函数形成的闭包中被保存起来。等到函数真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。
假如我们要写一个计算每月开销的函数,在每天结束之前,我们都要记录今天花掉多少钱。代码如下:

 var monthlyCost = 0;
var cost = function(money){
  monthlyCost += money;
}
cost(100);
cost(200);
cost(300);
alert(monthlyCost)//600

通过这段代码可以看到,每天结束后我们都会记录并计算到今天为止花掉的钱。我们并不太关心每天花了多少钱,只想知道到月底的时候会花掉多少钱。也就是说,实际上只需要在月底计算一次。
我们想要的效果是,每个月钱29天我们都只是保存好当天的开销,直到第30天才进行求值计算,下面的代码不是一个currying函数的完整实现,但有助于我们理解思想:

var cost = (function(){
  var args = [];
  return function(){
    if(arguments.length === 0){
      var money = 0;
      for(var i = 0,l = args.length;i<l;i++){
        money+=args[i];
      }
      return money
    }else{
      [].push.apply(args,arguments)
    }
  }
})()
console.log(cost(100));//undefined
console.log(cost(200));//undefined
console.log(cost(300));//undefined
console.log(cost());//600

接下来我们要编写一个通用的function currying(){}代码如下:

var currying = function(fn){
  var args = [];
  return function(){
    if(arguments.length === 0){
      return fn.apply(this,args) 
    }else{
      [].push.apply(args,arguments);
      return arguments.callee;
    }
  }
}
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);
cost(100);
cost(200);
cost(300);
console.log(cost());//600

至此,我们完成了一个currying函数,当调用cost的时候,如果明确带一些参数,表示现在不是真正的求值,而是要把这些参数保存下来,此时让cost返回另一个函数,只有不传参数时。才能进行最后的求值计算。
2.uncurrying
在上一章我们讲了call 和apply,可以看到一个对象未必只能使用它自身的方法,比如我们常常让类数组对象去借用Array.prototype的方法,最常见的场景之一:

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

这样一来,方法中用到this的地方就不再局限于原来规定的对象,而是加以泛化并得到更广的适用性。那么有没有办法把泛化this的过程提取出来呢?uncurrying就是用来解决这个问题的。
下面是uncurrying实现方法之一:

Function.prototype.uncurrying = function(){
  var self = this;
  return function(){
    var obj = Array.prototype.shift.call(arguments);
    return self.apply(obj,arguments)
  }
}

先不要纠结这段代码的原理,看看它有什么作用吧。
在类数组对象arguments借用Array.prototype的方法之前,先把Array.prototype.push.call这句代码转换为一个通用的push函数:

Function.prototype.uncurrying = function(){
  var self = this;
  return function(){
    var obj = Array.prototype.shift.call(arguments);
    return self.apply(obj,arguments)
  }
}
var push = Array.prototype.push.uncurrying();
(function(){
  push(arguments,4)
  console.log(arguments)//1,2,3,4
})(1,2,3)

通过uncurrying的方式,Array.prototype.push.call变成了一个通用的pu sh函数,这样一来push函数的作用就跟Array.prototype.push一样了。
我们还可以一次性地把Array.prototype上的方法“复制”到array对象上,当然这些方法可操作的对象也不仅仅只是array对象:

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)
console.log(\"-------------\")
var first = Array.shift(obj)
console.log(first);
console.log(first,obj);
console.log(\"-------------\")
Array.forEach(obj,function(i,n){
  console.log(i,n)
});

现在我们分析一下调用Array.prototype.push.uncurrying()这句代码时发生了什么事情:

Function.prototype.uncurrying = function(){
  var self = this;
  return function(){
    var obj = Array.prototype.shift.call(arguments);
    // obj是{
    // \"length\":1,
    // \"0\":1
    // }
    // arguments对象的第一个元素被截去,剩下[2]
    return self.apply(obj,arguments);
    // 相当于Array.prototype.push.apply(obj,2);
  }
}
var push = Array.prototype.push.uncurrying();
var obj = {
  \"length\":1,
  \"0\":1
}
push(obj,2);
console.log(obj);//输出

在这里插入图片描述
3.函数节流
JavaScript中的函数大多数情况下都是由用户主动调用触发的,除非是函数本身的实现不合理,否则我们一般不会遇到跟性能相关的问题。但在一些少数情况下,函数的触发不是由用户直接控制的。在这些场景下,函数有可能被非常频繁地调用,而造成大的性能问题。
(1)函数被频繁调用的场景

  1. window.onresize事件。我们给window对象绑定了resize事件,当浏览器窗口大小被拖动而改变的时候,这个事件触发的频率非常之高。如果哦我们在window.onresize事件函数里加一些跟DOM节点相关的操作,因为DOM节点是很消耗性能的,这时候浏览器可能会出现卡顿现象。
  2. mousemove事件。同样,如果我们给一个div节点绑定了拖拽事件,当div节点被拖动的时候,也会频繁地触发该拖拽事件函数。
  3. 上传进度。微云的上传功能使用了公司提供的一个浏览器插件。该浏览器插件在真正开始上传文件之前,会对文件进行扫描并随时通知JavaScript函数,以便在页面中显示当前的扫描进度。但该插件通知的频率非常之高,大约一秒钟10次,很显然我们在页面中不需要如此频繁的去提示用户。

(2)函数节流的原理
上面的三个场景,发现它们面临的共同问题是函数被触发的频率太高。
比如我们在window.onresize事件中要打印当前的浏览器窗口大小,在我们通过拖拽来改变窗口大小的时候,打印窗口大小的工作1秒钟进行了10次。但是我们实际上只需要2次或者3次,这就需要我们按时间段来忽略一些事件请求,比如确保在500ms内打印一次。很显然,我们可以借助setTimeout来完成这件事。
(3)函数节流的代码实现
我们来看一下下面这段代码:

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)

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

4.分时函数
假如,我们需要创建webQQ的QQ好友列表。列表中通常会有成百上千个好友,如果一个好友是一个节点,那我们在渲染列表时,会一次性往页面中创建成百上千个节点。
短时间内往页面中大量添加DOM节点可能会让浏览器卡顿甚至时假死,代码如下:

var ary = [];
for(var i = 1;i<=1000;i++){
  ary.push(i);
}
var renserFriendList = function(data){
  for(var i = 0,l = data.length;i < l;i++){
    var div = document.createElement(\'div\');
    div.innerHTML = i;
    document.body.appendChild(div)
  }
}
renserFriendList(ary)

要解决这个问题,我们可以使用下面的timeChunk函数:

var timeChunk = function(ary,fn,count){
  var obj,t;
  var len = ary.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);//分批执行的时间间隔,也可以用参数的形式传入
  }
}

timeChunk函数让创建节点的工作分批进行,比如把1秒钟创建1000个节点,改为每隔200毫秒创建8个节点。
假设我们有1000个好友数据,接下来使用一下timeChunk。

var ary = [];
for(var i = 1;i<=1000;i++){
  ary.push(i);
}
var renserFriendList = timeChunk(ary,function(n){
  var div = document.createElement(\'div\');
    div.innerHTML = n;
    document.body.appendChild(div)
},8)
renserFriendList()

5.惰性加载函数
在web开发中,因为浏览器之间的实现差异,一些嗅探工作总时不可避免。比如我们需要一个在各个浏览器中能够通用的事件来绑定函数addEvent,常见的写法:

var addEvent = function(elem,type,handler){
  if(window.addEventListener){
    return elem.addEventListener(type,handler,false);
  }
  if(window.attachEvent){
    return elem.attachEvent(\'on\'+type,handler)
  }
}

这个函数每次调用的时候都会执行里面的if条件分支,虽然执行这些if分支的开销不算大,但有方法可以让程序避免这些重复的执行过程。
第二种方案,我们把嗅探浏览器的操作提前到代码加载的时候,在代码加载的时候就立刻进行一次判断,以便让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,如果这样的话这就属于多余操作,而且会稍稍延长页面呢ready时间。
第三种方案就是我们将要讨论的惰性载入函数方案,此时addEvent依然被声明为一个普通的函数,在函数里依然会有一些分支的判断,但是在第一次进入条件分支后,在函数内部会重写这两个函数,重写之后的函数就是我们期望的addEvent函数,在下一次进入addEvent函数的时候,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);
    }
  }
  addEvent(elem,type,handler);
}
var div = document.getElementById(\'div1\')
addEvent(div,\'click\',function(){
  alert(1)
})
addEvent(div,\'click\',function(){
  alert(2)
})

来源:https://blog.csdn.net/weixin_37365539/article/details/123027354
本站部分图文来源于网络,如有侵权请联系删除。

未经允许不得转载:百木园 » 《javascript设计模式与开发实践》——第三章(闭包和高阶函数)学习记录

相关推荐

  • 暂无文章