闭包的理解
闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。
闭包是指引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现,比如:
function createComparisonFunction(propertyName) {
return function(object1, object2) {
let value1 = object1[propertyName]; // 引用了外部 函数的变量
let value2 = object2[propertyName]; // 引用了外部 函数的变量
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
};
}
在 调用一个函数时,会为这个函数调用创建一个执行上下文,并创建一个作用域链。然后用 arguments 和其他命名参数来初始化这个函数的活动对象。外部函数的活动对象是内部函数作用域链上的第二个对 象。这个作用域链一直向外串起了所有包含函数的活动对象,直到全局执行上下文才终止。 在函数执行时,要从作用域链中查找变量,以便读、写值。
闭包函数的作用域链包含了外部函数的作用域链。
// 创建比较函数
let compareNames = createComparisonFunction('name');
// 调用函数
let result = compareNames({ name: 'Nicholas' }, { name: 'Matt' });
// 清除闭包:解除对函数的引用,这样就可以释放内存了 ,
compareNames = null;
创建的比较函数被保存在变量 compareNames 中。把 compareNames 设置为等于 null 会 解除对函数的引用,从而让垃圾回收程序可以将内存释放掉。作用域链也会被销毁,其他作用域(除全 局作用域之外)也可以销毁。
闭包的特点
1.让外部访问函数内部变量变成可能
2.变量会常驻在内存中
3.可以避免使用全局变量,防止全局变量污染;
闭包的好处和坏处
好处:可以读取其他函数内部的变量,并将其一直保存在内存中。
坏处:可能会造成内存泄漏或溢出。
闭包的作用
闭包有两个常用的用途
闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
闭包的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。
私有变量和延迟变量的存在时间
闭包的作用一:可以创建私有变量
严格来讲,JavaScript 没有私有成员的概念,所有对象属性都公有的。不过,倒是有私有变量的概 念。任何定义在函数或块中的变量,都可以认为是私有的,因为在这个函数或块的外部无法访问其中的 变量。私有变量包括函数参数、局部变量,以及函数内部定义的其他函数。来看下面的例子:
function add(num1, num2) {
let sum = num1 + num2;
return sum;
}
在这个函数中,函数 add()有 3 个私有变量:num1、num2 和 sum。这几个变量只能在函数内部使 用,不能在函数外部访问。如果这个函数中创建了一个闭包,则这个闭包能通过其作用域链访问其外部 的这 3 个变量。基于这一点,就可以创建出能够访问私有变量的公有方法
特权方法(privileged method)是能够访问函数私有变量(及私有函数)的公有方法。在对象上有 两种方式创建特权方法。第一种是在构造函数中实现,比如:
function MyObject() {
// 私有变量和私有函数
let privateVariable = 10;
function privateFunction() {
return false;
}
// 特权方法
this.publicMethod = function() {
privateVariable++;
return privateFunction();
};
}
const sum = () => {
let count = 666;
return () => {
return count++;
}
}
let getCount= sum();
console.log(getCount()); // 667
console.log(getCount()); // 668
console.log(getCount()); // 669
这个模式是把所有私有变量和私有函数都定义在构造函数中。然后,再创建一个能够访问这些私有 10.16 私有变量成员的特权方法。这样做之所以可行,是因为定义在构造函数中的特权方法其实是一个闭包,它具有访 问构造函数中定义的所有变量和函数的能力
闭包的作用二:使已经运行结束的函数上下文中的变量对象继续留在内存中
因为闭包函数 保留了这个变量对象的引用,所以这个变量对象不会被回收。其实闭包的本质就是作用域链的一 个特殊的应用
使用闭包实现每隔一秒打印 1,2,3,4
// 使用闭包实现
for (var i = 0; i < 5; i++) {
(function(i) {
setTimeout(function() {
console.log(i);
}, i * 1000);
})(i);
}
// 使用 let 块级作用域
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}
闭包与内存泄漏
闭包导致的内存泄漏的例子
由于 IE 在 IE9 之前对 JScript 对象和 COM 对象使用了不同的垃圾回收机制,所以 闭包在这些旧版本 IE 中可能会导致问题。在这些版本的 IE 中,把 HTML 元素保存在某个闭包的作用域 中,就相当于宣布该元素不能被销毁。来看下面的例子:
function assignHandler() {
let element = document.getElementById('someElement');
element.onclick = () => console.log(element.id);
}
以上代码创建了一个闭包,即 element 元素的事件处理程序。 而这个处理程序又创建了一个循环引用。
匿名函数引用着 assignHandler()的活动对象(就是只函数内部创建的对象、this、arguments),
阻止了对 element 的引用计数归零。只要这个匿名函数存在,element 的引用计数就至少等于 1。
也就是说, 内存不会被回收。其实只要这个例子稍加修改,就可以避免这种情况,比如:
function assignHandler() {
let element = document.getElementById('someElement');
let id = element.id;
element.onclick = () => console.log(id);
element = null;
}
在这个修改后的版本中,闭包改为引用一个保存着 element.id 的变量 id,从而消除了循环引用。
不过,光有这一步还不足以解决内存问题。因为闭包还是会引用包含函数的活动对象,而其中包含 element。
即使闭包没有直接引用 element,包含函数的活动对象上还是保存着对它的引用。
因此,必 须再把 element 设置为 null。这样就解除了对这个 COM 对象的引用,其引用计数也会减少,从而确 保其内存可以在适当的时候被回收
闭包为什么会导致内存占用过多?
一般来讲,当函数执行完毕后,局部活动对象就会销毁,内存仅保存全局作用域。但是,闭包的情况不同,closure
函数执行完毕后,其活动对象不会销毁,因为匿名函数的作用域链仍然引用这个活动对象。直到匿名函数被销毁后,closure
函数的活动对象才会被销毁。
由于闭包会携带包含它的函数的作用域,因此会占用更多的内存,过度的使用闭包会导致内存占用过多,因此,在绝对必要时,再考虑使用闭包。
内存占用和内存泄漏
很多人都会把内存使用和内存泄露搞混。所谓内存泄露是老浏览器(主要是IE6)由于垃圾回收有问题导致的 bug
,跟 JS
本身没有关系。
闭包不会造成内存泄漏,程序写错了才会造成内存泄漏。
跟闭包和内存泄露有关系的地方是,使用闭包的同时比较容易形成循环引用,如果闭包的作用域链中保存着一些 DOM
节点,这时候就有可能造成内存泄露。但这本身并非闭包的问题,也并非 JavaScript
的问题。
在 IE
浏览器中,由于 BOM
和 DOM
中的对象是使用 C++
以 COM
对象的方式实现的,而 COM
对象的垃圾收集机制采用的是引用计数策略。在基于引用计数策略的垃圾回收机制中,如果两个对象之间形成了循环引用,那么这两个对象都无法被回收,但循环引用造成的内存泄露在本质上也不是闭包造成的。
闭包本身不会造成内存泄漏,但闭包过多很容易导致内存泄漏。