js的嵌套函数与闭包函数
js的嵌套函数与闭包函数
先看一下代码示例:
function f(){ var cnt=0; return function(){ return ++cnt;} } var fa=f();//将函数f的的返回值给变量fa // fa(); //对fa的函数调用 console.log(fa());//1 console.log(fa());//2 console.log(fa());//3
函数的返回值是函数(对象的引用),这里将其赋值给变量fn。在调用fn时,其输出结果每次都会自增加1
从表面看,闭包(closure)具有状态的函数,或者也可以将闭包的特征理解为:其相关的局部变量在函数调用结束后会继续存在
一、闭包的原理
1.1 嵌套的函数声明:
闭包的前提条件是需要在函数声明的内部声明另一个函数(即嵌套的函数声明)贴一下函数函数声明的simple example:
function f(){ function g(){ console.log('g is called'); } g(); } f()// g is called
在函数f的声明中包含函数g的声明以及调用语句。再调用函数f时,就间接地调用了函数g。为了更好理解该过程,在此对其内部机制进行说明。
在javaScript中,调用函数时将会隐式地生成call对象。为了方便起见,我们将调用函数f生成的call对象称作call-f对象。在函数调用完成之后,call对象将被销毁。
函数f内的函数g的声明将会生成一个与函数的g相对应function对象。其名称g是call-f对象的属性。由 于每一次调用函数都会独立生成call对象,因此在调用函数g时将会隐式地生成另一个call对象。为了方便起见,我们将该call对象称作call-g对象。
离开函数g之后,call-g对象将被自动销毁。类似的,离开函数f之后,call-f对象也就自动销毁。此时,由于属性g将与call-g对象一起被销毁,所以由g所引用的function对象将会失去其引用,而最终(通过垃圾回收机制)被销毁。
1.2嵌套函数与作用域
对上面代码稍稍修改:
function f(){ var n=123; function g(){ console.log("n is"+n); console.log('g is called'); } g(); } f(); 运行结果: js>f(); n is 123 g is called'
在内层进行声明函数g可以访问外层的函数f的局部变量(在这里指变量n),对于嵌套声明的函数,内部的函数将会首先查找被调用时所生成的call对象的属性,之后之后在查找外层函数的call对象的属性。这一机制被称为作用链。
1.3嵌套函数的返回
上面的代码稍稍修改
function f(){ var n=123; function g(){ console.log("n is"+n); console.log('g is called'); } return g; } js> f(); function g(){ console.log("n is"+n); console.log('g is called'); }
由于return语句,函数将会返回一个function对象(的引用)。调用函数f的结果是一个function对象。这时,虽然会生成与函数f相对应的call对象(call-f对象)(并在离开函数f后被销毁),但由于不会调用函数g,所以此时还不会生成与之相对应的call对象(call-g对象),请对此多加注意。
二、闭包
2.1、作用域
待更新
2.2、闭包示例:
function init() { var name = "Mozilla"; // name 是一个被 init 创建的局部变量 function displayName() { // displayName() 是内部函数,一个闭包 console.log(name); // 使用了父函数中声明的变量 } displayName(); } init();
运行这段代码的效果和之前 init() 函数的示例完全一样。其中不同的地方(也是有意思的地方)在于内部函数 displayName() 在执行前,从外部函数返回。。在本例子中,myFunc 是执行 makeFunc 时创建的 displayName 函数实例的引用。displayName 的实例维持了一个对它的词法环境(变量 name 存在于其中)的引用。因此,当 myFunc 被调用时,变量 name 仍然可用,其值 Mozilla 就被传递到alert中
function makeFunc() { var name = "Mozilla"; function displayName() { console.log(22, name); } return displayName; } var myFunc = makeFunc(); myFunc(); //22 "Mozilla"
下面是一个更有意思的示例 — 一个 makeAdder 函数
function makeAdder(x) { return function(y) { return x + y; }; } var add5 = makeAdder(5); var add10 = makeAdder(10); console.log(add5(2)); // 7 console.log(add10(2)); // 12
在这个示例中,我们定义了 makeAdder(x) 函数,它接受一个参数 x ,并返回一个新的函数。返回的函数接受一个参数 y,并返回x+y的值。
从本质上讲,makeAdder 是一个函数工厂 — 他创建了将指定的值和它的参数相加求和的函数。在上面的示例中,我们使用函数工厂创建了两个新函数 — 一个将其参数和 5 求和,另一个和 10 求和。
add5 和 add10 都是闭包。它们共享相同的函数定义,但是保存了不同的词法环境。在 add5 的环境中,x 为 5。而在 add10 中,x 则为 10。
三、闭包的实际应用
1、return 一个函数
2、传以一个函数的形似传参数
3、IIF立即执行函数
css:
<style> body { font-family: Helvetica, Arial, sans-serif; font-size: 12px; } h1 { font-size: 1.5em; } h2 { font-size: 1.2em; } </style>
html:
<hr> <a href="#" id="size-12">12</a> <a href="#" id="size-14">14</a> <a href="#" id="size-16">16</a>
js:
window.onload = function(){ function makeSizer(size) { return function() { document.body.style.fontSize = size + 'px'; }; } var size12 = makeSizer(12); var size14 = makeSizer(14); var size16 = makeSizer(16); document.getElementById('size-12').onclick = size12; document.getElementById('size-14').onclick = size14; document.getElementById('size-16').onclick = size16; }
用闭包模拟私有方法:
//***** var Counter = (function() { var privateCounter = 0; function changeBy(val) { privateCounter += val; } return { increment: function() { changeBy(1); }, decrement: function() { changeBy(-1); }, value: function() { return privateCounter; } } })(); console.log(Counter.value()); /* logs 0 */ Counter.increment(); Counter.increment(); console.log(Counter.value()); /* logs 2 */ Counter.decrement(); console.log(Counter.value()); /* logs 1 */
在之前的示例中,每个闭包都有它自己的词法环境;而这次我们只创建了一个词法环境,为三个函数所共享:Counter.increment,Counter.decrement 和 Counter.value。
该共享环境创建于一个立即执行的匿名函数体内。这个环境中包含两个私有项:名为 privateCounter 的变量和名为 changeBy 的函数。这两项都无法在这个匿名函数外部直接访问。必须通过匿名函数返回的三个公共函数访问。
这三个公共函数是共享同一个环境的闭包。多亏 JavaScript 的词法作用域,它们都可以访问 privateCounter 变量和 changeBy 函数。
四、在循环中创建闭包常见误区
htm代码片段:
<p id="help">Helpful notes will appear here</p> <p>E-mail: <input type="text" id="email" name="email"></p> <p>Name: <input type="text" id="name" name="name"></p> <p>Age: <input type="text" id="age" name="age"></p>
js代码片段:
function showHelp(help) { document.getElementById('help').innerHTML = help; } function setupHelp() { var helpText = [ {'id': 'email', 'help': 'Your e-mail address'}, {'id': 'name', 'help': 'Your full name'}, {'id': 'age', 'help': 'Your age (you must be over 16)'} ]; for (var i = 0; i < helpText.length; i++) { var item = helpText[i]; document.getElementById(item.id).onfocus = function() { showHelp(item.help); } } } setupHelp();
运行这段代码后,您会发现它没有达到想要的效果。无论焦点在哪个input上,显示的都是关于年龄的信息。
原因是赋值给 onfocus 的是闭包。这些闭包是由他们的函数定义和在 setupHelp 作用域中捕获的环境所组成的。这三个闭包在循环中被创建,但他们共享了同一个词法作用域,在这个作用域中存在一个变量item。这是因为变量item使用var进行声明,由于变量提升,所以具有函数作用域。当onfocus的回调执行时,item.help的值被决定。由于循环在事件触发之前早已执行完毕,变量对象item(被三个闭包所共享)已经指向了helpText的最后一项。
改进方法一:
function showHelp(help) { document.getElementById('help').innerHTML = help; } function makeHelpCallback(help) { return function() { showHelp(help); }; } function setupHelp() { var helpText = [ {'id': 'email', 'help': 'Your e-mail address'}, {'id': 'name', 'help': 'Your full name'}, {'id': 'age', 'help': 'Your age (you must be over 16)'} ]; for (var i = 0; i < helpText.length; i++) { var item = helpText[i]; document.getElementById(item.id).onfocus = makeHelpCallback(item.help); } } setupHelp();
改进方法二:
function showHelp(help) { document.getElementById('help').innerHTML = help; } function setupHelp() { var helpText = [ {'id': 'email', 'help': 'Your e-mail address'}, {'id': 'name', 'help': 'Your full name'}, {'id': 'age', 'help': 'Your age (you must be over 16)'} ]; for (var i = 0; i < helpText.length; i++) { (function() { var item = helpText[i]; document.getElementById(item.id).onfocus = function() { showHelp(item.help); } })(); // 马上把当前循环项的item与事件回调相关联起来 } } setupHelp();
避免使用过多的闭包,可以用let关键词:
function showHelp(help) {
document.getElementById('help').innerHTML = help; } function setupHelp() { var helpText = [ {'id': 'email', 'help': 'Your e-mail address'}, {'id': 'name', 'help': 'Your full name'}, {'id': 'age', 'help': 'Your age (you must be over 16)'} ]; for (var i = 0; i < helpText.length; i++) { let item = helpText[i]; document.getElementById(item.id).onfocus = function() { showHelp(item.help); } } } setupHelp();
例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用时,方法都会被重新赋值一次(也就是,每个对象的创建)。
考虑以下示例:
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); this.getName = function() { return this.name; }; this.getMessage = function() { return this.message; }; }
在上面的代码中,我们并没有利用到闭包的好处,因此可以避免使用闭包。修改成如下:
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); } MyObject.prototype = { getName: function() { return this.name; }, getMessage: function() { return this.message; } };
但我们不建议重新定义原型。可改成如下例子:
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); } MyObject.prototype.getName = function() { return this.name; }; MyObject.prototype.getMessage = function() { return this.message; };
在前面的两个示例中,继承的原型可以为所有对象共享,不必在每一次创建对象时定义方法。