什么是闭包?
闭包是指那些能够访问自由变量的函数。
自由变量:指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。
闭包 = 函数 + 函数能够访问的自由变量
ECMAScript中,闭包指的是:
- 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
- 从实践角度:以下函数才算是闭包:
- 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
- 在代码中引用了自由变量
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
var foo = checkscope();
foo(); //"local scope"
- 进入全局代码,创建全局执行上下文,全局执行上下文压入执行上下文栈
- 全局执行上下文初始化
- 执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 执行上下文被压入执行上下文栈
- checkscope 执行上下文初始化,创建变量对象、作用域链、this等
- checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出
- 执行 f 函数,创建 f 函数执行上下文,f 执行上下文被压入执行上下文栈
- f 执行上下文初始化,创建变量对象、作用域链、this等
- f 函数执行完毕,f 函数上下文从执行上下文栈中弹出
Q:当 f 函数执行的时候,checkscope 函数上下文已经被销毁了啊(即从执行上下文栈中被弹出),怎么还会读取到 checkscope 作用域下的 scope 值呢?
A: f 执行上下文维护了一个作用域链:
fContext = {
Scope: [AO, checkscopeContext.AO, globalContext.VO],
}
就是因为这个作用域链,f 函数依然可以读取到 checkscopeContext.AO 的值,说明当 f 函数引用了 checkscopeContext.AO 中的值的时候,即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。
Javascript语言特有的"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,子可以访问父作用域的变量,但是父无法访问子作用域中的变量。
function f1(){
var n=999;
}
alert(n); // error
Q:如何从外部读取内部变量?
在函数内部创建一个函数f2,并返回f2
function f1() {
var n = 999;
function f2() {
console.log(n);
}
return f2;
}
let result = f1();
result(); // 999
如上f2
函数就是一个闭包。闭包就是可以读取其他函数内部变量的函数,也可以理解是 定义在一个函数内部的函数。 闭包是将函数内部作用域和函数外部连接起来的一座桥梁。
闭包的作用
- 读取函数内部的变量
- 让这些变量始终保持在内存中
function f1() {
var n = 999;
nAdd = function () {
n += 1
}
function f2() {
console.log(n);
}
return f2;
}
var result = f1();
result(); // 999
nAdd();
result(); // 1000
result实际是闭包f2函数,两次运行值不一样,说明函数f1的局部变量n一直保存在内存中,并没有再f1调用后被自动清除。
原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。
nAdd=function(){n+=1}
没有var关键字,所以nAdd是全局变量,其值是一个匿名函数,而这个匿名函数本身是一个闭包,所以可以再函数外部对函数内部的局部变量进行操作。
http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html
闭包的特点
- 闭包外层是一个函数(闭包是定义在函数内部的函数)
- 闭包内部都有函数
- 闭包会返回内部函数(因为闭包的目地是外部要访问内部变量)
- 闭包返回的函数内部不能有return this... 因为这是this已经改变
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name;
};
}
};
alert(object.getNameFunc()());
- 执行闭包后,闭包内部变量会存在,而闭包内部函数的内部变量不存在。
闭包的应用
- 模拟私有变量的实现
仅在对象内部生效,无法从外部触及,这样的变量,就是私有变量。
// 利用闭包生成IIFE,返回 User 类
const User = (function() {
// 定义私有变量_password
let _password
class User {
constructor (username, password) {
// 初始化私有变量_password
_password = password
this.username = username
}
login() {
// 这里我们增加一行 console,为了验证 login 里仍可以顺利拿到密码
console.log(this.username, _password)
// 使用 fetch 进行登录请求,同上,此处省略
}
}
return User
})()
let user = new User('xiuyan', 'xiuyan123')
console.log(user.username) // xiuyan
console.log(user.password) // undefined
console.log(user._password) // undefined
user.login() // xiuyan xiuyan123
我们把 _password 放在了 login 方法的外层函数作用域里,并通过立即执行 User 这个函数,创造出了一个闭包的作用域环境。我们看到不管是 password,还是 _password,都被好好地保护在了 User 这个立即执行函数的内部。
看到User对外暴露的属性确实已经没有 password,通过闭包,我们成功达到了用自由变量来模拟私有变量的效果!
- 偏函数和柯里化
- 柯里化:把接受 n 个参数的 1 个函数改造为只接受 1个参数的 n 个互相嵌套的函数的过程。也就是 fn (a, b, c)fn(a,b,c) 会变成 fn (a)(b)(c)fn(a)(b)(c)。
- 偏函数:固定你函数的某一个或几个参数,然后返回一个新的函数(这个函数用于接收剩下的参数)。
function generateName(prefix) {
return function(type, itemName) {
return prefix + type + itemName
}
}
// 把3个参数分两部分传入
var itemFullName = generateName('大卖网')('母婴', '奶瓶')
使用闭包的注意点
- 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
- 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
思考题
var test = (function() {
var num = 0
return () => {
return num++
}
}())
for (var i = 0; i < 10; i++) {
test()
}
console.log(test())
function test (){
var num = []
var i
for (i = 0; i < 10; i++) {
num[i] = function () {
console.log(i)
}
}
return num[9]
}
test()()
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = (function (i) {
return function(){
console.log(i);
}
})(i);
}
data[0]();
data[1]();
data[2]();