这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战javascript
理解闭包,首先必须理解变量做用域,在ECMAScript5的标准中有两种做用域:全局做用域和函数做用域。 二者的调用关系是:java
let num = 1;
function test() {
let n = 2;
console.log(num); // 1
}
test();
console.log(n); // ReferenceError: n is not defined
复制代码
实际开发中会出于各类缘由,咱们必须得拿到函数内部的局部变量。数组
JavaScript 语言规定:父对象的全部变量,对子对象都是可见的,反之则不成立。即"链式做用域"结构(chain scope) 。 基于这一点,咱们就能够在目标函数内再定义一个函数,这个子函数就能够正常访问其父函数的内部变量。缓存
function parent() {
let n = 1;
function child() {
console.log(n); // 1
}
}
复制代码
既然子函数能够拿到父函数的局部变量,那么父函数直接返回这个子函数,不就达到了在全局做用域下访问函数内部变量的目的了。安全
function parent() {
let n = 1;
function child() {
console.log(n); // 1
};
return child;
}
let f1 = parent();
f1();
复制代码
上述的例子就是一个最简单的闭包的写法:函数 child 就是闭包,因此闭包就是一个“定义在函数内部的函数”。 在本质上,闭包就是一座链接函数内外的桥梁。markdown
闭包自己还具备如下几点重要的特性:闭包
3.1 函数做为返回值异步
上述的例子还能够进一步精简为匿名函数的写法: 经过匿名函数访问其外层函数的内部变量 num,而后外层函数返回该匿名函数,该匿名函数继续返回 num 变量。函数
function closure1(){
let num = 1;
return function(){
return num
}
}
let fn1 = closure1();
console.log(fn1()); // 1
复制代码
这样就能够在全局做用域下声明一个变量 fn1 来承接 num 变量,这样就达到了在全局做用域访问函数内局部变量的目的。oop
3.1.1 保存变量 闭包在能够读取函数内局部变量的同时,它还可让这些变量始终保存在内存中,不会在函数调用结束后,被垃圾回收机制回收。 好比这个例子:
function closure2(){
let num = 2;
return function(){
let n = 0;
console.log(n++,num++);
}
}
let fn2 = closure2();
fn2(); // 0 2
fn2(); // 0 3
复制代码
执行两次函数实例 fn2(),能够看到结果是略有差别的:
变量 n 是匿名函数的内部变量,在匿名函数调用结束后,它这块内存空间就会被正常释放,即被垃圾回收机制回收。
匿名函数内引用了其外层函数的局部变量 num,即便匿名函数的调用结束了,可是这种依赖关系依然存在,因此变量 num 就没法被销毁。一直保存在内存中 匿名函数下次调用时,就会继续沿用上次的调用结果。
利用闭包的这一特性,确实能够作简单的数据缓存。 可是也不能滥用闭包,这样很容易使内存消耗增大,进而致使内存泄漏或者网页的性能问题。
3.1.2 多个闭包函数彼此独立
同一个闭包机制能够建立出多个闭包函数实例,它们彼此独立,互不影响。
好比下面这个简单的例子:
function fn(num){
return function(){
return num++
}
}
复制代码
咱们分别声明三个闭包函数实例,分别传入不一样的参数。而后分别执行1,2,3次:
function fn(num){
return function(){
return num++
}
}
let f1 = fn(10);
let f2 = fn(20);
let f3 = fn(30);
console.log(f1()) // 10
console.log(f2()) // 20
console.log(f2()) // 21
console.log(f3()) // 30
console.log(f3()) // 31
console.log(f3()) // 32
复制代码
能够看到:f1(),f2(),f3()的第一次执行依次输出了10 20 30,多执行的也是在自身上次执行的结果上累加的,互相之间没有影响。
3.2 当即执行函数(IIFE)
上一种写法中函数只是做为返回值返回,而具体的函数调用是写在其余地方。那么咱们能不能让外层函数直接返回闭包的调用结果呢?
答案固然是能够的:采用当即执行函数(IIFE)的写法。
接下来就先了解一下具体什么是当即执行函数(IIFE):
咱们都知道,在 JavaScript中调用函数最经常使用的方法就是函数名以后跟圆括号()。有时,咱们须要在定义函数以后,当即调用该函数。可是你不能直接在函数定义以后加上圆括号,这样会产生语法错误。
// 提示语法错误
function funcName(){}();
复制代码
产生错误的缘由是,function 关键字既能够看成语句,也能够看成表达式。
// 语句
function f() {}
// 表达式
var f = function f() {}
复制代码
看成表达式时,函数能够定义后直接加圆括号调用。
var f = function f(){ return 1}();
console.log(f) // 1
复制代码
为了不解析的歧义,JavaScript 规定,若是 function 关键字出如今行首,一概解释成语句。那么若是咱们还想用 function 关键字声明函数后能当即调用,就须要让 function 不直接出如今行首,让引擎将其理解成一个表达式。 最简单的处理,就是将其放在一个圆括号里面。
(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();
复制代码
这就叫作“当即调用的函数表达式”(Immediately-Invoked Function Expression),即当即执行函数 简称IFE 。
3.2.1 定时器 setTimeout 的经典循环输出问题
了解过当即执行函数后,赶忙来看一个实例:使用for循环依次输出1~5。那么若是是下面的代码,它的运行结果是什么?
for (var i = 1; i <= 5; i++) {
setTimeout( function timer() {
console.log(i); // 6 6 6 6 6
}, 1000 );
}
复制代码
结果确定是输出5个6。缘由是 for 循环属于同步任务,setTimeout 定时器属于异步任务的宏任务范畴。JavaScript 引擎会优先执行同步的主线程代码,再去执行宏任务。
因此在执行 setTimeout 定时器以前,for 循环就已经结束了,此时循环变量 i = 6。而后 setTimeout 定时器被循环建立了 5 次,所有执行完毕也就输出了5个6。
可是咱们的目的是但愿输出1~5,这样显然没达到要求。在正式介绍当即执行函数(IIFE)的写法以前,我先说另一种方法:循环变量 i 使用let关键字声明。
for (let i = 1; i <= 5; i++) {
setTimeout( function timer() {
console.log(i); // 1 2 3 4 5
}, 1000 );
}
复制代码
为何换成let声明以后就能够呢?是由于要想实现1~5循环输出的本质要求是记住每次循环时循环变量的值。
而 let 的声明方式刚好就能够知足。传送门:JavaScript中 var、let、const 特性及区别详解
这样再来看当即执行函数(IIFE)的写法:
for (var i = 1; i <= 5; i++) {
(function(i){
setTimeout( function timer() {
console.log(i); // 1 2 3 4 5
}, 1000 );
})(i);
}
复制代码
把 setTimeout 定时器函数用一个外层匿名函数包裹构成闭包的形式,而后再采用当即执行函数(IIFE)的写法:继续用圆括号包裹外层匿名函数,而后跟上圆括号调用,并把每次的循环变量做为参数传入。 这样每次循环的结果就是闭包的调用结果:输出 i 的值;再根据闭包自己的特性之一:能够保存变量或参数,就知足了全部条件从而正确输出了1~5。
再多说一点,目前的输出形式是一秒后同时输出1~5;那我想这五个数字每隔一秒再输出一个呢?
for (var i = 1; i <= 5; i++) {
(function(i){
setTimeout( function timer() {
console.log(i);
}, i*1000 );
})(i);
}
复制代码
能够控制每一个setTimeout定时器的第二个参数:间隔时长,依次乘上循环变量 i 便可。 效果以下:
3.2.2 函数做为API的形参传入
闭包结合当即执行函数(IIFE) 的这种机制还有一类很重要的用处是:须要函数做为形参的各类API。 以数组的 sort() 方法为例:Array.prototype.sort() 方法中支持传入一个比较器函数,来让咱们自定义排序的规则。该比较器函数必需要有返回值,推荐返回 Number 类型。
好比如下的数组场景:咱们但愿你能编写一个 mySort() 方法:能够按照指定的任意属性值降序排列数组元素。 mySort() 方法确定须要两个形参:须要排序的数组 arr 和指定的属性值 property。
另外用到的 API 确定仍是 sort() 方法,这里咱们就不能直接传入一个比较器函数,而是采用闭包的IIFE写法: 属性值 property 做为参数传入外层匿名函数,而后匿名函数内部返回最终 sort() 方法须要的比较器函数。
var arr = [
{name:"code",age:19,grade:92},
{name:"zevi",age:12,grade:94},
{name:"jego",age:15,grade:95},
];
function mySort(arr,property){
arr.sort((function(prop){
return function(a,b){
return a[prop] > b[prop] ? -1 : a[prop] < b[prop] ? 1 : 0;
}
})(property));
};
mySort(arr,"grade");
console.log(arr);
/* [ {name:"jego",age:15,grade:95}, {name:"zevi",age:12,grade:94}, {name:"code",age:19,grade:92}, ] */
复制代码
3.3 封装对象的私有属性和私有方法
闭包同时也能够用于对象的封装,尤为是封装对象的私有属性和私有方法:
咱们封装了一个对象 Person,它拥有一个公共属性 name,一个私有属性 _age 和两个私有方法。 咱们不能直接访问和修改私有属性 _age,必须经过调用其内部的闭包 getAge 和 setAge。
function Person(name) {
var _age;
function setAge(n) {
_age = n;
}
function getAge() {
return _age;
}
return {
name: name,
getAge: getAge,
setAge: setAge
};
}
var p1 = Person('zevin');
p1.setAge(22);
console.log(p1.getAge()); // 22
复制代码
4.1 优势
4.1.1 实现封装,保护函数内的变量安全
采用闭包的写法能够把变量保存在内存中,不会被系统的垃圾回收机制销毁,从而起到了保护变量的做用。
function closure2(){
let num = 1;
return function(){
console.log(num++)
}
}
let fn2 = closure2();
fn2(); // 1
fn2(); // 2
复制代码
4.1.2 避免全局变量的污染
开发中应该尽可能避免使用全局变量,防止没必要要的命名冲突和调用错乱
// 报错
var num = 1;
function test(num){
console.log(num)
}
test();
let num = test(4);
console.log(num);
复制代码
这时就能够选择把变量声明在函数内部,并采用闭包的机制。
这样既能保证变量的正常调用,又能够避免全局变量的污染。
function test(){
let num = 1;
return function(){
return num
}
}
let fn = test();
console.log(fn());
复制代码
4.2 缺点
4.2.1 内存消耗和内存泄漏
外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,因此内存消耗很大。
解决方法:不滥用闭包。
同时闭包中引用的内部变量会被保存,得不到释放,从而也形成了内存泄漏的问题。
解决方法:
window.onload = function(){
var userInfor = document.getElementById('user');
var id = userInfor.id;
oDiv.onclick = function(){
alert(id);
}
userInfor = null;
}
复制代码
在内部闭包使用变量 userInfor 以前,先用一个其余的变量id 来承接一下,而且使用完变量 userInfor 后手动为它赋值为 null。