本文深入解析JavaScript闭包的定义与词法作用域原理,通过函数工厂、私有变量模拟及事件处理等实例展示其核心应用场景。同时探讨循环中创建闭包的常见陷阱及解决方案,并分析闭包对内存与性能的影响,提供优化建议与注意事项。
闭包是由函数及其绑定的词法环境(lexical environment)引用共同构成的组合体。简单来说,闭包机制允许内部函数访问外部函数的作用域。在 JavaScript 中,每当函数被创建时,闭包也随之生成。
简而言之,闭包是一种能够读取其他函数内部变量的函数。
词法作用域
示例1:
function init() {
var name = "localName"; // name 是一个被 init 创建的局部变量
function closureName() { // closureName() 是内部函数,一个闭包
alert(name); // 使用了父函数中声明的变量
}
closureName();
}
init();上述词法作用域示例展示了分析器如何在函数嵌套结构中解析变量名。所谓“词法”,是指变量的可用范围由其在源代码中声明的位置决定。嵌套函数可以访问其外部作用域中声明的变量。
示例2:
function fn() {
var name = "localName";
function closureName() {
alert(name);
}
return closureName;
}
var myFn = fn()
myFn();示例2的运行结果与示例1相同。区别在于,内部函数 closureName() 在外部函数执行完毕后被返回。
JavaScript 中的函数会形成闭包。闭包由函数本身及其声明该函数的词法环境组成,该环境包含了闭包创建时作用域内的所有局部变量。在示例2中,myFn 是执行 fn 时创建的 closureName 函数实例的引用。closureName 的实例维持了对词法环境的引用,该环境中存在变量 name。
因此,当调用 myFn 时,变量 name 依然可用。
示例3:
function fn(x) {
return function(y) {
return x + y;
}
}
var addOne = fn(1)
var addTwo = fn(2)
console.log(addOne(3)) // 4
console.log(addTwo(3)) // 5在示例3中,fn 可视为一个函数工厂,它创建将指定值与参数相加的函数。利用该工厂创建了两个新函数:一个将参数与 1 相加,另一个将参数与 2 相加。
addOne 和 addTwo 均为闭包,它们共享相同的函数定义,但保存了不同的词法环境。在 addOne 的词法环境中,x 的值为 1。在 addTwo 中,x 的值为 2。
使用闭包场景
闭包允许将函数与其操作的数据(环境)关联起来,这类似于面向对象编程。例如,对象允许将数据(如属性)与一个或多个方法关联。
- 在任何只需使用单个方法的对象场景中,都可以使用闭包替代。
- 在 Web 开发中,大部分 JavaScript 代码基于事件驱动,即定义某种行为并将其绑定到用户触发的事件上(如按钮点击)。这类代码通常称为回调,即为响应事件而执行的函数。
例如,若要在页面上添加调整字体大小的按钮,size1 和 size2 即为闭包的应用。
示例4:
<button id="btn1">size12</button>
<button id="btn2">size24</button>
<p>hello</p>
<script>
function setFontSize(num) {
return function() {
return num + 'px';
}
}
var size1 = setFontSize(12)
var size2 = setFontSize(24)
document.getElementById("btn1").onclick = () => {
let pdom = document.getElementsByTagName("p")
pdom[0].style.fontSize = size1()
}
document.getElementById("btn2").onclick = () => {
let pdom = document.getElementsByTagName("p")
pdom[0].style.fontSize = size2()
}
</script>用闭包模拟私有方法
利用闭包模拟私有方法,不仅有助于限制代码访问权限,还能有效管理全局命名空间,防止非核心方法污染代码的公共接口。
示例5:使用闭包定义公共函数,并令其可以访问私有函数和变量
var Counter = (function () {
var privateCounter = 0
function operateCounter(val) {
privateCounter += val
}
return {
increment: function () {
operateCounter(1);
},
decrement: function () {
operateCounter(-1);
},
value: function () {
return privateCounter;
}
}
})()
console.log(Counter.value())
Counter.increment()
Counter.increment()
console.log(Counter.value())
Counter.decrement()
console.log(Counter.value())在前面的示例中,每个闭包拥有独立的词法环境。而在示例5中,仅创建了一个词法环境,供三个函数共享:Counter.increment、Counter.decrement 和 Counter.value。
示例5的共享环境创建在一个立即执行的匿名函数体内。该环境包含两个私有项:名为 privateCounter 的变量和名为 operateCounter 的函数。这两项无法在匿名函数外部直接访问,必须通过匿名函数返回的三个公共函数进行访问。
这三个公共函数共享同一个环境的闭包。得益于 JavaScript 的词法作用域,它们均可访问 privateCounter 变量和 operateCounter 函数。
注意:在示例5中,我们定义了一个立即执行的匿名函数并赋值给 Counter。若不立即执行,而是将匿名函数赋值给变量 makeCounter,则可以创建多个计数器实例。
var makeCounter = (function () {
var privateCounter = 0
function operateCounter(val) {
privateCounter += val
}
return {
increment: function () {
operateCounter(1);
},
decrement: function () {
operateCounter(-1);
},
value: function () {
return privateCounter;
}
}
})
var Counter1 = makeCounter();
var Counter2 = makeCounter();Counter1 和 Counter2 闭包均引用其各自词法作用域内的变量。
在一个闭包内对变量的修改,不会影响另一个闭包中的变量。
闭包的用途
总结闭包的主要用途:
可以读取函数内部的变量;
使变量的值始终保存在内存中。
在循环中创建闭包:一个场景的错误
在引入 let 关键字之前,在循环中创建闭包存在一个常见问题。
for(var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i)
}, 0)
} // 5 5 5 5 5我们期望输出 0 1 2 3 4,但实际打印了 5 个 5。这是为什么?
for 循环在宏任务阶段执行,setTimeout 在微任务阶段执行。此时变量 i 作为全局变量,i 的值已变为 5,因此最后打印出 5 个 5。
如何解决呢?
1.使用闭包
将 setTimeout 的内容置于闭包中,循环执行时每个闭包都拥有对应的作用域 i。
for(var i = 0; i < 5; i++) {
(function(i){
setTimeout(() => {
console.log(i)
}, 0)
})(i)
} // 0 1 2 3 42.使用 let 声明变量 i
for(let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i)
}, 0)
} // 0 1 2 3 43.使用 forEach 来遍历数组
[0, 1, 2, 3, 4].forEach((i) => {
setTimeout(() => {
console.log(i)
}, 0)
}) // 0 1 2 3 4性能考量
除非特定任务必需,否则在其他函数中创建函数是不明智的,因为闭包在运行速度和内存消耗方面对脚本性能有负面影响。
例如:在创建新对象或类时,方法通常应关联到对象的原型上,而非定义在对象的构造器中。因为每次调用构造器时,方法都会被重新赋值一次(即每创建一个对象,方法都会被重新赋值)。
比如:
function CreateObject(name, message) {
this.name = name.toString();
this.message = message.toString();
this.getName = function () {
return this.name;
};
this.getMessage = function () {
return this.message;
};
}上述代码并未利用闭包的优势,因此应避免使用闭包。修改后如下:
function CreateObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
CreateObject.prototype = {
getName() {
return this.name;
},
getMessage() {
return this.message;
},
};然而,我们不建议重新定义原型,而是在现有原型基础上添加方法。修改后如下:
function CreateObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
CreateObject.prototype.getName = function () {
return this.name;
};
CreateObject.prototype.getMessage = function () {
return this.message;
};上述示例中,继承的原型可被所有对象共享,无需在每次创建对象时定义方法。
使用闭包注意点
由于闭包会使函数中的变量保存在内存中,导致内存消耗较大,因此不能滥用闭包,否则可能引发网页性能问题,在 IE 浏览器中甚至可能导致内存泄露。解决方法是在退出函数前,删除不使用的局部变量。
闭包会在父函数外部改变父函数内部变量的值。因此,若将父函数视为对象(object),将闭包视为其公用方法(Public Method),将内部变量视为私有属性(private value),此时务必小心,避免随意更改父函数内部变量的值。
到此这篇关于一文带你深入了解JavaScript中的闭包的文章就介绍到这了,更多相关JavaScript闭包内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!