什么是闭包
补充:闭包形成的本质是内层作用域中堆地址暴露,原因是局部作用域中访问了其他局部变量?
最原始定义
闭包(closure),是指函数变量可以保存在函数作用域内,因此看起来是函数将变量“包裹”了起来。
//根据定义,包含变量的函数就是闭包
function foo() {
var a = 0;
}
cosole.log(a)
// Uncaught ReferenceError: a is not defined
《JavaScript高级程序设计》对闭包定义
闭包是指有权访问另一个函数作用域中的变量的函数
//访问上层函数的作用域的内层函数就是闭包
function foo() {
var a = 2;
function bar() {
console.log(a);
}
bar();
}
foo();
《JavaScript权威指南》对闭包定义
函数对象可以通过作用域链相互关联起来,函数体内部变量可以保存在函数作用域内,这就是闭包。
var global = "global scope"; //全局变量
function checkscope() {
var scope = "local scope"; //局部变量
function f() {
return scope; //在作用域中返回这个值
};
return f();
}
checkscope(); // 返回 "local scope"
《你不知道的JavaScript》这样描述
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
//fn3就是fn2函数本身。执行fn3能正常输出name
//这不就是fn2能记住并访问它所在的词法作用域,而且fn2函数的运行还是在当前词法作用域之外了。
function fn1() {
var name = 'iceman';
function fn2() {
console.log(name);
}
return fn2;
}
var fn3 = fn1();
fn3();
MDN 上面这么说:
闭包是一种特殊的对象。它由两部分构成:函数,以及创建该函数的环境。环境由闭包创建时在作用域中的任何局部变量组成。
简单说就是指那些能够访问自由变量的函数。
严格来说,闭包需要满足三个条件:
【1】访问所在作用域;
【2】函数嵌套;
【3】在所在作用域外被调用
闭包的形成原理
先了解JavaScript的垃圾回收机制
Javascript 会找出不再使用的变量,不再使用意味着这个变量生命周期的结束。
Javascript 中存在两种变量——全局变量和局部变量,全部变量的声明周期会一直持续,直到页面卸载而局部变量声明在函数中,它的声明周期从执行函数开始,直到函数执行结束。在这个过程中,局部变量会在堆或栈上被分配相应的空间以存储它们的值,函数执行结束,这些局部变量也不再被使用,它们所占用的空间也就被释放。
但是有一种情况的局部变量不会随着函数的结束而被回收,那就是局部变量被函数外部的变量所使用,其中一种情况就是闭包,因为在函数执行结束后,函数外部的变量依然指向函数内的局部变量,此时的局部变量依然在被使用,所以也就不能够被回收
var scope = 'global scope';
function checkScope() {
var scope = 'local scope';
return function() {
console.log(scope);
}
}
var result = checkScope();
result(); // local scope checkScope变量对象中的scope,非全局变量scope
此匿名函数的作用域链包括checkScope的活动对象和全局变量对象, 当checkScope函数执行完毕后,checkScope的活动对象并不会被销毁,因为匿名函数的作用域链还在引用checkScope的活动对象,也就是checkScope的执行环境被销毁,但是其活动对象没有被销毁,留存在堆内存中,直到匿名函数销毁后,checkScope的活动对象才会销毁
从作用域链理解闭包的形成
- 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
- 从实践角度:以下函数才算是闭包:
i. 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
ii. 在代码中引用了自由变量
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
var foo = checkscope();
foo();
fContext = {//f函数的执行上下文
Scope: [AO, checkscopeContext.AO, globalContext.VO],
}
对的,就是因为这个作用域链,f函数在声明的时候压入了上层的变量对象,f 函数依然可以读取到 checkscopeContext.AO 的值,并且如果当 f 函数引用了 checkscopeContext.AO 中的值的时候,即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中(和垃圾回收机制有关下文会说),f 函数依然可以通过 f 函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。
闭包的作用-模仿块级作用域,封装私有变量
任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数外部访问这些变量。
私有变量包括函数的参数、局部变量和函数内定义的其他函数。
function module() {
var arr = [];
function add(val) {
if (typeof val == 'number') {
arr.push(val);
}
}
function get(index) {
if (index < arr.length) {
return arr[index]
} else {
return null;
}
}
return {
add: add,
get: get
}
}
var mod1 = module();
mod1.add(1);
mod1.add(2);
mod1.add('xxx');
console.log(mod1.get(2));//外部是无法直接拿到arr的只能通过get来拿
闭包的作用-使变量保存在内存中不被销毁
实例1-计数器
我们来实现一个计数器,每调用一次计数器返回值加一:
var counter = 0;
function add() {
return counter += 1;
}
add();
add();
add();// 计数器现在为 3
问题:
- 全局变量容易被其他代码改变
- 如果我需要同时用两个计数器,但这种写法只能满足一个使用,另一个还想用的话就要再写个counter2函数,再定义一个counter2的全局变量。
那我们把counter放在add函数里面不就好了么?
function add() {
var counter = 0;
return counter += 1;
}
add();
add();
add();// 本意是想输出 3, 但输出的都是 1
所以这样做的话,每次调用add函数,counter的值都要被初始化为0,还是达不到我们的目的。
使用闭包来写就会解决这些问题
function add() {
var index = 1;
function counter() {
return index ++;
}
return counter;
}
// test
var addA = add() ;
var addB = add() ;
addA(); // 1
addA(); // 2
addB(); // 1
addB(); // 2
实例2-延时打印
这样打印出来的全部都是10,原因是for循环是同步的会在延时1000毫秒的过程中一直执行
等function执行的时候变量i指向的是同一个内存地址,且值已经变成的10
for (var i = 1; i <= 10; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
改进,用自执行的函数创建简单的闭包,让每一次for循环的i都在不同的内存地址中且不被销毁
for (var i = 1; i <= 10; i++) {
(function () {
var j = i;
setTimeout(function () {
console.log(j);
}, 1000);
})();
}
错误写法:
for (var i = 1; i <= 10; i++) {
(function () {
setTimeout(function () {
// 这样虽然也是闭包,但没有缓存i的值到作用域
// 导致1000ms到时,然后还是去外层作用域链找i的值,已经变成了11
console.log(i);
}, 1000);
})();
}
优化写法:
for (var i = 1; i <= 10; i++) {
(function (j) {
setTimeout(function () {
console.log(j);
}, 1000);
})(i);
}
ES6写法:
for (let i = 1; i <= 10; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
for循环里面用let,()会生成一个隐藏作用域,产生多个不同的块级作用域,类似于闭包,如下:
// 伪代码
(let i = 0) {
setTimeout(function () {
console.log(i);
}, 1000);
}
(let i = 1) {
setTimeout(function () {
console.log(i);
}, 1000);
}
(let i = 2) {
setTimeout(function () {
console.log(i);
}, 1000);
};
......
......
联系Static静态变量
闭包的作用主要就是让变量的值始终保持在内存中。
C++或C语言还有Java中都有static静态变量也是让变量始终保存在内存中。
这样来看好像闭包好像有点static静态变量的意思。
总结
闭包就是子函数可以有权访问父函数的变量、父函数的父函数的变量、一直到全局变量。
归根结底,就是利用js得词法(静态)作用域,即作用域链在函数创建的时候就确定了。
子函数如果不被销毁,整条作用域链上的变量仍然保存在内存中,这样就形成了闭包
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。