js闭包详解
定义
- MDN文档:一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)
- js高程:闭包是指有权访问另一个函数作用域中的变量的函数(意思是闭包是个函数)
简单介绍
先举两个例子:
function f1 () {
var a = 1
function f2 () {
console.log(a)
}
f2()
}
f1() // 1
- 显然,f1调用了f2,f2引用了父级作用域的a变量,这都是常规操作,并不是闭包。那如果我想在f1外边直接调用f2呢?而不是通过f1去调用:
function f1 () {
var a = 1
function f2 () {
console.log(a)
}
return f2
}
const closure = f1()
closure() // 1
-
现在呢?我们把f1中的f2这个函数返回给了closure(此时f1已经执行完毕,按正常进度a已经被销毁了),相当于closure就是函数f2,调用时(在外部调用f2),发现f2依旧调用到了f1中的变量a,说明a没有销毁,而是在内存中持续存在。化用MDN的解释:closure是执行f1时创建的f2函数实例的引用,f2的实例维持了一个对它词法环境(变量a存在于其中)的引用,导致a暂时未被销毁。
-
事实也的确是这样,被闭包包装的变量并不会在函数销毁后进入js的垃圾回收机制,而是继续存在于内存中,而这些变量需要执行额外的操作进行销毁。所以,可以看出闭包对性能的影响是负面的,在使用闭包时一定要考虑清除是否有必要。
-
可以看出闭包有这种功能:能在外部调用函数(f1)内部的函数(f2)时调用其父级作用域(f1)里的变量,也就是使得本该被销毁的变量没有被销毁,同时还能被调用
闭包与变量
- 闭包所保存的是整个变量对象,而不是某个特殊的变量,所以它只能取得变量的最后一个值
function f () {
var a = new Array()
for (var i = 0; i < 10; i++) {
a[i] = function () {
return i
}
}
return a
}
let b = f() // 返回的是一个函数数组
b[0]() // 10
b[1]() // 10
...
- 这个例子说明,b初始化后,f执行完毕,i经历循环取得值10,意味着f函数的变量对象(变量对象储存了执行上下文的所有变量和函数,这是作用域相关知识里的)此时保存的是 “ i = 10 ”, 而当你调用b[i] ()时会重新取得i的值,且i的值已经固定
闭包与this对象
var name = "Window"
var obj = {
name: "Object",
getName: function () {
return function () {
return this.name
}
}
}
// 为何会是双括号:因为第一个括号只是执行getName函数,而该函数又返回一个函数,所以得再加一个“()”来执行返回的函数
alert(obj.getName()()) // "Window"
- 为什么 输出“ Window ” ?最后一个执行语句可以被拆分理解,首先是执行
obj.getName()
,返回了一个函数,相当于下一步执行返回函数func()
(相当于在全局调用func函数),此时该函数中的this指向全局对象window,因为this是运行时绑定机制,所以此时返回函数的执行追溯它的上一级就是全局。
var name = "Window"
var obj = {
name: "Object",
getName: function () {
var that = this
return function () {
return that.name
}
}
}
alert(obj.getName()())
- 这里与上题不同的是,this已经赋值给that,执行
obj.getName()
时,this指向obj对象,所以that也是,所以执行返回函数func()
时由于是that而非this(this会随环境改变),所以返回的是obj的name(“Object”)
面试题
- 理解闭包不是一蹴而就,多看资料多看题是很有必要的
for ( var i = 0 ; i < 5; i++ ) {
// 此处涉及到 “事件循环” 知识
setTimeout(function(){ // 该定时器被加入任务队列,循环结束后实行
console.log(i);
}, 0);
}
// 5 5 5 5 5
for ( var i = 0 ; i < 5; i++ ) {
// 每一次循环,函数都会开辟一个新的作用域,五个作用域分别存了0,1,2,3,4
// 每次定时器的函数调用的i都是不同作用域里的
(function(j){
setTimeout(function(){
console.log(j);
}, 0);
})(i);
}
// 0 1 2 3 4
for ( let i = 0 ; i < 5; i++ ) {
// let定义了块级作用域,类似上题函数的效果
setTimeout(function(){
console.log(i);
},0);
}
// 0 1 2 3 4
function f1(a,b) {
console.log(b)
return {
fun: function (c) {
return fun(c,a);
}
};
}
var d = f1(0); // undefined,d:{fun: func...}
d.fun(1); // 0
d.fun(2); // 0
d.fun(3); // 0
- 为什么末尾三个输出都是0?按照普通思路,一步步追溯你可能会以为返回undefined,这说明你忽略了闭包。首先,d是什么?d是个对象,里面有个fun属性,该属性是一个函数(暂且称作f2,闭包)。而d初始化时f1中a为0,所以当执行d.fun(1)时,f2中的a会引用f1中的a=0,所以无论传入什么参数,a都是恒定的,f2外部的f1已经规定好了。