JS进阶-理解闭包
定义
闭包是指函数声明时所处作用域外被调用的函数。所以闭包也是函数,只不过要满足3个条件才叫闭包:
- 访问函数所处作用域。
- 函数嵌套。因为只有函数嵌套才能创建不同的作用域。
- 函数所处作用域外被调用。
function foo() {
var a = 1;
function foo2() {
console.log(a); // 1
}
return foo2;
}
foo()();
// 等价于
function foo() {
var a = 1;
return function foo2() {
console.log(a); // 1
}
}
foo()();
示例中在全局作用域中被调用的foo2函数就是一个闭包。foo2声明时所处的作用域就是foo函数作用域,它在foo函数作用域外的全局作用域中被调用,并且foo2中的变量a访问了foo2所处的作用域,满足闭包的三个条件。
IIFE立即执行函数
函数定义后立即被调用的函数就是立即执行函数,全称是立即调用的函数表达式IIFE(Imdiately Invoked Function Expression)
严格来说IIFE不是闭包,因为不满足闭包的三个条件。但是如果在IIFE中嵌套了函数,那就另当别论了。
IIFE的常见形式
// 常用
(function(){ /* code */ }());
(function(){ /* code */ })();
// 其他
var i = function(){ /* code */ }();
true && function(){ /* code */ }();
0, function(){ /* code */ }();
!function(){ /* code */ }();
~function(){ /* code */ }();
-function(){ /* code */ }();
+function(){ /* code */ }();
new function(){ /* code */ };
new function(){ /* code */ }();
注意: JS引擎规定,如果function关键字出现在首行,一律解释成函数声明。所以function关键字前面添加!、~、-、+
符号,是为了让引擎把它解释成一个表达式。
// 函数声明必须要定义函数名称,所以报错
function(){ /* code */ }(); // Uncaught SyntaxError: Unexpected token (
// 这个并不是函数调用,而是函数声明和分组操作符的组合。由于分组操作符不能为空,所以报错。
function foo(){ /* code */ }(); // Uncaught SyntaxError: Unexpected token )
// 这样虽然不报错,但是没什么意义。
function foo(){ /* code */ }(1);
// 函数表达式
!function(){ /* code */ }();
注意: 函数表达式和IIFE使用时,表达式后面的分号必须要有。
// 示例1
var a = function(){
return 1;
}
(function(){
console.log(1);// 报错
})();
// 示例2
var a = function(){
return 1;
};
(function(){
console.log(1);// 正常
})();
闭包的常见形式
返回值
var foo = function(){
var a = 1;
var foo2 = function() {
return a;
}
return foo2;
}
var fn = foo();
console.log(fn()); // 1
函数赋值
把内部函数赋值给外部变量
var fn;
var foo = function() {
var a = 1;
var foo2 = function() {
return a;
}
fn = foo2;
}
foo();
console.log(fn()); // 1
函数参数
通过把内部函数作为参数传递给外部函数的形式实现闭包。
var fn = function(f) {
console.log(f()); // 1
};
var foo = function() {
var a = 1;
var foo2 = function() {
return a;
}
fn(foo2);
};
foo();
上面的3种闭包形式,foo函数声明后是立即被调用的,所以上面的示例可以用IIFE表示。
var fn = (function(){
var a = 1;
var foo2 = function() {
return a;
}
return foo2;
})();
console.log(fn()); // 1
var fn;
var foo = (function() {
var a = 1;
var foo2 = function() {
return a;
}
fn = foo2;
})();
console.log(fn()); // 1
var fn = function(f) {
console.log(f()); // 1
};
var foo = (function() {
var a = 1;
var foo2 = function() {
return a;
}
fn(foo2);
})();
闭包的常见用途
循环赋值
function foo() {
var arr = [];
for(var i = 0; i < 3; i++) {
arr[i] = function() {
return i;
}
}
return arr;
}
var ret = foo();
console.log(ret[0]()); // 3
上面的循环,没有出现预想的结果0,原因是在调用匿名函数时,通过作用域找到的不是循环时候的瞬间值,而是循环结束后的索引值。
正确写法:
function foo() {
var arr = [];
for(var i = 0; i < 3; i++) {
arr[i] = (function(j) {
return function() {
return j;
};
})(i)
}
return arr;
}
var ret = foo();
console.log(ret[0]()); // 0
变量私有化
通过闭包把变量私有化,避免污染全局作用局。
var getVal, setVal;
(function(){
var secret = 0;
getVal = function() {
return secret;
}
setVal = function(v) {
secret = v;
}
})()
setVal(3);
getVal(); // 3
迭代器
迭代器
function setup(arr) {
var i = 0;
return function() {
return arr[i++];
}
}
var next = setup(['a','b','c']);
next(); // 'a'
next(); // 'b'
next(); // 'c'
累加器
var add = (function(){
var i = 0;
return function() {
return ++i;
}
})();
console.log(add()) // 1
console.log(add()) // 2
闭包的缺点
使用闭包是有一定代价的,闭包函数所处作用域中的变量会一直存在于内存中,这是为了保证闭包在调用的时候,能够通过作用域链找到这个变量,直到页面关闭内存才会被释放。
由于闭包占用内存空间,所以使用闭包要尽量少用。并且使用结束后通过把值赋值为null解除引用,以便尽早释放内存。
var foo = function(){
var a = 1;
var foo2 = function() {
return a;
}
return foo2;
}
var fn = foo();
fn();
fn = null;
胖胖熊笔记,笔记已迁移