JavaScript闭包的产生原理及运行机制

265 阅读8分钟

JavaScript闭包

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。--MDN

JS闭包是什么

一个概念的产生总要有其存在的意义,闭包从字面上来看是一个封闭的包或者说是一个封闭的环境环境产生的意义在于为环境内部的事物提供生存空间以及保护环境内的事物不受外界的影响,就像地球是一个闭包,中国也是一个闭包。

在JavaScript中函数就是一个符合闭包的概念,但是并不是只有函数才是闭包

function closure(){
    var a = 1;
    var b = 2;
    function add(a,b){ return a + b };
}
closure();

函数在入栈执行之前就已经形成了执行上下文,将内部的变量存储在variable object上,同时上下文的形成也保护了内部变量不被外界所影响,所以我们可以说这是一个闭包

如何访问闭包内的值

闭包的特性决定了,外界不能够直接访问闭包内部的值,但是闭包的主要作用之一就是保存变量,如果保存的变量不能使用,那么闭包的概念就会显得有些累赘,所以闭包也是提供了一些能够获取到内部值的方式

1. 外部引用+返回值

当我们函数执行时,函数的私有执行上下文与全局之间没有依赖关系的话,在执行完成之后会直接pop出栈的,但是在全局环境下私有上下文被引用的话,该函数的私有上下文是不会被pop出栈的,同时还能通过变量来接收函数的返回值

function closure(){
    var a = 1;
    var b = 2;
    function add(a,b){ return a + b };
    return {	// 通过返回一个对象,将私有上下文中的变量暴露
        a,b,add
    }
}
var clo = closure();	// 通过变量接收,函数执行返回的对象,进而操作函数的私有属性
console.log(clo.a);		// 1
console.log(clo.b);		// 2
console.log(clo.add(clo.a,clo.b));	// 3

2. window全局暴露

function closure(){
    var a = 1;
    var b = 2;
    function add(a,b){ return a + b };
    window.a = a;	// 上下文通过这种方式建立依赖,也是不会被ECStack释放
    window.clo = {
        a,b,add
    };
}
closure()
console.log(a);			// 1
console.log(clo.b);		// 2
console.log(clo.add(a,clo.b));	// 3

直接将私有属性暴露在window这种方式在很多类库的源码中都能看到,就比如说jquery、lodash...

function closure(){
    var jquery = function(){
        // jquery内部逻辑
    }
    window.jquery = window.$ = jquery;
}
$();

3.内部函数通过作用域链(scope-chain)访问

函数在创建的时候会将代码字符串存至堆内存中,同时还会在标记该函数的创建环境(函数声明时所处的执行上下文),在形成函数私有执行上下文的时候,会初始化上下文的作用域链(scope-chain),将当前上下文于标记的上级上下文相连

function closure(a){	// 函数执行返回输入加2
    var b = 2;
    function add(){
        return a + b;
    }
    return add();
}
console.log(closure(1))	// 3

闭包的高级应用

高级单例模式

高级单例模式,可以说是最早的模块化思想,目的是通过闭包的保护机制和对象的分组机制,实现闭包中私有方法和属性的调用

let AModule = (function(){
    let name = "我的幸运7"function log1(){console.log(1)};
    function log2(){console.log(2)}
    
    //1. window.xxx = xxx  缺点:当方法暴露过多时,还是会引发全局变量污染 
    //2. 基于对象分组的特性,把需要暴露的API,都放置在同一堆内存空间下。
    
    return {
        name,
        log1,
        log2,
        init(){
            //控制业务逻辑的执行顺序的“控制命令”
            log1();
            log2();
            console.log(name)
        }
    };
})();

Amodule.log1()     // 1
Amodule.log2()     // 2
Amodule.init()     // 1  2  '我的幸运7'

像vue组件中的data,用的也是高级单例模式创建的,目的是保证组件复用时创建不同的data对象

//...
export default {
	// ...
	data: function(){
		return {	// 通过高级单例模式,每次函数执行创建的对象都是不同的
			name: '我的幸运7'
		}
	}
}

惰性函数

能执行一次,就绝不执行第二次的函数

// 例如当我要获取css属性时,为了同时兼容IE6~8,需要做环境判断,但是问题是,像这种环境判断的操作,没有必要每次执行函数时都做一次if判断,只要第一次执行时判断,以后执行函数都不用判断了,这种情况就可以使用惰性函数了

// 方案一:基于if每次判断
let isCompatible = 'getComputStyle' in window;
function getCss(element,attr){
    if(isCompatible){
        return window.getComputedStyle(element)[attr];
    }
    //IE6~8
    return element.currentStyle[attr];
}

// 方案二:基于惰性函数
function getCss(element,attr){
    if(window.getComputedStlye){
        //将全局的getCss重构
        getCss = function(element,attr){
            return window.getComputedStyle(element)[attr];
        }
    }else{
        getCss = function(element,attr){
            return element.currentStyle[attr];
        }
    }
    //将会重构后的函数执行,确保第一次执行能够获得结果
    return getCss(element,attr);
}

柯里化函数

柯里化函数使用的是预处理思想,应用的也是闭包的机制。在第一次执行大函数,形成一个闭包,把一些信息存储在闭包中(传递的实参或者当前闭包中的声明的一些私有变量等信息),等到后面需要执行内部的匿名函数,在遇到非自己私有变量时,则向上级上下文中查找(也就是把之前存储在闭包中的信息获取)

// 例题
let res = fn(1,2)(3);
console.log(res);  //=> 6   1+2+3
function fn(...outer){
    return function(...inner){
        return [...outer,...inner].reduce((total,item)=>(total + item));
    }
}

柯里化函数的亮点在于函数执行后再返回函数,这样第一次执行传入的值就被保存在闭包中了

compose组合函数

一道经典面试题

/* 
    在函数式编程当中有一个很重要的概念就是函数组合, 实际上就是把处理数据的函数像管道一样连接起来, 然后让数据穿过管道得到最终的结果。 例如:
    const add1 = (x) => x + 1;
    const mul3 = (x) => x * 3;
    const div2 = (x) => x / 2;
    div2(mul3(add1(add1(0)))); //=>3

    而这样的写法可读性明显太差了,我们可以构建一个compose函数,它接受任意多个函数作为参数(这些函数都只接受一个参数),然后compose返回的也是一个函数,达到以下的效果:
    const operate = compose(div2, mul3, add1, add1)
    operate(0) //=>相当于div2(mul3(add1(add1(0)))) 
    operate(2) //=>相当于div2(mul3(add1(add1(2))))

    简而言之:compose可以把类似于f(g(h(x)))这种写法简化成compose(f, g, h)(x),请你完成 compose函数的编写 
*/

借用柯里化函数的思想,先将函数集合以数组的形式存储在外层函数的执行上下文中,再在内层函数中接收参数,并在内层函数中循环执行需要操作的函数。

function compose(...args){
    return function(val){
        if(args.length === 0) return val;
        if(args.length === 1) return args[0](val);
        return args.reduceRight((N,item)=>item(N));
    }
}

redux源码中compose的实现方式

function compose(...funcs){
    if(funcs.lenght === 0){
        return arg => arg
    }
    if(funcs.lenght === 1){
        return funcs[0]
    }
    return funcs.reduce((a,b)=>(...args)=>a(b(...args)))
}

函数防抖

防止用户在短时间内多次触发,导致函数执行多次,函数防抖是指短时间内多次触发同一事件,只执行最后一次,或者只执行最开始的一次,中间的不执行。

function debounce(func,wait = 300,immediate = false){
    let timer = null;
    return function(...params){
        let new = immediate && !timer;
        clearTimeout(timer);            //触发函数清除定时器
        timer = setTimeout(()=>{        //重新设置一个定时器,监听wait时间内是否发生二次触发
            timer = null;       //手动回归到初始状态
            !new? func.call(this,...params):null;
        },wait)
        
        //immediate=true 执行第一次触发时返回的结果
        new ? func.call(this,...params):null;
    }
}

函数节流

函数节流可以想象成函数的冷却时间,指单位时间内只能执行一次,超过冷却时间才可以再一次执行。

function throttle(func,wait = 300){
    let timer = null,
        previous = 0;       //记录上一次的操作时间
    return function(...params){
        let now = new Date(),
            remaining = wait - (now - previous);     //记录还差多少时间触发函数
        if(remaining <= 0){     //两次操作时间间隔超过wait
            window.clearTimeout(timer);
            timer = null;
            previous = new;
            func.call(this,...params);
        }else if(!timer){       //!timer设置防止重复设置定时器
            //两次操作时间间隔未超过wait,设置定时器,并传入剩余时间remaining
            timer = setTimeout(()=>{
                timer = null;
                previous = new Date();
                func.call(this,...params);
            },remaining);
        }
    }
}

闭包的争议

闭包的概念在前端一直是一个饱受争议的话题,不同人对于闭包的概念有不同的理解,总结下来大概分为两种学派。一种认为闭包是在函数入栈执行之前所形成的不被外界影响的用于保护私有变量的私有执行上下文,这种理解更加偏向于计算机学科的词法闭包。而另一种人认为由于JS的特性,以及垃圾回收机制导致未被引用的上下文会在执行之后,立即被pop出栈释放,所以他们认为只有没有被垃圾回收的用于保护和保存变量的私有执行上下文才是闭包。我不评价那种思想是对的,但我想说的是技术是在争议中才会活力