JavaScript闭包全解(含常考笔面题)

348 阅读3分钟

文章的行文思路都是按照博主个人对相关知识的理解做编写,综合了权威书籍和诸多贴文的解释,最后编写为方便自己个人理解的版本。

同时附带校招过程中遇到的相关试题

闭包

谈谈JavaScript的闭包 (juejin.cn)

  • 概念:

    • 函数对其周围状态(即词法环境)的引用捆绑再一起构成闭包 (MDN解释)
    • 也就是闭包=函数+词法环境的捆绑
    • 其实在 JavaScript 中,每当函数被创建,就会在函数生成时生成闭包。
  • 作用:

    • 让内部函数访问外部函数的作用域
  • 常见使用场景:

    • 模拟私有变量

    • bind,函数柯里化的实现原理 (实际上就是,在外部函数中将某个变量锁定为某个值 )

    • 在定时器、事件监听器、 Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!

  • 使用方法

    1. 在外部函数中定义并返回内部函数

    2. 内部函数引用了外部函数的变量

    3. 在其他地方调用返回函数

    4. 形如:

      function outer(){
      	var n=0;
      	return function inner(){
      		console.log(n);
      	}
      }
      var method = outer();
      method();
      // 由于闭包让函数保持对周围状态(函数编写时的词法作用域)的引用,所以依旧能够调用外部函数中的变量n.(n值锁定为0)
      
  • 缺点:

    1. 内存泄露(内部函数保留对外部函数的引用,导致其不能回收,外部函数的活动对象依旧在内存中存在。)
    2. 闭包在处理速度和内存消耗方面对脚本性能具有负面影响

内存泄露

定义:已分配的内存由于某些原因,无法进行垃圾回收来释放内存,从而导致内存浪费。

  • 常见的内存泄露:

    1. 意外的全局变量 (绑定到window对象上,无法释放)
    2. 被遗忘的定时器和事件处理程序(回调函数)
    3. 闭包 (保持引用,无法释放)
    4. DOM引用
  • 垃圾回收的方法:

    1. 标记清除
    2. 引用计数

笔试题

//CVTE笔试
function f(n,o) {
            console.log(n+o);
            return {
                f:function(m) {
                    return f(m,n)
                }
            };
        }
        f(1).f(2).f(3).f(4); // NaN 3 5 7

//佳都笔试
function cb(m, n) {
            console.log(n)
            return {
                cb: function (x) {
                    return cb(x, m);
                }
            };
        }
        let res = cb(0);// undefined 第一个输出的n未指定参数,为undefined
        res.cb(1);//0 后面都是因为闭包利用外层函数的m参数,所以是cb(x,0); 执行cb函数的console,但是没有接收返回值,所以res不变,后面的执行也是cb(x,0);
        res.cb(2);//0
        res.cb(3);//0
        cb(0).cb(2).cb(3).cb(2)
        // undefined 0 2 3

面试题

输出1-5(间隔一秒)

经典面试题,但是网上没有一种很好的解答方案。

题目:如何解释? 输出 5 5 5 5 5
for(var i =0;i<5;i++){
	setTimeout(function(){
		console.log(i)
	},0)
}
  • 涉及知识点:

    • Event Loop事件循环
    • 变量提升
    • 作用域问题
  • 出现这种现象的根本原因:

    • 没有把每次for的i值保存下来,导致setTimeout的回调没有自己独立的i值。
  • 题解:

    • 根据Event Loop原理分析得,setTimeout将挂起到事件队列,完成同步代码执行后再执行setTimeout

    • 由于var 声明只存在于函数作用域和全局作用域,没有块级作用域,其将做变量声明提升到作用域顶部。

      即整个作用域中将共用一个i变量,当然for循环内部保持的i 在内存中都指向同一个位置,就是同一个i值。

    • 同步代码执行完后,i变为5,setTimeout全部调用这个i值。

      // 1.利用立即执行函数,创建一个函数作用域,将每次for的i值保存到IIFE的函数作用域,执行异步代码时依旧能使用到i的引用。
        for (var i = 0; i < 5; i++) {
            (function(i){
                setTimeout(function () {
                    console.log(i);
                },1000*i);
            })(i)
        }

      //2.   let 自带的块级作用域
        for (let i = 0; i < 5; i++) {
                setTimeout(function () {
                    console.log(i);
                },1000*i);

        }

             

函数柯里化

/*
	必须根据面试官所给题目的具体情况书写,其实不难,根据所给条件做函数的输出即可,
	其他的就做args数组,依次把数加进去数组即可。
	函数柯里化,闭包的典型应用
	内层函数保持对外层函数的持续引用,在外层做一个数组args,保存全部传入的参数 
	内层函数若有argument===0 就直接返回相加结果,否则返回内部函数用于下一次的调用同时将参数压入args数组

 */

        //1. 让指定函数套用柯里化的版本,需要实现知道长度做柯里化的函数
        function add(a, b, c) {
            return a + b + c;
        }
        function curry(fn,args){
            var len = fn.length;
            var args= args||[];
            return function(){
                var newArgs = args.concat(Array.prototype.slice.call(arguments));
                if(newArgs.length<len){
                    return curry.call(this,fn,newArgs)
                }else{
                    return fn.apply(this,newArgs);
                }
            }
        }
        var f2 =curry(add);
        console.log(f2(1,2)(3))
        console.log(f2(1)(2,3))
        console.log(f2(1,2,3))
      
           // ES6 版本
            const curry = (fn) =>
                (judge = (...args) =>
                    args.length === fn.length
                    ? fn(...args)
                    : (...arg) => judge(...args, ...arg));
                const add = (a, b, c) => a + b + c;
                const curryAdd = curry(add);
                console.log(curryAdd(1)(2)(3)); // 6
                console.log(curryAdd(1, 2)(3)); // 6
                console.log(curryAdd(1)(2, 3)); // 6
     


        //2. 在toString中做和函数,返回函数本身的打印结果则为相加的结果。没有指定的终止条件
        function sum() {
            let args = [...arguments];

            let fn = function(){
                    args.push(...arguments);
                    return fn; //这样返回,原本会输出这个函数的代码,但是由于改变了toString方法,因此输出为和
            }

            fn.toString = function(){
                return args.reduce((a,b)=>a+b);
            }
            return fn;
        }
        console.log(sum(1)(2)(3))

        //3. 依旧是返回函数本身,此次的终止条件为(),即所传参数为空时返回相加结果
        function plus() {
            let args = [...arguments];
            let cb = function () {
                if (arguments.length === 0) {
                    return args.reduce((a, b) => a + b);
                } else {
                    args.push(...arguments)
                    return cb;
                }
            }
            return cb;
        }

        console.log(plus(1)(2)(3)())//此次的终止条件为(),即所传参数为空时返回相加结果