闭包是指有权访问另外一个函数做用域中的变量的函数。ajax
建立闭包的常见方式,就是在一个函数内部建立另外一个函数。闭包的造成与变量的做用域以及变量的生存周期有关。编程
变量的做用域就是指变量的有效范围。设计模式
当在函数中声明一个变量时,若是变量前面没有带上关键字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一直处在这个环境里。既然局部变量所在的环境还能被外界访问,这个局部变量就有了不被销毁的理由。在这里产生了一个闭包结构,局部变量的生命周期被延续了。app
封装变量异步
延续局部变量的寿命函数
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(面向切面编程)的主要做用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能一般包括日志统计、安全控制、异常处理等。把这些功能抽离处理以后,再经过“动态织入”的方式掺入业务逻辑模块中。这样作的好处首先是能够保持业务逻辑模块的纯净和高内聚性,其次是能够很方便地复用日志统计等功能模块。
在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设计模式与开发实践》第三章 笔记。