前期准备:最好要稍微了解Js的作用域范畴,Js中变量定义
一个问题
例一:假设要写动态生成HTML的函数,HTML有一部分是固定的。
-
方案①
function buildHtml(args) { var template = ['<table><tr><td>', '', '</td><td>', '', '<td></td></tr></table>']; template[1] = args[0]; template[3] = args[1]; return template.join(''); } 执行函数时,先定义template,执行完销毁template,多次执行就多次重复定义和销毁,这样不好,考虑把template变量放到生成Html函数的外面,改造后成了这样:
-
方案②
//... var template = ['<table><tr><td>', '', '</td><td>', '', '<td></td></tr></table>']; function buildHtml(args){ template[1] = args[0]; template[3] = args[1]; return template.join(''); } //...这里可以调用上面的代码 当其它页面或模块也需要这个生成Html函数时,拷贝相关代码时,要保证template变量名和buildHtml函数名不能与已有的命名空间冲突,还可以这样实现:
-
方案③
//... buildHtml.template = ['<table><tr><td>', '', '</td><td>', '', '<td></td></tr></table>']; function buildHtml(args){ var template = buildHtml.template; template[1] = args[0]; template[3] = args[1]; return template.join(''); } //...这里可以调用上面的代码
-
方案④
var buildHtml = (function(){ var template = ['<table><tr><td>', '', '</td><td>', '', '<td></td></tr></table>']; return function(args){ template[1] = args[0]; template[3] = args[1]; return template.join(''); } })();
方案3和方案4都只用考虑其中一个变量名是否冲突。方案4 结构更紧凑,且template私有化,外面无法访问,这里就用到了闭包。
闭包定义
在一个函数内定义一个函数,并且这个内部函数可以在外面访问,这时候就形成了闭包(Closure)。
方案④是立执行函数,函数内部变量template在函数执行完后,仍保留在内存中,等待内部函数调用,当buildHtml=null时,才被内存回收。同理,当外部函数执行完毕后,若内部函数就不能访问了,就不形成闭包。
那怎样实现访问内部函数,通过return将内部引用暴露给外部?将内部函数赋给全局变量?全局变量可以是未经var声明而即时产生的,也可能是传入的实参,如例四。
例二:
function wrapFns (){
var arr = [];
for(var i = 10;i--;){
arr[i] = function(){
return i;
}
}
return arr;
}
var fns = wrapFns();
console.log(fns[10]()); // 值是多少? 内部函数对象,通过隐藏属性引用[[scope]],访问指向的作用域链各个活动对象。在这里,第一个活动对象就是wrapFns的上下文,所以当执行wrapFns时,访问到的i已经为0了,输出值是:0。
闭包的内部机制
为什么函数的局部变量会被维持?闭包的内部机制是怎样的?函数嵌套很多层时的闭包是怎样的?
-
函数也是对象,有[[scope]]属性(只能内部通过JavaScript引擎访问),指向函数定义时的执行环境上下文。
-
假如A是全局的函数,B是A的内部函数。执行A函数时,当前执行环境的上下文指向一个作用域链。作用域链的第一个对象是当前函数的活动对象(this、参数、局部变量),第二个对象是全局window。
-
当执行代码运行到B定义地方, 设置函数B的[[scope]]属性指向执行环境的上下文作用域链。
-
执行A函数完毕后,若内部函数B的引用没外暴,A函数活动对象将被Js垃圾回收处理;反之,则维持,形成闭包。
-
调用函数B时,JavaScript引擎将当前执行环境入栈,生成新的执行环境,新的执行环境的上下文指向一个作用域链,由当前活动对象+函数B的[[scope]]组成,链的第一个对象是当前函数的活动对象(this、参数、局部变量组成),第二个活动对象是A函数产生的,第三个window。
-
B函数里面访问一个变量,要进行标志符解析(JavaScript原型也有标识符解析),它从当前上下文指向的作用域链的第一个对象开始查找,找不到就查找第二个对象,直到找到相关值就立即返回,如果还没找到,报undefined错误。
-
当有关A函数的外暴的内部引用全部被消除时,A的活动对象才被销毁。
在函数内部声明的变量在函数外部无法访问,但在内嵌函数中是可以访问的。
如果B函数引用给外部,它的[[scope]]中的活动对象,比如父函数A的活动对象会被维持,当B函数再次调用时,仍然可以读写,这样可实现数据的本地存储,并且只能由你这个接口访问。
作用域链临时变更情况
- 如使用with(obj){},try{}catch(obj){},会将obj添加到作用域链的首部,其它活动对象相应后移一位,直到with结束。
- 使用Function定义的函数对象,函数对象的[[scope]]属性置为window。
闭包的其它理解
至于闭包的形成,有些说法是由于语法域,最近面试时,有位面试官也因此直接否定我的理解。我也看过类似博客,但介绍得不够全面,无法形成系统的理解,在最底层编译时,闭包有可能是因为语法域…
闭包的应用
封装插件
(function(window,undefined){
window.ymPrompt = {}; //
})(window);
保存私有属性
var factorial = (function () {
var cache = [];
return function (num) {
if (!cache[num]) {
if (num == 0) {
cache[num] = 1;
}
cache[num] = num * factorial(num - 1);
}
return cache[num];
}
})(); ### 回调函数的包装 回调函数的参数是固定的,如何摆脱这个限制呢?
setTimeout((function(xx){
//...
return function(){
// ...
}
})(xx),0); setTimeout的回调函数是无参数,如果要让回调能传参,就像上面那样写。
当然,我们更常见的写法如下,这时就没用到闭包了。
setTimeout(function(){
funs(xx);
},0);
那些不经意间用了闭包
-
Demo1
var object = { name:"My Object", getNameFunc: function(){ var that = this; return function(){ return that.name; }; } }; console.log(object.getNameFunc()());// My Object
-
Demo2
(function(w,undefined){ var temp = ['a'], domRefer = document.getElementById(''); domRefer.addEventListener('click',function(event){ //... },false); })(window)
使用闭包注意
-
只有需要时才使用闭包,闭包会导致一些东西常驻内存。
一个这样的构造函数
function Construt(){ var bb; this.aa = '1'; this.fun1 = function(){}; this.fun2 = function(){}; } 在fun1和fun2中没用到bb,就不要用闭包。 function Construt(){ this.aa = '1'; } Construt.prototype={ constructor:Construt, fun1:function(){}, fun2:function(){} }
-
使用闭包时,可以将不需继续调用局部变量在父函数末尾置为null。
-
Js的作用域是函数,访问或修改一变量时,会沿着作用域链一层层向上找。所以如果有重复使用的变量,最好赋值给局部变量。
最后要说的
一个函数,能访问什么变量,看的是定义在什么地方,而不是执行的位置。
// 外部定义的函数,在一函数中被返回并执行,执行时,并无闭包功能
function inside() {
return x;
}
function outside(){
var x = 1;
return inside;
}
var xx = outside()();
想知道结果,点击链接,打开调试器即可。