一、闭包的定义
【JavaScript高级程序设计】
闭包:指有权访问另一个函数作用域中的变量的函数。
创建闭包的常见方式:在一个函数内部创建另一个函数。
【百度百科 】闭包就是能够读取其他函数内部变量的函数。例如在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。
总结: 在JavaScript中,在函数中定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。
闭包中包含了内部函数的代码,以及所需外部函数中的变量的引用。
二、产生闭包的条件
(1) 函数内部定义另一个函数
(2) 内部函数访问了外部函数的变量
function outer() {
var num = 10;
function inner() {
console.log(num); // 产生
console.log(20); // 不产生
}
return inner;
}
outer()();
三、闭包的作用
(1) 保护变量数据(如全局变量污染问题)
(2) 将变量值保存在内存中
(3) 把变量作为自己的私有变量
- 应用1: 计数器
举个栗子:统计一个函数的调用次数
var count = 0;
function fn(){
count++;
console.log("调用次数是"+count);
}
fn(); // 调用次数是1
fn(); // 调用次数是2
fn(); // 调用次数是3
缺点:count是全局变量,不安全。==> 使用闭包
function outer(){
var count = 0;
function add(){
count++;
console.log("调用次数是"+count);
}
return add;
}
var result = outer();
result(); // 调用次数是1
result(); // 调用次数是2
result(); // 调用次数是3
- 应用2: 私有变量
举个栗子:使用闭包实现私有变量的读取和设置
function outer(){
var num = 10;
function set_num(n){
num = n;
}
function get_num(){
return num;
}
return {
set_num: set_num,
get_num: get_num
}
}
var obj = outer();
console.log(obj.get_num());
obj.set_num(20);
console.log(obj.get_num());
四、闭包容易遇到的坑点
(1) 引用的变量可能发生变化
(2) this指向问题
(3) 内存泄露问题
- 引用的变量可能发生变化
function outer() {
var result = [];
for (var i = 0; i<10; i++){
result[i] = function () {
console.log(i);
}
}
return result;
}
看样子result每个闭包函数打印对应数字,0,1,2,3,…,9。
实际不是!!!
因为每个闭包函数访问变量i是outer执行环境下的变量i,随着循环的结束,i已经变成10了,所以执行每个闭包函数,结果打印10, 10,10,10,…,10。
function outer() {
var result = [];
for(var i = 0; i<10; i++){
result[i] = function (num) {
return function() {
console.log(num);
// 此时访问的num,是上层函数执行环境的num,数组有10个函数对象,每个对象的执行环境下的number都不一样
}
}(i);
}
return result;
}
- this指向问题
在闭包中使用 this 对象可能会导致一些问题。
匿名函数的执行环境具有全局性,因此其 this 对象通常指向 window。
var name = "The Window";
var object = {
name: 'My Object',
getName: function() {
return function() {
console.log(this.name);
}
}
}
object.getName()(); // The Window
因为里面的闭包函数是在window作用域下执行的,也就是说,this指向window。
每个函数在被调用时都会自动取得两个特殊变量: this 和 arguments 。
内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。
解决方法:要想访问到,可以使用that保存this值,arguments同理。
var name = "The Window";
var object = {
name: "My Object",
getName: function(){
var that = this;
return function(){
console.log(that.name);
};
}
};
object.getName()(); // My Object
- 内存泄露问题
闭包占用的内存是不会被释放的,因此,如果滥用闭包,会造成内存泄漏的问题。
function showId() {
var el = document.getElementById("app");
el.onclick = function(){
alert(el.id);
// 这样会导致闭包引用外层的el,当执行完showId后,el无法释放
console.log(el);
}
}
释放闭包占用内存
function showId() {
var el = document.getElementById("app");
var id = el.id;
el.onclick = function(){
alert(id);
// 这样会导致闭包引用外层的el,当执行完showId后,el无法释放
console.log(el);
}
el = null; // 主动释放el
}
结论:闭包很强大,但是只有在必须使用闭包的时候才使用。
五、面试题
- 点击不同的按钮, 打印对应不同的下标: 0~9
var buttons = document.querySelectorAll('button');
console.log(buttons);
// 以为的答案 (no no no !!!)
for(var i = 0; i < buttons.length; i++) {
buttons[i].onclick = function () {
console.log(i);
}
}
// 第1种方式: 闭包
for (var i = 0; i < buttons.length; i++) {
(function (i) {
buttons[i].onclick = function () {
console.log(i);
}
})(i);
}
// 第2种方式: 闭包另一种方式
for(var i = 0; i < buttons.length; i++) {
buttons[i].onclick = (function (i) {
return function () {
console.log(i);
}
})(i);
}
// 第3种方式: let
for(let i = 0; i < buttons.length; i++) {
buttons[i].onclick = function () {
console.log(i);
}
}
// 第4种方式: 存下标
for(var i = 0; i < buttons.length; i++) {
buttons[i].index = i;
buttons[i].onclick = function () {
console.log(this.index);
}
}
// 第5种方式: forEach(实质也是闭包)
buttons.forEach(function (v, i) {
v.onclick = function () {
console.log(i);
}
})
// 第6种方式: bind
for(var i = 0; i < buttons.length; i++) {
buttons[i].onclick = (function (i) {
console.log(i);
}).bind(null, i);
}
// 第7种方式: with 块级作用域
for(var i = 0; i < buttons.length; i++) {
with({i:i}) {
buttons[i].onclick = function () {
console.log(i);
}
}
}
- 每隔一秒钟打印一个数字: 1,2,3,4,5,6,7,8,9,10
// 第1种方式:闭包
for(var i = 1; i <= 10; i++) {
(function(i) {
setTimeout(function() {
console.log(i);
}, i*1000);
})(i);
}
// 第2种方式: 在定时器里面使用闭包
for(var i = 1; i <= 10; i++) {
setTimeout((function(i) {
return function () {
console.log(i);
}
})(i), i*1000);
}
// 第3种方式: let
for(let i = 1; i <= 10; i++) {
setTimeout(function() {
console.log(i);
}, i*1000);
}
// 第4种方式: 传参(其实也就是闭包)
var outer = function(i) {
setTimeout(function() {
console.log(i);
}, i*1000);
}
for (var i = 1; i <= 10; i++) {
outer(i);
}
// 第5种方式: forEach(实质也是闭包)
var arr = [1,2,3,4,5,6,7,8,9,10];
arr.forEach(function(v, i) {
setTimeout(function() {
console.log(v);
}, v*1000);
})
// 第6种方式: bind
for (var i = 1; i <= 10; i++) {
setTimeout((function(i) {
console.log(i);
}).bind(null, i), i*1000);
}
// 第7种方式: with 块级作用域
for (var i = 1; i <= 10; i++) {
with({i:i}) {
setTimeout(function() {
console.log(i);
}, i*1000);
}
}
【核心思想:】不使用全局的 i ,使用自己作用域里的 i 。