<<你不知道的JavaScript>>上卷
- 定义:当函数可以记住并访问所在词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行
- 小栗子:
function foo() {
var a = 2;
function bar() {
console.log(a) // 2
}
bar()
}
foo()
是不是有点像嵌套作用域,函数 bar() 可以访问外部作用域中的的变量a。
- 这个是闭包吗?
function foo() {
var a = 2;
function bar() {
console.log(a)
}
return bar;
}
var baz = foo()
baz() // 2 -- 这就是闭包的效果
JS引擎有垃圾回收器来释放不在使用的内存空间,通常情况下 foo() 会被回收,然而以上例子显然不能进行回收,因为有闭包的存在,导致它会阻止这件事情的发生,使得它的内部作用域依然存在,故没有被回收。
- bar() 依然持有对该作用域的引用,而这个引用就叫做闭包,所以最后 baz是可以访问变量 a 的。
function foo() {
var a = 2
function baz() {
console.log(a) // 2
}
bar(baz)
}
function bar(fn) {
fn() // 这不就是闭包
}
foo()
把 baz 传递给 bar ,当我们调用这个内部函数时(fn),访问了 a
var fn
function foo() {
var a = 2
function baz() {
console.log(a)
}
fn = baz
}
function bar() {
fn() // 闭包
}
foo()
bar() // 2
将传递内部函数传递到所在的词法作用域外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
- 那是不是说?
function wait(message) {
setTimeout(function timer() {
console.log(message)
},1000)
}
wait("Hellow,closure!")
timer将值传递给setTimeout(...),timer具有涵盖wait(...)作用域的闭包,因此还保留对变量message的引用,wait(...)执行1000毫秒后,它的内部作用域不会消失,timer函数依然保持有wait(...)作用域的闭包。
function setupBot(name, selector) {
$(selector).click(function activator() {
console.log("Activating:" + name)
})
}
setupBot("Closure Bot 1", "#bot_1")
setupBot("Closure Bot 2", "#bot_2")
在定时器、事件监听器、Ajax请求、跨窗口通信、Wed Workers 或者其他的异步(或者同步)任务中,只要使用回调函数,实际上就是在使用闭包!
- 它是闭包吗?
var a = 2
(function IIFE() {
console.log(a)
})()
从严格意义来说它并不是闭包,因为 IIFE 并不是在它本身的词法作用域以外执行的。
那到底什么是闭包?
-
作用:能够在函数定义的作用域外,使用函数定义作用域内的局部变量,并且不会污染全局。
-
原理:基于词法作用域链和垃圾回收机制,通过维持函数作用域的引用,让函数作用域可以在当前作用域外被访问到,也就是说闭包就是能够读取其他函数内部变量的函数。
作用域
- 作用域:用于确定在何处以及如何查找变量(标识符)的一套规则。
- 词法作用域:词法作用域是定义在词法阶段的作用域。词法作用域是由写代码时将代码和块作用域写在哪里来决定的,因此当词法作用域处理代码是会保持作用域不变(大部分情况)。
- 块作用域:指的是变量和函数不仅可以属于所处的作用域,也可以属于某个代码块(通常用{}包裹)。常见的块级作用域有 with,try/catch,let,const 等。
- 函数作用域:属于这个函数的全部变量都可以在整个函数范围内使用及复用(包括嵌套作用域)。
- 作用域链:查找变量时,先从当前作用域开始查找,如果没有找到,就会到父级(词法层面上的父级)作用域中查找,一直找到全局作用域。作用域链正是包含这些作用域的列表。
闭包的用途
- 可以读取函数内部的变量,并且保护函数内的变量安全,无法通过其他途径访问到,因此保护了变量的安全性。
- 在内存中维持一个变量,不会被垃圾回收。
function f1(){
var n=520;
function f2(){
console.log(n); // 520
}
}
f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。
- 通过保护变量的安全实现JS私有属性和私有方法(不能被外部访问)推荐阅读:javascript.crockford.com/private.htm… 私有属性和方法在Constructor外是无法被访问的
function Constructor(...) {
var that = this;
var membername = value;
function membername(...) {...}
}
闭包的缺点
- 不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包把变量存在内存,导致处理速度和内存消耗方面对脚本性能具有负面影响,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
- 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。 例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用时,方法都会被重新赋值一次(也就是说,对于每个对象的创建,方法都会被重新赋值)。
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
this.getName = function() {
return this.name;
};
this.getMessage = function() {
return this.message;
};
}
在上面的代码中,我们并没有利用到闭包的好处,因此可以避免使用闭包。修改成如下:
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype = {
getName: function() {
return this.name;
},
getMessage: function() {
return this.message;
}
};
但我们不建议重新定义原型。可改成如下例子:
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype.getName = function() {
return this.name;
};
MyObject.prototype.getMessage = function() {
return this.message;
};