JS闭包之灵魂7问

简介: Q1:慧眼认“包”Q2: 参数为何凭空消失Q3:作用域嵌套惹的祸Q4:迟到的兑现,错误的值Q5:变量的从一而终Q6:公私分明Q7:柯里化


奋斗这种事是需要动机的

一是对贫穷的恐惧

二是对美好生活的向往

前言

无论你是前端老鸟还是即将遭受社会“毒打”的前端萌新,闭包(Closures)都是一个在你获得心仪offer路上的拦路虎。

在前面的几篇文章中,我们从不同的角度来分析了何为闭包等。

文章链接 查看顺序
再谈JS闭包 基础篇
兄台:JS闭包了解一下 进阶篇

然而,纸上得来终觉浅,绝知此事要躬行。So,为了避免成为那种眼高手低的人。这篇文章,通过几个常见的闭包应用场景评估一下,是否真正的懂闭包。

一图胜千言

直接拿来主义了,这是前几篇中,关于闭包的一些简单汇总。

文章概要

  1. Q1:慧眼认“包”
  2. Q2: 参数为何凭空消失
  3. Q3:作用域嵌套惹的祸
  4. Q4:迟到的兑现,错误的值
  5. Q5:变量的从一而终
  6. Q6:公私分明
  7. Q7:柯里化

1:慧眼认“包”

仔细分析下面的各个方法clickHandler/immediatedelayedReload,哪些方法属于闭包。

  1. clickHandler
let countClicks = 0;
button.addEventListener('click', function clickHandler() {
  countClicks++;
});
复制代码
  1. immediate
const result = (function immediate(number) {
  const message = `number is: ${number}`;
  return message;
})(100);
复制代码
  1. delayedReload
setTimeout(function delayedReload() {
  location.reload();
}, 1000);
复制代码

答疑解惑:

代码解析 结果
clickHandler访问了外部作用域内的变量countClicks clickHandler是一个闭包
immediate没有访问任何的外部作用域变量 未形成闭包
delayedReload 访问了全局作用域的全局变量:location delayedReload是一个闭包

Q2: 参数为何凭空消失

(function immediateA(a) {
  return (function immediateB(b) {
    console.log(a); 
  })(1);
})(0);
复制代码

答疑解惑:

代码运行后,控制台打印了 0

immediateA被调用时,参数的值为0,也就是说 a参数的值为 0。

immediateB内嵌在immediateA函数内部,并且访问了其外部作用域(immediateA作用域)的变量a,所以immediateB是一个闭包。在函数调用时, 变量a为0,所以在控制台输出结果也是0。


Q3:作用域嵌套惹的祸

let count = 0;  // ①
(function immediate() {
  if (count === 0) {
    let count = 1;
    console.log(count); // 输出结果?
  }
  console.log(count); // 输出结果?
})();
复制代码

答疑解惑:

代码运行后,控制台依次输出 10

在①行出,通过let count =0声明了变量 count且值为0。

immediate是一个闭包:它访问了位于它外部作用域内的变量count。 在immediate函数作用域内部,count的值一直都是0。

但是,在条件判断语句下形成的块级作用域里面,另一个let count =1的语句,声明了一个局部变量count,该变量重写(覆盖)了外部作用域的count。所以在局部作用域内部,count的值为1,也就是第一个打印结果是1。

而第二个打印结果是0。因为,第二个打印语句的所能访问到的范围是immediate作用域-->全局作用域。 它没有权限访问位于其头上的块级作用域(子作用域)。

这里需要简单强调下:作用域是可以嵌套的,详情请参考再谈JS闭包


Q4:迟到的兑现,错误的值

for (var i = 0; i < 3; i++) {
  setTimeout(function log() {
    console.log(i); // 输出结果?
  }, 1000);
}
复制代码

答疑解惑:

代码运行后,控制台依次输出 333

该代码执行过程,可以分成2个阶段

阶段一

  1. for迭代了3次。每次迭代,都新建函数(log()),而在函数中,都访问了变量isetTimeout()log()函数延迟1000ms触发
  2. for()循序执行完时,变量i已经变成 3

阶段二

该阶段发生在1000ms之后

  1. setTimeout()执行被延后处理的log()函数。而log()访问的变量i已经变成3了。 所以,三次打印都是3

解决方案

这里多说一嘴,知道病因是啥,还需要能够对症下药

ES5解决方案:利用IIFE

IIFE:英文名是Immediately Invoked Function Expression

中文名是立即调用函数表达式,简称为 立即执行函数


---请用小沈阳语言包阅读以上内容

for (var i = 0; i < 3; i++) {
  (function (a){
    setTimeout(function(){
      console.log(a);
    },1000)
  })(i);
}
复制代码

其实,这种处理方式,也是在利用闭包特性。

setTimeout外层又包了一层,此时 IIFE中的参数a的值就是当时i的值。而log中又引用了变量a。 也就是说,通过IIFE,让某个时刻的变量ilog产生了关联。也就是每次打印的时候,都是当时绑定的值。

ES6解决方案: 使用Let声明变量

for (let i = 0; i < 3; i++) {
  setTimeout(function log() {
    console.log(i); 
  }, 1000);
}
复制代码

Q5:变量的从一而终

function createIncrement() {
  let count = 0;
  function increment() { 
    count++;
  }
  let message = `Count is ${count}`;
  function log() {
    console.log(message);
  }
  return [increment, log];
}
const [increment, log] = createIncrement();
increment(); 
increment(); 
increment(); 
log(); // 输出结果
复制代码

答疑解惑:

代码运行后,控制台输出 0

increment()函数被调用了三次,变量count的值更新为3

message位于createIncrement函数作用域内。它的初始值为Count is 0。即使 Count 变量增加了几次,message总是保持初始值。

log()函数是一个闭包,访问了createIncrement函数作用域内的值message。 而message一直没有发生变化。所以,无论increment()被调用任意次,count被增加到任意数值。message的引用count的值,都是其初始值。

Q6:公私分明

构建了一个用于生成栈(stack)数据结构的函数。

function createStack() {
  return {
    items: [],
    push(item) {
      this.items.push(item);
    },
    pop() {
      return this.items.pop();
    }
  };
}
const stack = createStack();
stack.push(10);
stack.push(5);
stack.pop(); // => 5
stack.items; // => [10]
stack.items = [10, 100, 1000]; // 封装性被破坏了
复制代码

对实例进行push/pop处理,都符合stack的操作特性。但是,存在一个小小的瑕疵。如果有人蓄意对items进行直接赋值处理,那么原先的操作都会被抹杀,对于一个成熟的数据结构来讲,这是不允许的。

所以,我们需要将针对items的操作,进行过滤处理。或者说将其对外隐藏。

利用闭包,使得items的属性对外不可见。

function createStack() {
  const items = [];
  return { // ①
    push(item) {
      items.push(item);
    },
    pop() {
      return items.pop();
    }
  };
}
const stack = createStack();
stack.push(10);
stack.push(5);
stack.pop(); // => 5
stack.items; // => undefined
复制代码

变量items被移动createStack函数作用域内部。也就是说items私有变量,而在return对象(①行所在位置)的各个方法(push/pop)是公共变量。

对外开放的方法中,访问了createStack作用域内的变量items,从而形成了闭包。


Q7:柯里化

现在有一个需求,让你写一个用于计算两数乘积的函数multiply

function multiply(num1, num2) {
  // bala bala
}
复制代码

如果multiply(num1, numb2)在调用的时候,有两个满足要求的参数(都是Number类型),那么就返回两数乘积(num1 * numb2

如果调用multiply的时候,只传入了一个参数,那需要返回另外一个函数(const anotherFunc = multiply(num1)),而返回的函数,又可以接受另外一个参数(num2)。被调用时,返回的是num1 * num2的乘积。

需要满足下面的各种情况。

multiply(4, 5); // => 20
multiply(3, 3); // => 9
const double = multiply(2);
double(5);  // => 10
double(11); // => 22
复制代码

话不多说,直接上代码。

function multiply(number1, number2) {
  if (number2 !== undefined) {
    return number1 * number2;
  }
  return function doMultiply(number2) {
    return number1 * number2;
  };
}
复制代码

number2 !== undefined时,说明multiply调用时,传入了两个参数。

number2 == undefined时,说明multiply调用时,只传入了一个参数,此时函数返回了接收另外一个参数的函数doMultiply。 而在返回的函数中,访问了multiply作用域的变量number1,形成了闭包。

也就会出现,虽然multiply(number1)被调用执行后,在返回的doMultiply函数中,依然能够访问到变量number1

--

后记

参考资料:

  1. 7 Interview Questions on JavaScript Closures.
  2. 再谈JS闭包
  3. 兄台:JS闭包了解一下



相关文章
|
21天前
|
自然语言处理 JavaScript 前端开发
JavaScript中闭包:概念、用途与潜在问题
【4月更文挑战第22天】JavaScript中的闭包是函数及其相关词法环境的组合,允许访问外部作用域,常用于数据封装、回调函数和装饰器。然而,不恰当使用可能导致内存泄漏和性能下降。为避免问题,需及时解除引用,减少不必要的闭包,以及优化闭包使用。理解并慎用闭包是关键。
|
21天前
|
JavaScript
闭包(js的问题)
闭包(js的问题)
13 0
|
21天前
|
JavaScript 前端开发
解释JavaScript闭包的工作原理,并举例说明其在游戏开发中的应用。
JavaScript闭包允许内部函数访问并保持对外部函数变量的引用,即使外部函数执行结束。当函数返回内部函数时,形成闭包,继承父函数作用域链。在游戏开发中,闭包用于创建具有独立状态和行为的角色实例。例如,`createCharacter`函数创建角色并返回包含属性和方法的对象,内部函数如`getHealth`、`setHealth`和`attack`通过闭包访问并操作角色的变量。这种方式确保了每个角色的状态在不同实例间独立,是实现游戏逻辑的强大工具。
17 2
|
21天前
|
存储 缓存 JavaScript
|
20天前
|
JavaScript 前端开发
JavaScript 闭包:让你更深入了解函数和作用域
JavaScript 闭包:让你更深入了解函数和作用域
|
20天前
|
自然语言处理 JavaScript 前端开发
JavaScript闭包基础
JavaScript闭包基础
|
21天前
|
缓存 自然语言处理 JavaScript
JavaScript内存泄漏导致应用性能下降,常见于闭包使用不当
【5月更文挑战第14天】JavaScript内存泄漏导致应用性能下降,常见于闭包使用不当。闭包能记住并访问词法作用域,若函数返回后,其引用的对象未被释放,就会引发泄漏。例如,`createLeakyFunction`创建的闭包保留了对大型对象`someLargeObject`的引用,即使函数执行完毕,对象也无法被垃圾回收。避免泄漏的方法包括及时解除引用、清除事件监听器、使用WeakMap和WeakSet以及定期清理缓存。使用性能分析工具可检测和修复内存泄漏问题。
22 3
|
21天前
|
JavaScript 前端开发
JavaScript闭包允许内部函数访问并保留外部函数的变量,即使外部函数执行结束
【5月更文挑战第13天】JavaScript闭包允许内部函数访问并保留外部函数的变量,即使外部函数执行结束。在游戏开发中,闭包常用于创建独立状态的角色实例。例如,`createCharacter`函数生成角色,内部函数(如`getHealth`、`setHealth`)形成闭包,保存角色的属性(如生命值)。这样,每个角色实例都有自己的变量副本,不互相影响,从而实现角色系统的独立性。
23 0
|
21天前
|
自然语言处理 JavaScript 前端开发
深入理解JavaScript中的闭包机制
闭包是JavaScript中一个重要且常被误解的概念。本文将深入探讨闭包的本质、工作原理以及在实际开发中的应用。通过详细解析闭包的定义、作用域链、内存管理等方面,读者将对闭包有更清晰的理解,并能够运用闭包解决实际开发中的问题。
|
21天前
|
前端开发 JavaScript
闭包在JavaScript中有许多应用场景
【5月更文挑战第7天】闭包在JavaScript中发挥关键作用,如封装私有变量和函数提升安全性,维护变量生命周期,实现高阶函数,模拟块级作用域,支持回调函数以处理异步操作,以及促进模块化编程,增强代码组织和管理。闭包是理解和掌握JavaScript高级特性的重要一环。
30 7