闭包
1.闭包入门
闭包有许多种理解方式,尽管我的理解不能算是绝对正确,但是我希望它能加深你的理解。你可以随意copy我给出的示例,
观察它们的工作方式。最终你会有深入的理解闭包。
在C语言及其他诸多语言中,函数作为栈上自动内存管理的一部分,当函数调用结束时,为该函数分配的内存会被清除。
而在 JavaScript 中,即使调用完函数,该函数内部定义的变量和方法任然会保留在内存中。在函数执行完后保留函数内部
定义的变量和方法的链接,这就是闭包工作方式的一部分。
JavaScript是一门不断进化的语言。在闭包出现的时候, JavaScript还没有类或私有变量的概念。可以说在ES6之前,
闭包可以用来大致模拟类似于对象的私有方法的内容。闭包是JavaScript传统编程风格的一部分。这是面试中最常见的问题之一。
2.什么是闭包
在函数退出了之后,闭包还能保留对所有局部函数变量的引用。
如果对JavaScript中的作用域规则和执行语境委托控制流一无所知,那么闭包理解起来会很难,但是如果从简单的示例入手,这会变的简单一些。
要理解闭包,至少需要理解如下结构。这主要是因为JavaScript允许在一个函数中定义另一个函数。从技术角度来讲这就是闭包。
//函数global定义在与window一起创建的全局作用域中 global的this指向window
function global(){
// 在调用global时,将为该函数创建一个新的执行语境。在声明绑定示例化的过程中,
// 在JavaScript解释器的内部,inner将作为一个新的局部对象被创建,其作用域指
// 向global的执行语境的变量环境
function inner(){
console.log("inner")
}
inner(); //调用inner
}
global(); //输出"inner"
复制代码
在下面的代码中,全局函数sendEmail 定义了一个匿名函数,并将其赋值给变量 send。该变量仅对 sendEmail 函数的作用域可见,
而对全局作用域不可见。
function sendEmail(){
let msg = `"${name}" 》 "${words}"`;
let send = function(){ console.log(msg) };
send()
}
sendEmail('小动脉','你好');
复制代码
当我们调用 sendEmail 时,它会创建并调用 send 方法。全局作用域无法直接调用 send 方法。
控制台输出:小动脉 > 你好
我们可以通过调用从函数返回的私有方法(内部函数),来公开对变量,方法等等的引用。在下面代码中第 004 行并不是调用send 方法,而是返回对它的引用。除此之外,下面的代码块和上一个代码块完全相同
function sendEmail(){
let msg = `"${name}" 》 "${words}"`;
let send = function(){ console.log(msg) };
return send;
}
//创建对sendEmail的引用
let ref = sendEmail('小动脉','你好');
//用变量名进行调用
ref();
复制代码
现在,我们就可以在全局作用域中通过引用来调用 send 方法。即使在调用完 sendEmail 函数之后,变量 msg 和 send 仍会保留在内存中。在 C语言中,它们会从栈内存中自动清除,我们将无法再次访问它们。但在JavaScript中可以。
我们再来看一个示例。在下面的示例中,首先定义全局变量 get , set , add , 和 decreased。
let get, set, add, decreased;
function manager(){
console.log("manager()");
let number = 15;
get = function(){ console.log(number) };
set = function(val){ number = val };
add = function(){ number++ };
cut = function(){ number-- }
}
复制代码
为了将匿名函数赋值给全局函数,我们至少需要运行一次 manager 。如下所示
manager(); //初始化manager
add(); //15
for (let i = 0; i < 200; i++)( add() );
get(); //215
cut();
get(); //214
set(755);
get() //755
let old_get = get; //保存对get的引用
manager(); //再次初始化manager
get(); //15
old_get(); //755
复制代码
详解
在第一次调用manager之后,执行的函数和所有的全局引用都链接到了它们各自的匿名函数。这样就创建了我们的第一个闭包。
我们调用 add , cut 和 set , 来修改manager 函数中定义的变量 number 的值。每一步都会用 get 来打印该值以确保它已经改变了。
3.漂亮的闭包
函数式编程使用闭包,其原因与面对对象编程使用私有方法类似。闭包以函数的形式为对象提供 api 方法。
如果根据这个思路进一步创建一个看起来很漂亮的闭包,返回多个方法,而不是只有一个方法,那会怎么样呢?请看以下示例
let get = null; //全局getter函数的变量
function closure(){
this.inc = 0;
get = () => this.inc; //getter
function add(){ this.inc++; }
function cut(){ this.inc--; }
function set(v){ this.inc = v; }
function reset(){ this.inc = 0 ;}
function del(){
delete this.inc; //变为undefined 即未赋值
this.inc = null; //将其赋值为null
console.log("this.inc已被删除");
}
function readd(){
//如果为null或undefined
if(!this.inc){
this.inc = "added"
}
}
//同时返回多个方法
return [add, cut, set, reset, del, readd];
}
复制代码
del方法会从对象中彻底删除 inc 属性,而 readd 会重新添加该属性。为简单起见,这里并没有执行防错处理。如果在 inc 属性已被删除后去访问这些方法时,将会发生引用错误。
//初始化闭包
let f = closure();
//变量f现在指向一组公开的方法,把方法赋值给函数名,它们就进入了全局作用域了
//为各个方法赋上唯一的函数名
let add = f[0];
let cut = f[1];
let set = f[2];
let res = f[3];
let del = f[4];
let readd = f[5];
//注意要用get()方法来获取this.inc的值,其余方法只是对this.inc进行操作并没有返回this.inc, 方法后的注释是this.inc的值
add(); //1
add(); //2
add(); //3
cut(); //2
get(); //2
set(7); //7
get(); //7
readd(0); //0
get(); //0
//最后可以用 del 方法删除该属性本身
//删除属性
del(); //null
get(); //null
//这时调用其他方法将会报错,我们需要将inc属性重新添加到对象中
readd();
get(); //"added"
//将 inc 属性的值重置为0,递增1
res(); //0
add(); //1
get(); //1
复制代码
4.闭包小结
如果在一个函数中创建另一个函数,就创建了闭包。
当调用的函数包含另一个函数时,就会新建执行语境,它持有所有局部变量的全新副本。通过链接到全局作用域中定义的变量名,或在外层函数中使用 return 关键字返回闭包,就可以在全局作用域中创建它们的引用。
闭包可以使你持有所有局部函数变量的引用,在函数执行退出后仍然可使用。
注意:new Function 构造函数不会创建闭包。这是应为构造函数创建的函数只能在全局作用域中运行
5.柯里化
柯里化(currying) 指的是将一个多参数的函数拆分成一系列函数,每个拆分后的函数都只接受一个参数。通过定义时立即返回所有内部函数来链接闭包,就可以创建柯里化函数。
let add = function(a){
return function(b){
return a+b;
}
}
let f = add('aa');
console.log(f('bb')); //aabb
console.log(f('cc')); //aacc
console.log(f('dd')); //aadd
//函数 add 会返回一个匿名函数,因此当函数 add 接收了参数 1 赋值给 f 的时候,它会根据第二给参数再次进行调用。
复制代码