闭包就是外层函数将内层函数返回出去,并且内层函数执行时带着外层函数的作用域,可以使用外层函数内部的变量,这些变量始终保存在内存中
本质:闭包相当于桥梁,连接函数内核函数外。
特点:保存函数的诞生环境
使用原因:函数外想要获取函数内部的变量,通过闭包形式
注意事项:闭包会将作用域保存在内存中,不用时需要将变量设置为null,防止内存泄漏。
函数中返回一个函数
// 返回值:直接在函数中返回外层函数的变量
function fn() {
var age = 18;
return function () {
return age;
}
}
var func = fn();
console.log(func()); // 18
将内部函数赋值给外部变量
// 函数赋值
var fn2;
function fn() {
var age = 18;
fn2 = function () {
console.log(age);
}
};
fn2(); // 18
将内层函数当做参数传入全局定义的函数中
function fnc(fn) {
age = fn();
console.log("已经"+age);
}
function fn() {
var age = 18;
fn2 = function () {
return age;
}
fnc(fn2);
};
fn(); // 已经18
使用自执行函数,省去调用过程
上文自执行函数介绍
function fnc(fn) {
age = fn();
console.log("已经"+age);
}
(function fn() {
var age = 18;
fn2 = function () {
return age;
}
fnc(fn2);
})();
循环赋值问题:查看代码
封装私有方法:查看代码
计数器和迭代器:查看代码
实现缓存机制:查看代码
计数器
通过闭包制作计数器
作用:读取函数内部的变量,这些变量始终保存在内存中
function fn() {
count = 0;
return function () {
++count;
console.log("这是第"+count+"个函数;");
}
}
var ins = fn();
ins(); // 这是第1个函数;
ins(); // 这是第2个函数;
ins = null; // 防止内存泄漏
类似于计数器
// 传入一个数组,返回一个迭代器
function initIter(iter) {
arr = iter;
count = 0;
return {
next:function () {
if(count < arr.length){
return {
value:arr[count++],
done:false
}
}
return {
value:undefined,
done:true
}
}
}
}
var arr = ["apple","banana","orange"];
var it = initIter(arr);
console.log(it.next()); // {done: false,value: "apple"}
console.log(it.next()); // {done: false,value: "banana"}
console.log(it.next()); // {done: false,value: "orange"}
console.log(it.next()); // {done: true,value: undefined}
通过设置函数属性,来保存信息
function add() {
return add.count++;
}
add.count = 0
console.log(add()); // 0
console.log(add()); // 1
封装私有方法
// 闭包存储私有属性和方法
function Person() {
var age;
var setAge = function (n) {
if(Number(n)){
age = n;
}
return age;
};
var getAge = function () {
return age;
};
return {
setAge:setAge,
getAge:getAge
}
}
// 函数只能使用其提供的两个接口来操作age的值
var p1 = Person();
p1.setAge(18);
console.log(p1.getAge());
p1 = null;
闭包条件:函数嵌套、访问所在的作用域、在所在的作用域外被调用
自执行函数介绍
自执行函数,通过自执行函数,可以省去闭包初始化过程。
同样,自执行函数内部作用域和外部隔开的,不属于全局作用域,而是函数作用域
// 自执行函数 IIFE
// 方法一:作为函数表达式
(function () {
})();
// 方法二:作为匿名函数
(function () {
}());
//注意加分号
例如:封装私有属性和方法
// 闭包存储私有属性和方法
var p1 = (function Person() {
var age;
var setAge = function (n) {
if(Number(n)){
age = n;
}
return age;
};
var getAge = function () {
return age;
};
return {
setAge:setAge,
getAge:getAge
}
})();
p1.setAge(18);
console.log(p1.getAge());
p1 = null;
缓存机制介绍
没有缓存机制时,每次使用都会重复调用函数,影响性能
// 没有缓存机制时
function factorial(n) {
if(n < 0){
throw RangeError("负数没有阶乘");
}
var res = 1;
for(var i = 1;i <= n;i++){
res *= i;
}
return res;
}
console.log(factorial(0));
console.log(factorial(1));
console.log(factorial(2));
添加缓存机制后
function factorial() {
var fac_arr = {}; // 缓存:用来存储计算结果
// 计算阶乘函数
function calculate(n) {
if(n < 0){
throw RangeError("负数没有阶乘");
}
var res = 1;
for(var i = 1;i <= n;i++){
res *= i;
}
return res;
}
return function (n) {
// 判断之前是否计算过,计算过直接取值,没有则计算,存储到缓存中
if(n in fac_arr){
return fac_arr[n];
}else{
var res = calculate(n);
fac_arr[n] = res;
return res;
}
}
}
var fac = factorial()
console.log(fac(0)); // 结果:1 fac_arr值{0: 1}
console.log(fac(10)); // 结果:3628800 fac_arr值{0: 1, 10: 3628800}
console.log(fac(10)); // 结果:3628800 fac_arr值{0: 1, 10: 3628800}
这个阶乘函数可以,更进一步,每计算一次,缓存一次
function factorial() {
var fac_arr = [1,]; // 缓存:记录0-最大阶乘之间所有阶乘
var max_fac = 0; // 记录最大阶乘
// 计算阶乘函数
function calculate(n) {
console.log(fac_arr);
if(n < 0){
return RangeError("负数没有阶乘");
}else if(n <= max_fac){ // 获取的值在该范围内,直接返回
return fac_arr[n];
}else{
for(var i = max_fac;i < n;i++){
fac_arr.push(fac_arr[i] * (i+1));
}
max_fac = n;
}
return fac_arr[n];
}
return calculate
}
var fac = factorial()
console.log(fac(20)); // 将1,2,3.。。20阶乘全部计算存储下来
console.log(fac(10)); // 直接从缓存中获取
console.log(fac(15)); // 直接从缓存中获取
优点:计算一次大的阶乘之后,0-20之间所有阶乘都存储下来。下次想要0-20之间的阶乘直接获取,不用重复计算。
缺点:当阶乘过大时,缓存存储的阶乘太多,消耗容量
循环赋值
当我们在循环中使用闭包时,注意闭包的作用域
- 你好
- 你好
- 你好
- 你好
- 你好
运行上述代码时,会发现无论点击哪个li标签,返回的都是索引5.
原因:
1.onclick点击事件设置后不会立即执行,触发点击事件时才会调用事件
2.在事件中查找i变量,因为js中{}不为独立的作用域,每次循环使用同一个i,所以循环结束后i变量值为5。 即每次循环的i都是同一个i,相当于
// 循环li标签
var i;
for(i = 0;i < lis.length;i++){
lis[i].onclick = function () {
this.innerHTML = i;
console.log(i);
}
}
console.log(i); // 5
使用es6的let创建变量
//点击li标签时,获取li索引
var myList = document.getElementById("myList");
var lis = myList.children; // 获取所有的li标签
// 循环li标签
for(let i = 0;i < lis.length;i++){
lis[i].onclick = function () {
this.innerHTML = i;
console.log(i);
}
}
console.log(i); //Uncaught ReferenceError: i is not defined
let变量会让{}变为独立作用域,即每循环一次,生成一个单独的作用域,每个作用域中都会创建一个i。
查找变量时,每个i的值不同。循环外部也不能访问i变量。
因为闭包可以保存数值,我们可以通过闭包来保存i的值
//点击li标签时,获取li索引
var myList = document.getElementById("myList");
var lis = myList.children; // 获取所有的li标签
// 循环li标签
for(let i = 0;i < lis.length;i++){
lis[i].onclick = function () {
return (function (n) {
lis[i].innerHTML = i;
console.log(n);
})(i);
}
}
在js中,{}不能算作独立作用域,函数内部才能算独立作用域。(除了es6声明let、const等)
所以使用自执行函数,并且将i值传入函数,保存下来。
因为img图片对象,在src获取url后,会下载图片,但是当前函数执行完毕后,变量会被销毁,导致丢失图片数据。
使用闭包解决
function report2() {
var imgs = [];
return function (src) {
var img = new Image();
img.src = src;
imgs.push(img);
return img;
}
}
var rp = report2();
var img2 = rp("../photo.jpg");
App.appendChild(img2);