作用域
概念
变量在程序中定义的可见范围。
详细解释
JavaScript采用的是词法作用域,即变量的作用域在定义变量时根据定义的位置决定。
JavaScript中有三种作用域:全局作用域、函数作用域和块级作用域。
全局作用域中定义的变量在所有地方都可见;函数中定义的变量只在函数内部可见;块,也就是大括号定义的变量只在块内可见。其中块级作用域是ES6新增的,搭配使用的是let和const声明方式,主要为了解决var声明对更大范围作用域的污染的问题。
作用域之间可以存在嵌套关系,如全局作用域内部包含函数作用域,内部又包含块级作用域。编译器在查找变量定义的时候会从内到外沿着作用域链查找,直到找到最近的定义,如果达到全局作用域仍然找不到,则会抛出ReferenceError。
总结
作用域是变量能够被访问到的范围,JS中包含三种作用域以及三种声明变量的方式,let 和const的作用域能够达到块级,var只有全局和函数级。
闭包
概念
内部函数持有对定义时的作用域内的变量的引用。即一个函数能够访问到它定义时所在的作用域的变量。
详细解释
将内部函数传递到所定义的词法作用域之外的地方去调用,此时该函数持有对定义时的词法作用域的引用,形成了闭包。当函数执行完毕后,该函数内部定义的变量和函数仍然存在于内存中,不会被自动回收,因此可以被其他函数继续访问和使用。这个机制称为闭包。
闭包的应用非常广泛,特别是在异步编程和模块化开发中。以下是一些常见的使用场景:
- 保存变量状态和私有化变量和函数。
- 用于事件处理和回调函数。
- 用于封装模块。
- 用于解决循环中异步问题。
- 用于实现缓存和记忆化等功能。
举例
循环错觉
来自《你不知道的JavaScript·上》
以下代码会输出5个6,而不是1,2,3,4,5
得到这个结果的原因是var声明的变量在全局作用域中,而timer在执行的时候取到的值都是全局作用域的这一份,并不取得到每轮循环时的副本值。
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
如何修正,让代码输出1-5呢,有若干种办法,核心都是创造非全局作用域。
方法一:创建函数作用域,并使用闭包保存副本。函数作用域使用立即执行函数实现
for (var i=1; i<=5; i++) {
(function() {
var j = i;
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})();
}
上面的写法在每轮循环内部创建了timer的闭包,把j作为词法作用域中的变量放了进来,使得i在那一轮的值能够被保存下来。
下面的写法也是能达到同样效果的,这里有个隐式的LHS,把i当做参数传进来。
for (var i=1; i<=5; i++) {
(function(j) {
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})( i );
}
方法二:使用let创建块作用域,并使用闭包保存副本
let声明在每轮循环都会执行一次(这个有点反直觉),且这个变量是块级作用域的,每一轮循环会被作为闭包保存下副本。
for (let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
模块管理
来自《你不知道的JavaScript·上》
构成闭包的地方是define和get方法对modules的引用。
var MyModules = (function Manager() {
var modules = {};
function define(name, deps, impl) {
for (var i = 0; i < deps.length; i++) {
const depName = deps[i];
// 把名称替换为实例。此处引用了modules,构成了闭包
deps[i] = modules[depName];
}
// 把deps传入impl,则为包装好了依赖的模块实例
modules[name] = impl.apply(impl, deps);
}
function get(name) {
// 此处构成了闭包
return modules[name];
}
return {
define: define,
get: get
};
})();
// 定义没有依赖的模块
MyModules.define("bar", [], function () {
function hello(who) {
return "Let me introduce: " + who;
}
return {
hello: hello
};
});
MyModules.define("bar2", [], function () {
function hello(who) {
return "Let me introduce: " + who + " again";
}
return {
hello: hello,
}
});
// 定义一个有两个依赖的模块。注意复习一下apply的第二个参数
MyModules.define("foo", ["bar", "bar2"], function (bar, bar2) {
function awesome() {
console.log(bar.hello("hippo").toUpperCase());
console.log(bar2.hello("hippo").toUpperCase());
}
return {
awesome: awesome
};
});
var bar = MyModules.get("bar");
var foo = MyModules.get("foo");
// Let me introduce: hippo
console.log(bar.hello("hippo"));
// LET ME INTRODUCE: HIPPO
// LET ME INTRODUCE: HIPPO AGAIN
foo.awesome();
扩展--内存泄漏问题
为什么会出现内存泄漏:
内部函数对变量的引用一直存在,如果该变量很大,则占用的内存空间一直不释放,造成内存泄漏。
如何避免:
- 在不需要用闭包之后及时将引用闭包的变量置为null,使得垃圾回收机制可以将它回收
- 在闭包内使用弱引用的数据结构,如WeakSet, WeakMap,同样可以触发垃圾回收机制
- 减少对DOM的引用,因为DOM占用的内存空间较大,一旦无法回收很可能造成内存泄漏
总结
当一个函数可以记住并访问所在的词法作用域中的变量时,即使该函数是在其他词法作用域被调用的。这时就形成了闭包。闭包具有封装变量的功能,因此被广泛应用于模块化等场景。使用闭包时需要注意内存泄漏的问题,在不需要时及时清理闭包。

748

被折叠的 条评论
为什么被折叠?



