资讯专栏INFORMATION COLUMN

JavaScript设计模式与开发实践 | 03 - 闭包和高阶函数

Leck1e / 2358人阅读

摘要:闭包闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。在这里产生了一个闭包结构,局部变量的生命周期被延续了。本节内容为设计模式与开发实践第三章笔记。

闭包

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

创建闭包的常见方式,就是在一个函数内部创建另一个函数。闭包的形成与变量的作用域以及变量的生存周期有关。

变量的作用域

变量的作用域就是指变量的有效范围。

当在函数中声明一个变量时,如果变量前面没有带上关键字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

提炼函数是代码重构中的一种常见技巧。如果在一个大函数中有一些代码能够独立出来,就把这些代码封装在独立的小函数里。独立出来的小函数有助于代码服用。

var mult = (function(){
  var cache = {};
  var calculate = function(){
      var a = 1;
      for(var i=0, l=arguments.length; i

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设计模式与开发实践》第三章 笔记。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/86175.html

相关文章

  • JS学习理解之闭包高阶函数

    摘要:闭包的形成与变量的作用域以及变量的生存周期密切相关。现在我们把变量用闭包封闭起来,便能解决请求丢失的问题二高阶函数高阶函数是指至少满足下列条件之一的函数。回调函数在异步请求的应用中,回调函数的使用非常频繁。 一、闭包 对于 JavaScript 程序员来说,闭包(closure)是一个难懂又必须征服的概念。闭包的形成与变量的作用域以及变量的生存周期密切相关。下面我们先简单了解这两个知识...

    张红新 评论0 收藏0
  • 前端进击的巨人(四):略知函数式编程

    摘要:自执行函数闭包实现模块化以乐之名程序员产品经理对作用域,以及闭包知识还没掌握的小伙伴,可回阅前端进击的巨人三从作用域走进闭包。参考文档利用闭包实现模块化翻译浅谈中的高阶函数系列更文请关注专栏前端进击的巨人,不断更新中。。。 系列更文前三篇文章,围绕了一个重要的知识点:函数。函数调用栈、函数执行上下文、函数作用域到闭包。可见不理解函数式编程,代码都撸不好。 showImg(https:/...

    omgdog 评论0 收藏0
  • JavaScript深入浅出

    摘要:理解的函数基础要搞好深入浅出原型使用原型模型,虽然这经常被当作缺点提及,但是只要善于运用,其实基于原型的继承模型比传统的类继承还要强大。中文指南基本操作指南二继续熟悉的几对方法,包括,,。商业转载请联系作者获得授权,非商业转载请注明出处。 怎样使用 this 因为本人属于伪前端,因此文中只看懂了 8 成左右,希望能够给大家带来帮助....(据说是阿里的前端妹子写的) this 的值到底...

    blair 评论0 收藏0
  • JS程序

    摘要:设计模式是以面向对象编程为基础的,的面向对象编程和传统的的面向对象编程有些差别,这让我一开始接触的时候感到十分痛苦,但是这只能靠自己慢慢积累慢慢思考。想继续了解设计模式必须要先搞懂面向对象编程,否则只会让你自己更痛苦。 JavaScript 中的构造函数 学习总结。知识只有分享才有存在的意义。 是时候替换你的 for 循环大法了~ 《小分享》JavaScript中数组的那些迭代方法~ ...

    melody_lql 评论0 收藏0
  • JavaScript设计模式

    摘要:可能因为先入为主,在编程之中,往往不由自主地以的逻辑编程思路设计模式进行开发。这是原型模式很重要的一条原则。关于闭包与内存泄露的问题,请移步原型模式闭包与高阶函数应该可以说是设计模式的基础要领吧。在下一章,再分享一下的几种常用设计模式。 前 在学习使用Javascript之前,我的程序猿生涯里面仅有接触的编程语言是C#跟Java——忽略当年在大学补考了N次的C与VB。 从静态编程语言,...

    keke 评论0 收藏0

发表评论

0条评论

Leck1e

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<