JavaScript闭包练习题

1,590 阅读3分钟

经典问题:for循环中的定时器

/**
 * 观察以下代码片段,提出修改方案
 */
for(var i = 0; i < 10; i++) {
    setTimeout(function(){
        console.log(i);
    }, 100);
}

解答

/**************方案一********************/
for(let i = 0; i < 10; i++) {
    setTimeout(function(){
        console.log(i);
    }, 100);
}

/**************方案二*********************/
for(var i = 0; i < 10; i++) {
    setTimeout(function(num){
        console.log(num);
    }, 100, i);
}
/**************方案三(耍赖(***************/
for(var i = 0; i < 10; i++) {
    +function(){
        console.log(i);
    }()
}
  • 方案一解析

    通过ES6的let关键字,管理i的作用域,每次观察i时都是观察的定时器启动时i的数值。块级作用域立功!通过块级作用域形成闭包

  • 方案二解析

    通过setTimeout的API解决,每次执行时,都传入参数i,并通过形参形成闭包。熟记API的胜利

  • 方案三解析

    这还解析啥,其实,还是能解析一下的,也就是事件循环机制

/**
 * 观察以下代码片段
 */
for(var i = 0; i < 10; i++) {
    setTimeout(function(){
        console.log(i);
    }, 0);
}

如果你不了解事件循环或者不了解同步异步,那么有可能这里你会认为能够正常输出

首先,这里肯定还是会输出10个10的,毫无疑问。

什么是事件循环:

js是一门单线程的语言,以事件周期为单位执行代码,在事件周期内的代码按照标准顺序执行,脱离事件周期的代码在可执行的事件周期上执行。脱离事件周期的代码被称为异步代码,异步代码又分为宏任务(macro-task)和微任务(micro-task);常见的异步宏任务setTimeout** setInterval **XMLHttpRequest script加载,常见的微任务:Promise process.nextTick

概念就先了解到这里,更多内容请看事件循环

显然这里即便是setTimeout也是一个异步的宏任务,只有在for循环完全执行之后才会进行执行的,可能有的人会问了:setTimeout设置的延时是0啊,显然声明完它就可以执行了,没错,但是对于js来说,只要事件循环没有完成,那么异步代码是无法插队执行的。这些异步代码会放到一个异步执行队列里面,等待合适的实际(即之前的事件循环完成,并满足触发条件)再进行执行。

所以这里,为了打破事件循环机制,改用了IIFE(Immediately-invoked function expression)立即执行函数

闭包与链式调用

function fun(n,o) {
  console.log(o)
  return {
    fun:function(m){
      return fun(m,n);
    }
  };
}
var a = fun(0);  a.fun(1);  a.fun(2);  a.fun(3);//undefined,?,?,?
var b = fun(0).fun(1).fun(2).fun(3);//undefined,?,?,?
var c = fun(0).fun(1);  c.fun(2);  c.fun(3);//undefined,?,?,?
//问:三行a,b,c的输出分别是什么?

题目来自 博客园-小小沧海的【大部分人都会做错的经典JS闭包面试题

解答

这里先观察第一个,第一次调用fun(0) 必然打印undefined

此时a的值为

{
    fun:function(m){
      return fun(m,n);
    }
}

其中m是待传入的参数,n是一开始传入的参数0。注意这个时候n是被闭包捕获的。

a.fun(1) m的值为1n的值为0 fun的行为没有发生改变,打印0,调用结束并返回一个新的对象

a.fun(2)/a.fun(3)同理 答案为//undefined,0,0,0

观察第二个var b = fun(0).fun(1).fun(2).fun(3);

分步骤来看b0=fun(0)打印undefined

b1 = b0.fun(1)打印0

到这里和上一步一样,然后让我们观察一下b1的状态

n值为1o值为0

返回的方法中m为带传入的参数,n值为被此轮闭包捕获的值1

b2 = b1.fun(2) 打印 1

同理

b = b2.fun(3) 打印2

答案为 //undefined,0,1,2

最后一个 更简单了

直接说答案 //undefined,0,1,1

利用闭包控制访问权限

观察以下代码

/**
 * 实现栈结构
 */
function Stack(){
    this.items = [];
    this.push = item=>{
        this.items.push(item);
    }
    this.pop = ()=>this.items.pop();
}
let stack = new Stack();

经过测试发现问题。可以通过stack.items 直接访问和修改栈内的数据,请提出修改方案

解答

思路基于JS-面向对象之封装(下) (这个我之后会发布到掘金上)

function Stack(){
    const items = [];
    this.push = item=>{
        items.push(item); // 这里通过闭包访问item
    }
    this.pop = ()=>items.pop();// 这里通过闭包访问item
}
let stack = new Stack();
stack.items // undefined

简单的乘法器

按照描述实现算法

实现方法mult,可接受1~2个参数

  1. 若参数中不包含回调函数,则返回一个包含mult方法的对象
  2. 若参数中包含回调函数,则执行回调函数
  3. 不需要考虑传入参数类型异常的情况

实现效果

mult(console.log) // 1
let mult4 = mult(4)
mult4(console.log) // 4
mult4.mult(7, console.log)//28
let mult28 = mult4.mult(7)
mult28(console.log) // 28
let mult2800 = mult28.mult(100)
mult2800(console.log) // 2800

multshow = mult(2, console.log) //2
// 计算10的阶乘
mult(1).mult(2).mult(3).mult(4).mult(5, console.log)//120

解答

参考本篇中的闭包与链式调用

let _mult = function(base) {
    return function(param, cb) {
        if('funciton' == typeof param){
            cb = param;
            param = 1;
        }
        if(cb) {
            cb(base * param);
        }else {
            return {
                mult: _mult(base * param)
            }
        }
    }
}
let mult = _mult(1)

对闭包还有不了解的小伙伴可以参考【JavaScript-闭包】