JavaScript设想形式与开辟实践 | 03 - 闭包和高阶函数

闭包

闭包是指有权接见另一个函数作用域中的变量的函数。

建立闭包的罕见体式格局,就是在一个函数内部建立另一个函数。闭包的构成与变量的作用域以及变量的生计周期有关。

变量的作用域

变量的作用域就是指变量的有用局限。

当在函数中声明一个变量时,假如变量前面没有带上关键字var,这个变量就会成为全局变量;假如用var关键字在函数中声明变量,这个变量就是局部变量,只要在该函数内部才接见到这个变量,在函数外部是接见不到的。

在JavaScript中,函数可以用来制造函数作用域。在函数内里可以看到表面的变量,而在函数表面则没法看到函数内里的变量。这是因为当在函数中搜刮一个变量的时刻,假如该函数内并没有声明这个变量,那末此次搜刮的历程会跟着代码的实行环境建立的作用域链往外层逐层搜刮,一向搜刮到全局对象。变量的搜刮是从内到外的。

var a = 1;

var func1 = function(){
  var b = 2;
  var func2 = function(){
      var c = 3;
      console.log(b);  // 输出:2
      console.log(c);  // 输出:1
  }
  func2();
  console.log(c);  // 变量c在函数内部,是局部变量,此时在外部接见不到。 输出:Uncaught ReferenceError: c is not defined
};

func1();

变量的生计周期

全局变量的生计周期是永远的,除非我们主动烧毁这个全局变量。而在函数内用var关键字声明的局部变量,当退出函数时,这些局部变量即失去了它们的代价,会跟着函数挪用的完毕而被烧毁:

var func = function(){
  var a = 1;  // 退出函数后局部变量a将被烧毁
  console.log(a);  // 输出:1
};

func();

然则,有一种状况却跟我们的推论相反。

var func = function(){
  var a = 1;  //函数外部接见不到局部变量a,退出函数后,局部变量a被烧毁
  console.log(a);  // 输出:1
};

func();  
console.log(a);  // 输出:Uncaught ReferenceError: a is not defined


var func = function(){
  var a = 1;
  return function(){
      a++;
      console.log(a);
  }
};

var f = func();

f();  // 输出:2
f();  // 输出:3
f();  // 输出:4
f();  // 输出:5

当退出函数后,局部变量a并没有消逝,而是好像一向在某个处所存在世。这是因为当实行 var f = func(); 时,f返回了一个匿名函数的援用,它可以接见到func()被挪用时发生的环境,而规划变量a一向处在这个环境里。既然局部变量地点的环境还能被外界接见,这个局部变量就有了不被烧毁的来由。在这里发生了一个闭包构造,局部变量的生命周期被连续了。

闭包的作用

  • 封装变量

  • 连续局部变量的寿命

1. 封装变量

闭包可以协助把一些不须要暴露在全局的变量封装成“私有变量”。

假设有一个盘算乘积的函数:

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, l=arguments.length; i< l; i++){
      a = a * arguments[i];
  }
  return cache[args] = a;
};

console.log(mult(1,2,3));  // 输出:6
console.log(mult(1,2,3));  // 输出:6

我们看到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, l=arguments.length; i<l; 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, l=arguments.length; i<l; 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);
    img.src = src;
  }
})();

闭包和面向对象设想

历程与数据的连系是描述面向对象中的“对象”时常常运用的表达。对象以要领的情势包含了历程,而闭包则是在历程当中以环境的情势包含了数据。通经常使用面临对象头脑能完成的功用,用闭包也能完成,反之亦然。

看看这段面向对象写法的代码:

var extent = {
  value: 0;
  call: function(){
    this.value++;    
    console.log(this.value);
  }
};

// 作为对象的要领挪用,this指向该对象
extent.call();  // 输出:1
extent.call();  // 输出:2
extent.call();  // 输出:3

换成闭包的写法以下:

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

闭包与内存治理

局部变量原本应该在函数退出的时刻就被消除援用,但假如局部变量被关闭在闭包构成的环境中,那末这个局部变量就可以一向生计下去。从这个意义上看,闭包确实会使一些数据没法被实时烧毁。运用闭包的一部份原因是我们挑选主动把一些变量关闭在闭包中,因为能够在今后还须要运用这些变量,把这些对象放在闭包中和放在全局作用域中,对内存方面的影响是一致的。假如在未来须要接纳这些变量,可以手动把变量设为null。

运用闭包的同时比较轻易形成轮回援用,假如闭包的作用域链中保留着一些DOM节点,这时刻就有能够形成内存泄漏。但这自身并不是闭包的题目,也并不是JavaScript的题目。在IE浏览器中,因为BOM和DOM中的对象是运用C++以COM对象的体式格局完成的,而COM对象的渣滓网络机制采纳的是援用计数战略。在基于援用计数战略的渣滓接纳机制中,假如两个对象之间构成了轮回援用,那末这两个对象都没法被接纳,但轮回援用形成的内存泄漏在本质上也不是闭包形成的。

假如要处置惩罚轮回援用带来的内存泄漏题目,我们只须要把轮回援用中的变量设为null。将变量设为null,意味着割断变量与它之前援用的值之间的衔接。当渣滓网络器下次运行时,就会删除这些值并接纳它们占用的内存。

高阶函数

定义

高阶函数是指最少满足以下前提之一的函数:

  • 函数可以作为参数被通报;

  • 函数可以作为返回值输出。

函数作为参数通报

1. 回调函数

在ajax异步要求的运用中,回调函数的运用非常频仍。当我们想在ajax要求返回以后做一些事变,但又不知道要求返回的确实时候时,最罕见的计划就是把callback函数看成参数传入提议的ajax要求的要领中,待要求完成以后实行callback函数:

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

getUserInfo(13157, function(data){
  console.log(data.userName);
});

回调函数的运用不仅只在异步要求中,当一个函数不适合实行一些要求时,我们也可以把这些要求封装成一个函数,并把它作为参数通报给另一个函数,“托付”给另一个函数来实行。

2. Array.prototype.sort

Array.prototype.sort接收一个函数看成参数,这个函数内里封装了数组元素的排序划定规矩。从Array.prototype.sort的运用可以看到,我们的目标是对数组举行排序,这是稳定的部份;而运用什么划定规矩去排序,则是可变的部份。把可变的部份封装在函数参数里,动态传入Array.prototype.sort,使Array.prototype.sort要领成为了一个非常天真的要领。

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

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

函数作为返回值输出

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 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(isArray([1,2,3]));   // 输出:true

2. getSingle

有一种设想形式叫单例形式,下面是它的例子:

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

console.log(script1 === script2);  // 输出:true

这个高阶函数的例子,既把函数看成参数通报,又让函数实行后返回了另一个函数。

高阶函数完成AOP

AOP(面向切面编程)的重要作用是把一些跟中心营业逻辑模块无关的功用抽离出来,这些跟营业逻辑无关的功用一般包含日记统计、平安掌握、非常处置惩罚等。把这些功用抽离处置惩罚以后,再经由过程“动态织入”的体式格局掺入营业逻辑模块中。如许做的优点首先是可以坚持营业逻辑模块的纯洁和高内聚性,其次是可以很方便地复用日记统计等功用模块。

在JavaScript中完成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(){
      var ret = __self.apply(this, arguments);
      afterfn.apply(this, arguments);
      return ret;
  }
};

var func = function(){
  console.log(2);
};

func = func.beforefn(function(){
  console.log(1);
}).after(function(){
  console.log(3);
});

func();

这类运用AOP的体式格局来给函数增加职责,也是JavaScript言语中一种非常迥殊和奇妙的装潢者形式完成。

PS:本节内容为《JavaScript设想形式与开辟实践》第三章 笔记。

    原文作者:small2
    原文地址: https://segmentfault.com/a/1190000003985658
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞