东莞网站seo方法,建设网站项目的目的,苏州网页制作报价,一个商城网站开发周期JavaScript深入理解—-闭包(Closures) 概要 本文将介绍一个在JavaScript经常会拿来讨论的话题 —— 闭包#xff08;closure#xff09;。闭包其实已经是个老生常谈的话题了#xff1b; 有大量文章都介绍过闭包的内容#xff0c;尽管如此#xff0c;这里还是要试着从理论角…JavaScript深入理解—-闭包(Closures) 概要 本文将介绍一个在JavaScript经常会拿来讨论的话题 —— 闭包closure。闭包其实已经是个老生常谈的话题了 有大量文章都介绍过闭包的内容尽管如此这里还是要试着从理论角度来讨论下闭包看看ECMAScript中的闭包内部究竟是如何工作的。 正如在此前文章中提到的这些文章都是系列文章相互之间都是有关联的。因此为了更好的理解本文要介绍的内容 建议先去阅读下第四章 - 作用域链和 第二章 - 变量对象。 概论 在讨论ECMAScript闭包之前先来介绍下函数式编程与ECMA-262-3 标准无关中一些基本定义。 然而为了更好的解释这些定义这里还是拿ECMAScript来举例。 众所周知在函数式语言中ECMAScript也支持这种风格函数即是数据。就比方说函数可以保存在变量中可以当参数传递给其他函数还可以当返回值返回等等。 这类函数有特殊的名字和结构。 定义 函数式参数“Funarg” —— 是指值为函数的参数。 如下例子 function exampleFunc(funArg) {funArg();
}exampleFunc(function () {alert(funArg);
});
复制代码上述例子中funarg的实参是一个传递给exampleFunc的匿名函数。 反过来接受函数式参数的函数称为 高阶函数high-order function 简称HOF。还可以称作函数式函数 或者 偏数理的叫法操作符函数。 上述例子中exampleFunc 就是这样的函数。 此前提到的函数不仅可以作为参数还可以作为返回值。这类以函数为返回值的函数称为 _带函数值的函数functions with functional value or function valued functions。 (function selfApplicative(funArg) {if (funArg funArg selfApplicative) {alert(self-applicative);return;}selfApplicative(selfApplicative);})();
复制代码以自己为返回值的函数称为 自复制函数auto-replicative function 或者 self-replicative function。 通常“自复制”这个词用在文学作品中 (function selfReplicative() {return selfReplicative;
})();
复制代码在函数式参数中定义的变量在“funarg”激活时就能够访问了因为存储上下文数据的变量对象每次在进入上下文的时候就创建出来了 function testFn(funArg) {// 激活funarg, 本地变量localVar可访问funArg(10); // 20funArg(20); // 30}testFn(function (arg) {var localVar 10;alert(arg localVar);});
复制代码然而我们知道特别在第四章中提到的在ECMAScript中函数是可以封装在父函数中的并可以使用父函数上下文的变量。 这个特性会引发 funarg问题。 Funarg问题 在面向堆栈的编程语言中函数的本地变量都是保存在 堆栈上的 每当函数激活的时候这些变量和函数参数都会压栈到该堆栈上。 当函数返回的时候这些参数又会从堆栈中移除。这种模型对将函数作为函数式值使用的时候有很大的限制比方说作为返回值从父函数中返回。 绝大部分情况下问题会出现在当函数有 自由变量的时候。 自由变量是指在函数中使用的但既不是函数参数也不是函数的局部变量的变量 如下所示 function testFn() {var localVar 10;function innerFn(innerParam) {alert(innerParam localVar);}return innerFn;
}var someFn testFn();
someFn(20); // 30
复制代码上述例子中对于innerFn函数来说localVar就属于自由变量。 对于采用 面向堆栈模型来存储局部变量的系统而言就意味着当testFn函数调用结束后其局部变量都会从堆栈中移除。 这样一来当从外部对innerFn进行函数调用的时候就会发生错误因为localVar变量已经不存在了。 而且上述例子在 面向堆栈实现模型中要想将innerFn以返回值返回根本是不可能的。 因为它也是testFn函数的局部变量也会随着testFn的返回而移除。 还有一个函数对象问题和当系统采用动态作用域函数作为函数参数使用的时候有关。 看如下例子伪代码 var z 10;function foo() {alert(z);
}foo(); // 10 – 静态作用域和动态作用域情况下都是(function () {var z 20;foo(); // 10 – 静态作用域情况下, 20 – 动态作用域情况下})();// 将foo函数以参数传递情况也是一样的(function (funArg) {var z 30;funArg(); // 10 – 静态作用域情况下, 30 – 动态作用域情况下})(foo);
复制代码我们看到采用动态作用域变量标识符处理是通过动态堆栈来管理的。 因此自由变量是在当前活跃的动态链中查询的而不是在函数创建的时候保存起来的静态作用域链中查询的。 这样就会产生冲突。比方说即使Z仍然存在与之前从堆栈中移除变量的例子相反还是会有这样一个问题 在不同的函数调用中Z的值到底取哪个呢从哪个上下文哪个作用域中查询 上述描述的就是两类 funarg问题 —— 取决于是否将函数以返回值返回第一类问题以及是否将函数当函数参数使用第二类问题。 为了解决上述问题就引入了 闭包的概念。 闭包 闭包是代码块和创建该代码块的上下文中数据的结合。 让我们来看下面这个例子伪代码 var x 20;function foo() {alert(x); // 自由变量 x 20
}// foo的闭包
fooClosure {call: foo // 对函数的引用lexicalEnvironment: {x: 20} // 查询自由变量的上下文
};
复制代码上述例子中“fooClosure”部分是伪代码。对应的在ECMAScript中“foo”函数已经有了一个内部属性——创建该函数上下文的作用域链。 这里“lexical”是不言而喻的通常是省略的。上述例子中是为了强调在闭包创建的同时上下文的数据就会保存起来。 当下次调用该函数的时候自由变量就可以在保存的闭包上下文中找到了正如上述代码所示变量“z”的值总是10。 定义中我们使用的比较广义的词 —— “代码块”然而通常在ECMAScript中会使用我们经常用到的函数。 当然了并不是所有对闭包的实现都会将闭包和函数绑在一起比方说在Ruby语言中闭包就有可能是 一个程序对象procedure object, 一个lambda表达式或者是代码块。 对于要实现将局部变量在上下文销毁后仍然保存下来基于堆栈的实现显然是不适用的因为与基于堆栈的结构相矛盾。 因此在这种情况下上层作用域的闭包数据是通过 动态分配内存的方式来实现的基于“堆”的实现配合使用垃圾回收器garbage collector简称GC和 引用计数reference counting。 这种实现方式比基于堆栈的实现性能要低然而任何一种实现总是可以优化的 可以分析函数是否使用了自由变量函数式参数或者函数式值然后根据情况来决定 —— 是将数据存放在堆栈中还是堆中。 ECMAScript闭包的实现 讨论完理论部分接下来让我们来介绍下ECMAScript中闭包究竟是如何实现的。 这里还是有必要再次强调下ECMAScript只使用静态词法作用域而诸如Perl这样的语言既可以使用静态作用域也可以使用动态作用域进行变量声明。 var x 10;function foo() {alert(x);
}(function (funArg) {var x 20;// funArg的变量 x 是静态保存的在该函数创建的时候就保存了funArg(); // 10, 而不是 20})(foo);
复制代码从技术角度来说创建该函数的上层上下文的数据是保存在函数的内部属性 [[Scope]]中的。 如果你还不了解什么是[[Scope]]建议你先阅读第四章, 该章节对[[Scope]]作了非常详细的介绍。如果你对[[Scope]]和作用域链的知识完全理解了的话那对闭包也就完全理解了。 根据函数创建的算法我们看到 在ECMAScript中所有的函数都是闭包因为它们都是在创建的时候就保存了上层上下文的作用域链除开异常的情况 不管这个函数后续是否会激活 —— [[Scope]]在函数创建的时候就有了 var x 10;function foo() {alert(x);
}// foo is a closure
foo: FunctionObject {[[Call]]: code block of foo,[[Scope]]: [global: {x: 10}],... // other properties
};
复制代码正如此前提到过的出于优化的目的当函数不使用自由变量的时候实现层可能就不会保存上层作用域链。 然而ECMAScript-262-3标准中并未对此作任何说明因此严格来说 —— 所有函数都会在创建的时候将上层作用域链保存在[[Scope]]中。 有些实现中允许对闭包作用域直接进行访问。比如Rhino针对函数的[[Scope]]属性对应有一个非标准的 __parent__属性在第二章中作过介绍 var global this;
var x 10;var foo (function () {var y 20;return function () {alert(y);};})();foo(); // 20
alert(foo.__parent__.y); // 20foo.__parent__.y 30;
foo(); // 30// 还可以操作作用域链
alert(foo.__parent__.__parent__ global); // true
alert(foo.__parent__.__parent__.x); // 10
复制代码 “万能”的[[Scope]] 这里还要注意的是在ECMAScript中同一个上下文中创建的闭包是共用一个[[Scope]]属性的。 也就是说某个闭包对其中的变量做修改会影响到其他闭包对其变量的读取 var firstClosure;
var secondClosure;function foo() {var x 1;firstClosure function () { return x; };secondClosure function () { return --x; };x 2; // 对AO[x]产生了影响, 其值在两个闭包的[[Scope]]中alert(firstClosure()); // 3, 通过 firstClosure.[[Scope]]
}foo();alert(firstClosure()); // 4
alert(secondClosure()); // 3
复制代码正因为这个特性很多人都会犯一个非常常见的错误 当在循环中创建了函数然后将循环的索引值和每个函数绑定的时候通常得到的结果不是预期的预期是希望每个函数都能够获取各自对应的索引值。 var data [];for (var k 0; k 3; k) {data[k] function () {alert(k);};
}data[0](); // 3, 而不是 0
data[1](); // 3, 而不是 1
data[2](); // 3, 而不是 2
复制代码上述例子就证明了 —— 同一个上下文中创建的闭包是共用一个[[Scope]]属性的。因此上层上下文中的变量“k”是可以很容易就被改变的。 如下所示 activeContext.Scope [... // higher variable objects{data: [...], k: 3} // activation object
];data[0].[[Scope]] Scope;
data[1].[[Scope]] Scope;
data[2].[[Scope]] Scope;
复制代码这样一来在函数激活的时候最终使用到的k就已经变成了3了。 如下所示创建一个额外的闭包就可以解决这个问题了 var data [];for (var k 0; k 3; k) {data[k] (function _helper(x) {return function () {alert(x);};})(k); // 将 k 值传递进去
}// 现在就对了
data[0](); // 0
data[1](); // 1
data[2](); // 2
复制代码上述例子中函数“_helper”创建出来之后通过参数“k”激活。其返回值也是个函数该函数保存在对应的数组元素中。 这种技术产生了如下效果 在函数激活时每次“_helper”都会创建一个新的变量对象其中含有参数“x”“x”的值就是传递进来的“k”的值。 这样一来返回的函数的[[Scope]]就成了如下所示 data[0].[[Scope]] [... // 更上层的变量对象上层上下文的AO: {data: [...], k: 3},_helper上下文的AO: {x: 0}
];data[1].[[Scope]] [... // 更上层的变量对象上层上下文的AO: {data: [...], k: 3},_helper上下文的AO: {x: 1}
];data[2].[[Scope]] [... // 更上层的变量对象上层上下文的AO: {data: [...], k: 3},_helper上下文的AO: {x: 2}
];
复制代码我们看到这个时候函数的[[Scope]]属性就有了真正想要的值了为了达到这样的目的我们不得不在[[Scope]]中创建额外的变量对象。 要注意的是在返回的函数中如果要获取“k”的值那么该值还是会是3。 顺便提下大量介绍JavaScript的文章都认为只有额外创建的函数才是闭包这种说法是错误的。 实践得出这种方式是最有效的然而从理论角度来说在ECMAScript中所有的函数都是闭包。 然而上述提到的方法并不是唯一的方法。通过其他方式也可以获得正确的“k”的值如下所示 var data [];for (var k 0; k 3; k) {(data[k] function () {alert(arguments.callee.x);}).x k; // 将“k”存储为函数的一个属性
}// 同样也是可行的
data[0](); // 0
data[1](); // 1
data[2](); // 2
复制代码 Funarg和return 另外一个特性是从闭包中返回。在ECMAScript中闭包中的返回语句会将控制流返回给调用上下文调用者。 而在其他语言中比如Ruby有很多中形式的闭包相应的处理闭包返回也都不同下面几种方式都是可能的可能直接返回给调用者或者在某些情况下——直接从上下文退出。 ECMAScript标准的退出行为如下 function getElement() {[1, 2, 3].forEach(function (element) {if (element % 2 0) {// 返回给函数forEach// 而不会从getElement函数返回alert(found: element); // found: 2return element;}});return null;
}alert(getElement()); // null, 而不是 2
复制代码然而在ECMAScript中通过try catch可以实现如下效果 var $break {};function getElement() {try {[1, 2, 3].forEach(function (element) {if (element % 2 0) {// 直接从getElement返回alert(found: element); // found: 2$break.data element;throw $break;}});} catch (e) {if (e $break) {return $break.data;}}return null;
}alert(getElement()); // 2
复制代码 理论版本 通常程序员会错误的认为只有匿名函数才是闭包。其实并非如此正如我们所看到的 —— 正是因为作用域链使得所有的函数都是闭包与函数类型无关 匿名函数FENFEFD都是闭包 这里只有一类函数除外那就是通过Function构造器创建的函数因为其[[Scope]]只包含全局对象。 为了更好的澄清该问题我们对ECMAScript中的闭包作两个定义即两种闭包 ECMAScript中闭包指的是 从理论角度所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此因为函数中访问全局变量就相当于是在访问自由变量这个时候使用最外层的作用域。从实践角度以下函数才算是闭包 即使创建它的上下文已经销毁它仍然存在比如内部函数从父函数中返回在代码中引用了自由变量 闭包实践 实际使用的时候闭包可以创建出非常优雅的设计允许对funarg上定义的多种计算方式进行定制。 如下就是数组排序的例子它接受一个排序条件函数作为参数 [1, 2, 3].sort(function (a, b) {... // 排序条件
});
复制代码同样的例子还有数组的map方法并非所有的实现都支持数组map方法SpiderMonkey从1.6版本开始有支持该方法根据函数中定义的条件将原数组映射到一个新的数组中 [1, 2, 3].map(function (element) {return element * 2;
}); // [2, 4, 6]
复制代码使用函数式参数可以很方便的实现一个搜索方法并且可以支持无穷多的搜索条件 someCollection.find(function (element) {return element.someProperty searchCondition;
});
复制代码还有应用函数比如常见的forEach方法将funarg应用到每个数组元素 [1, 2, 3].forEach(function (element) {if (element % 2 ! 0) {alert(element);}
}); // 1, 3
复制代码顺便提下函数对象的 apply 和 call方法在函数式编程中也可以用作应用函数。 apply和call已经在讨论“this”的时候介绍过了这里我们将它们看作是应用函数 —— 应用到参数中的函数在apply中是参数列表在call中是独立的参数 (function () {alert([].join.call(arguments, ;)); // 1;2;3
}).apply(this, [1, 2, 3]);
复制代码闭包还有另外一个非常重要的应用 —— 延迟调用 var a 10;
setTimeout(function () {alert(a); // 10, 一秒钟后
}, 1000);
复制代码也可以用于回调函数 ...
var x 10;
// only for example
xmlHttpRequestObject.onreadystatechange function () {// 当数据就绪的时候才会调用;// 这里不论是在哪个上下文中创建变量“x”的值已经存在了alert(x); // 10
};
..
复制代码还可以用于封装作用域来隐藏辅助对象 var foo {};// initialization
(function (object) {var x 10;object.getX function _getX() {return x;};})(foo);alert(foo.getX()); // get closured x – 10
复制代码 总结 本文介绍了更多关于ECMAScript-262-3的理论知识而我认为这些基础的理论有助于理解ECMAScript中闭包的概念。 原文地址 译文地址 重学系列传送门 重学JavaScript深入理解系列一重学JavaScript深入理解系列二重学JavaScript深入理解系列三重学JavaScript深入理解系列四重学JavaScript深入理解系列五 转载于:https://juejin.im/post/5ce78710f265da1bca51b5ef