文章目录
如何产生闭包
当一个嵌套的内部函数引用了嵌套的外部函数的变量(函数)是便产生了闭包
闭包说的通俗一点就是打通了一条在函数外部访问函数内部作用域的通道。
正常情况下函数外部是访问不到函数内部作用域变量的
产生闭包的必要条件
【方法一:】
1. 函数嵌套
2. 内部函数引用了嵌套外部函数的变量(函数)
3. 调用外部函数,执行内部函数定义
【方法二:】
1. 函数发生嵌套
2. 内存函数引用了嵌套外部函数的变量(函数)
3. 内部函数被return(返回)到外部
注:闭包存在于嵌套的内部函数中
执行下面代码会产生闭包
function fn(){
var a = 1;
function fn1(){ //执行函数定义
console.log(a);
}
}
fn(); //调用外部函数
执行下面的代码不会产生闭包
function fn(){
var a = 1;
function fn1(){ //执行函数定义
//没有引用外部函数的变量(函数)
}
}
fn(); //调用外部函数
举个栗子:
let fn = function () {
let num = 1;
return {
a: function () {
console.log(num);
},
b: function () {
num++;
}
};
};
let closure = fn();
//到这里outer函数已执行完毕,执行上下文被释放
closure.a(); // 1
在上方的例子中,外层函数fn执行返回了两个闭包 a,b。我们知道函数每次被调用执行都会创建一个新的执行上下文,当函数执行完毕函数执行上下文被弹出执行栈并销毁,所以在 let closure = fn() 执行完毕时函数fn的执行上下文已不复存在,但我们执行closure.a()可以看到依旧能访问到外层函数的局部变量num(说明fn指向的函数执行上下文还没有被销毁)。
为了让这种感觉更为强烈,我们直接销毁掉函数fn再次调用闭包函数,可以看到闭包不仅是访问甚至还能操作外层函数中的变量。
fn = null;
closure.b();
closure.a(); // 2
是不是很神奇?那为什么外层函数上下文都销毁了,闭包还能访问到自由变量呢,这就得说说闭包作用域链的特别之处了。
从执行上下文的角度分析闭包
JavaScript中的作用域是指变量与函数的作用范围(静态作用域)。当在某作用域使用某变量时,首先会在本作用域的标识符中查找有没有,如果没有就会去父级找,还没有就一直找到源头window为止(window也没有就报错),这个查找的过程便形成了我们所说的作用域链。
let scope = "global scope";
function checkscope() {
//这是一个自由变量
let scope = "local scope";
//这是一个闭包
function f() {
console.log(scope);
};
return f;
};
let foo = checkscope();
foo();
我们使用伪代码分别表示执行栈中上下文的变化,以及上下文创建的过程,首先执行栈中永远都会存在一个全局执行上下文。
常见的闭包
- 将函数作为另一个函数的返回值
- 将函数作为实参传递给另一个函数
注:下面代码虽然将内部函数进行返回单是没有产生闭包
function fn(){
var a = 1;
function fn1(){ //执行函数定义
//没有引用外部函数的变量(函数)
}
return fn1;
}
fn(); //调用外部函数
类型1:将函数作为另一个函数的返回值
function fn(){
var a = 1;
function fn1(){ //执行函数定义
console.log(a);
}
return fn1;
}
fn(); //调用外部函数
类型2:将函数作为实参传递给另一个函数
function showDelay(mes,time){
setTimeout(function () {alert(mes);},time);
}
showDelay('延迟2s后的信息',2000);
注:下面是没有产生闭包的:原因是内部函数没有引入外部函数的变量
function showDelay(mes,time){
setTimeout(function () {alert('1');},time);
}
showDelay('延迟2s后的信息',2000);
闭包的作用(优点)
- 可以沿着作用域链查找,从而读取自身函数外部的变量,并让这些外部变量始终保存在内存中(延长了局部变量的生存周期)
- 在函数外部就可以操作函数内部的数据(变量或函数)即将内部函数保存到函数外部的变量中
- 可以隔离作用域,不造成全局污染
闭包的缺点
由于闭包长期驻留内存,则长期这样会导致内存泄露
【解决:】
将暴露外边的全部闭包变量置为null
闭包的生命周期
- 产生:在嵌套内部函数定义完成时便产生了闭包(而不是调用内部函数时产生)
注:也就是当外层函数调用时,内部函数这个时候会完成定义 - 死亡:在嵌套内部函数成为垃圾时(也就是内部函数的引用为null)
注:在调用外层函数是要将内部函数保存到外部,如果没有将内部函数保存到外部,那么此时形成的闭包则会成为垃圾;因为闭包是存在于内部函数中,而内部函数的定义又是在外部函数的环境中完成的,当外部函数执行完成之后就会销毁自己的作用域链进行销毁,因此内部的函数定义也就跟着销毁,如果在外层函数销毁之前就将内部函数的引用保存到外部(全局),那么即使在外层函数销毁后,仍然可以引用其内部变量,因为此时的它已经被保存到外部了
使用闭包的注意事项
闭包的性能与内存占用
我们已经知道了闭包是自带执行环境的函数(可以使它外部的执行环境不被销毁),相比普通函数,闭包对于内存的占用还真就比普通函数大
function bindEvent(){
let ele = document.querySelector('.ele');
ele.onclick = function () {
console.log(ele.style.color);
};
};
bindEvent();
比如这个例子中,由于点击事件中使用到了外层函数中的DOM ele,导致 ele 始终无法释放,大家都知道操作DOM本来是件不太友好的事情,你现在操作别人不说,还抓着不放了,你良心不会痛?
比如这个例子你要获取color属性,那就单独复制一份color属性,在外层函数执行完毕后手动释放ele,像这样:
function bindEvent() {
let ele = document.querySelector('.ele');
let color = ele.style.color;
ele.onclick = function () {
console.log(color);
};
ele = null;
};
bindEvent();
闭包的使用场景
模拟私有属性、方法
在Java这类编程语言中是支持创建私有属性与方法的,所谓私有属性方法其实就是这些属性方法只能被同一个类中的其它方法所调用,但是JavaScript中并未提供专门用于创建私有属性的方法,但我们可以通过闭包模拟它,比如:
let fn = (function () {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
};
return {
increment: function () {
changeBy(1);
},
decrement: function () {
changeBy(-1);
},
value: function () {
console.log(privateCounter);
}
};
})();
fn.value(); //0
fn.increment();
fn.increment();
fn.value(); //2
fn.decrement();
fn.value(); //1
这个例子中我们通过自执行函数返回了一个对象,这个对象中包含了三个闭包方法,除了这三个方法能访问变量privateCounter与 changeBy函数外,你无法再通过其它手段操作它们。
节点变量循环绑定click事件
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<link rel="stylesheet" href="">
</head>
<body>
<p id="info">123</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>
<script>
function showContent(content){
document.getElementById('info').innerHTML = content;
};
function setContent(){
var infoArr = [
{'id':'email','content':'your email address'},
{'id':'name','content':'your name'},
{'id':'age','content':'your age'}
];
for (var i = 0; i < infoArr.length; i++) {
var item = infoArr[i];
document.getElementById(item.id).onfocus = function(){
showContent(item.content)
}
}
}
setContent()
//循环中创建了三个闭包,他们使用了相同的词法环境item,item.content是变化的变量
//当onfocus执行时,item.content才确定,此时循环已经结束,三个闭包共享的item已经指向数组最后一项。
/**
* 解决方法1 通过函数工厂,则函数为每一个回调都创建一个新的词法环境
*/
function showContent(content){
document.getElementById('info').innerHTML = content;
};
function callBack(content){
return function(){
showContent(content)
}
};
function setContent(){
var infoArr = [
{'id':'email','content':'your email address'},
{'id':'name','content':'your name'},
{'id':'age','content':'your age'}
];
for (var i = 0; i < infoArr.length; i++) {
var item = infoArr[i];
document.getElementById(item.id).onfocus = callBack(item.content)
}
}
setContent()
/**
* 解决方法2 绑定事件放在立即执行函数中
*/
function showContent(content){
document.getElementById('info').innerHTML = content;
};
function setContent(){
var infoArr = [
{'id':'email','content':'your email address'},
{'id':'name','content':'your name'},
{'id':'age','content':'your age'}
];
for (var i = 0; i < infoArr.length; i++) {
(function(){
var item = infoArr[i];
document.getElementById(item.id).onfocus = function(){
showContent(item.content)
}
})()//放立即执行函数,立即绑定,用每次的值绑定到事件上,而不是循环结束的值
}
}
setContent()
/**
* 解决方案3 用ES6声明,避免声明提前,作用域只在当前块内
*/
function showContent(content){
document.getElementById('info').innerHTML = content;
};
function setContent(){
var infoArr = [
{'id':'email','content':'your email address'},
{'id':'name','content':'your name'},
{'id':'age','content':'your age'}
];
for (var i = 0; i < infoArr.length; i++) {
let item = infoArr[i]; //限制作用域只在当前块内
document.getElementById(item.id).onfocus = function(){
showContent(item.content)
}
}
}
setContent()
</script>
</body>
</html>
闭包应用场景之setTimeout
//原生的setTimeout传递的第一个函数不能带参数
setTimeout(function(param){
alert(param)
},1000)
//通过闭包可以实现传参效果
function func(param){
return function(){
alert(param)
}
}
var f1 = func(1); // 产生了闭包
setTimeout(f1,1000);
练习题
练习1:
function a(){
function b(){
var bbb = 234;
console.log(aaa);
}
var aaa = 123;
return b;
}
练习2:
function fun(n,o){
console.log(o);
return {
fun: function (m) {
return fun(m,n);
}
}
}
var a = fun(0); a.fun(1);a.fun(2);a.fun(3);
var b = fun(0).fun(1).fun(2).fun(3);
var c = fun(0).fun(1);c.fun(2);c.fun(3);
从之态打印结果:
underfined 0 0 0
underfined 0 1 2
underfined 0 1 1
分析:每次调用fun对象的属性都是调用外部函数,因此会产生闭包;而这里使用的都是第一次产生的闭包,因此每次输出的都是0
这里每次使用的都是刚产生的闭包
练习3
var name = 'The window';
var object = {
name:'my object',
getNameFun: function () {
return function () {
return this.name;
}
}
}
console.log(object.getNameFun()());
控制台输出结果:The window
分析:
- 虽然有函数嵌套,但是内部函数并没有使用外部函数的变量(函数),因此没有形成闭包
object.getNameFun
获取object对象的getNameFun
属性object.getNameFun()
调用object对象属性里面的函数- .
object.getNameFun()()
调用object对象属性里面的函数所返回的函数;因此内部函数里面的this
指向的是window
练习4:
var name = 'The window';
var object = {
name:'my object',
getNameFun: function () {
var that = this;
return function () {
return that.name;
}
}
}
console.log(object.getNameFun()());
控制台输出结果:the object
分析:
- 由于内部函数使用了外层函数的变量
that
;因此形成了闭包 object.getNameFun
获取object对象的getNameFun
属性object.getNameFun()
调用object对象属性里面的函数 (这里因为是object调用的函数,因此this指向object并赋值给that)- .
object.getNameFun()()
调用object对象属性里面的函数所返回的函数;因为使用的是外部函数that变量,而that指向object