嘛是闭包?听俺唠唠

41 阅读5分钟

MDN如是说:

闭包是由捆绑起来(封闭的)的函数和函数周围状态(词法环境)的引用组合而成。换言之,闭包让函数能访问它的外部作用域。在 JavaScript 中,闭包会随着函数的创建而同时创建。

作用域

ES6前 在聊到闭包之前,先来唠下作用域,在es6之前,JavaScript变量只有两种类型的作用域函数作用域全局作用域,使用var声明的变量要么属于函数作用域要么是全局作用域,那么带来的问题就是没块级作用域那么变量提升有时就会让代码效果不及预期

if(Math.random() > 0.5){
    var a = 1;
} else {
    var a = 2;
}
console.log('a is ', a)

理论上来讲,上面的的console.log的调用会抛出一个异常才更加符合逻辑,但是因为var声明的变量不会生成块级作用域,那么上面的的var语句实际创建的是一个全局变量,所以最后的输出结果为"a is 2".

在ES6之后,新增了letconst关键字,也有了暂时性死区块级作用域等概念的出现.

if(Math.random() > 0.5){
    const a = 1;
} else {
    const a = 2;
}
console.log('a is ', a);  // ReferenceError: a is not defined

从本质上说,在ES6中仅当使用letconst声明变量时,块才会认为是作用域.

闭包

用大白话说内部函数引用了外部函数的变量并且会一直保持着对这些变量的引用,官方一点的就是说 闭包是由函数以及函数声明所在的词法环境组合而成的.该环境包含了这个闭包创建时作用域内的任何局部变量.

function addFn(x){
    return function(y){
        return x + y;
    }
}
const add5 = addFn(5);
console.log(add5(10));  // 15

其中返回的匿名函数就是就是闭包中提到的函数,而函数内引用的x变量所在的作用域就是上面提到的词法环境.

function addFn(x){
    return function(y){
        return x + y;
    }
}
const add5 = addFn(5);
const add7 = addFn(7);
console.log(add5(10));  // 15
console.log(add7(10));  // 17

在这个例子中,add5add7都创建了闭包,它们共享相同的函数体定义,但是保存了不同的词法环境,在add5词法环境中x为5,而在add7词法环境中x为7.(addFn函数每调用一次都会创建一个新的函数作用域)

闭包的一些使用方法

私有化方法,用闭包去实现私有方法

function makeCounter(){
    let counter = 0;
    return {
        get(){
            return counter;
        },
        set(val){
            counter = val;
        }
    }
}
const myCounter = makeCounter()
console.log(myCounter.get()); // 0
myCounter.set(1);
console.log(myCounter.get()); // 1

每个闭包都有自己的词法环境,而上面创建了一个由两个函数共享的词法环境: myCounter.get和 myCounter.set.

闭包也可以用在模块作用域上

// myModule.js
let x = 5;
export const getX = () => x;
export const setX = (val) => {
  x = val;
};
import { getX, setX } from "./myModule.js";

console.log(getX()); // 5
setX(6);
console.log(getX()); // 6

在循环中创建闭包的常见错误

function tempFn(){
    const list = ['1', '2', '3'];
    const result = [];
    for (var i = 0; i < list.length; i++) {
        var item = list[i];
        result.push(function () {
            console.log(item);
        })   
    }
    return result;
}
const list = tempFn();
list[0]();  // 3

这里虽然是用到了闭包但是结果却不尽人意,主要在于用到了var这个关键字去定义变量,var定义个变量只有函数作用域全局作用域两种,此时变量item明显是函数作用域并且在迭代时一直被赋新值,这相当于是通过for循环创建了多个函数而它们共享词法环境,也就是说当使用到item时,所有的函数里都是同一个item的值,上面的案例最后赋值后是3.

这明显不是我们想要的,那么此时可以使用一个立即执行函数来解决

function tempFn(){
    const list = ['1', '2', '3'];
    const result = [];
    for (var i = 0; i < list.length; i++) {
        (function (){
            var item = list[i];
            result.push(function () {
                console.log(item);
            })  
        })()
      
    }
    return result;
}
const list = tempFn();
list[0]();  // 1

此时虽然也是用var声明的item,但结果完全不一样了,因为此时item的作用域虽然还是函数作用域但却是这个匿名函数的并不是tempFn函数的,所以通过for循环生成的多个函数中虽也是共用了一个词法环境但它们内部item却不是共用的一个,所以此时结果是正确的.

当然了说了那么多还是因为var关键字作用域的问题,而到了ES6之后,嘿嘿,就不用这么麻烦了

function tempFn(){
    const list = ['1', '2', '3'];
    const result = [];
    for (var i = 0; i < list.length; i++) {
        let item = list[i];
        result.push(function () {
            console.log(item);
        })   
    }
    return result;
}
const list = tempFn();
list[0]();  // 1

只需要把var改成let或者const就可以了,因为这两个关键字使用时会触发块级作用域,所以最终结果也是对的.

当然了你也可以用forEach来解决

function tempFn(){
    const list = ['1', '2', '3'];
    const result = [];
    list.forEach(item => {
        result.push( function () {
            console.log(item);
        })
    });
    return result;
}
const list = tempFn();
list[0]();  // 1

注意事项

谈到闭包当然绕不过内存泄露,这是因为创建闭包时,函数会一直占用着变量导致释放不了,这个跟js的垃圾回收机制有关,也叫GC.

目前用的是标记清除法(更早之前是引用计数法),简单来说就是因为你创建的闭包引用着外部的变量,此时这些变量就被标记了,而在GC时会去清除那些没有被标记的对象,所以这些变量会常驻内存得不到不释放.

解决方法就是用完之后记得把闭包置空

function clouser(){
    let a = 1;
    return function(){
        consloe.log(a);
    }
}

const temp = clouser();
temp();
temp = null;