首页 > 其他分享 >一次搞懂作用域和闭包

一次搞懂作用域和闭包

时间:2024-06-12 20:29:36浏览次数:20  
标签:闭包 function 函数 作用域 词法 模块 搞懂

前言

对于那些有一点 JavaScript 使用经验但从未真正理解闭包概念的人来说,理解闭包可以看作是某种意义上的重生,但需要付出非常多的努力和牺牲才能理解这个概念。
闭包并不是一个需要学习新的语法或模式才能使用的工具,它也不是一件必须接受像 Luke 一样的原力训练才能使用和掌握的武器。理解闭包就好像 Neo 第一次见到矩阵一样。

希望这篇文章可以像 Morpheus 一样,引导你去发现闭包这个神秘的矩阵。那么你选择蓝色药丸还是红色药丸?

开胃小菜

闭包其实就是作用域的产物,所以先了解一下作用域相关的概念。

JS 的编译

  • 传统编译语言的编译步骤:词法分析、语法分析、代码生成。
  • JS 是一种解释型语言,代码片段在执行前(几微秒)进行编译。
  • 参与 JS 编译的角色:
    • 引擎:负责整个 JS 的编译和执行过程
    • 编译器:负责语法分析和代码生成
    • 作用域:相当于一个容器,负责收集并维护所有标识符(变量、函数),确定代码对标识符的访问权限

词法作用域

  • 作用域的模型有词法作用域和动态作用域,词法作用域就是定义在词法阶段的作用域。JS 采用的是词法作用域模型,编译的词法分析阶段会确定代码中全部的标识符在哪个作用域以及是如何声明的,从而预测执行时应该如何查找。
  • 在 JS 中,除全局作用域外,函数声明以及代码块也会创建一个新的作用域,对应的词法作用域是由书写代码时函数声明或代码块的位置来决定的。作用域内声明的所有变量都会附属于这个作用域。
  • 作用域发生嵌套时,查找会从运行时所处的作用域开始,逐级向上进行,直到查到第一个匹配的标识符为止。
function Matrix() {
  const name = 'Neo';
  {
    const name = 'Morpheus';
    console.log(name);
  }
  console.log(name);
}

Matrix();
// 'Morpheus'
// 'Neo'

函数作用域

  • 函数声明把变量和函数包裹起来,变成属于自己作用域的私有变量或函数,这遵循了软件设计中的最小授权(最小暴露)原则,可以规避因命名冲突导致的变量值被覆盖。
  • 函数作用域的含义是,属于这个函数的全部变量都可以在整个函数的范围内(包括内部嵌套的作用域)使用。

块作用域

  • ES6 中新增的块作用域可以将代码在块中隐藏,是对最小授权原则的扩展。
  • 使用 var 在块作用域中声明变量,会被提升到外部作用域,并不能把变量隐藏在块作用域中,使用 es6 的 const/let 进行声明可以将变量绑定到所在的作用域中,不会被提升。
  • 常见的块作用域:
    • for 循环
    • if 语句
    • with
    • try/catch 中的 catch
    • {…}

鸡汤来喽

闭包是什么

闭包是基于词法作用域书写代码时所产生的自然结果。当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

MDN 对 闭包 的定义是:

闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。

function f1() {
  var a = 1;

  function f2() {
    console.log(a);
  }

  return f2;
}

var f = f1();
f(); // 1

上述代码中,f2 可以访问 f1 的内部作用域,f1() 返回内部的 f2 函数,然后赋值给变量 f 并调用,实际上是 f2 在定义时的词法作用域外被调用。

f1 执行后,通常情况下它的整个内部作用域会被引擎的垃圾回收机制销毁并释放内存。而在这段代码中,f2 作为 f1() 的返回值,f1 的内部作用域一直被 f2 使用,所以 f1 的作用域不会被回收,以供 f2 在之后的任意时间进行引用。

f2 始终保持对 f1 作用域的引用,这个引用和 f2 本身就组成了闭包。

总结一下:

  • 无论通过何种方式将内部函数传递到所在的词法作用域以外,这个函数都会保持对原始作用域的引用,这样就形成了闭包。
  • 无论在何处执行这个函数都会使用闭包,闭包使得函数可以继续访问定义时的词法作用域。

所以,闭包 = 函数 + 外部作用域

我们的代码中其实到处都是闭包,只是我们没有发现。

函数柯里化

函数柯里化(curry)是函数式编程里面的概念。函数柯里化后,每次调用时只接受一部分参数,并返回一个函数,直到传递所有参数为止。返回的函数保持对调用函数作用域的引用,就形成了闭包。

// 柯里化之前
function add(a, b) {
  return a + b;
}
add(1, 2); // 3

// 柯里化之后
function Add (a) {
  return function (b) {
    return a + b;
  }
}
Add(1)(2); // 3

回调函数

回调函数保持对外部作用域的引用,就形成了闭包。

function wait(msg) {
  setTimeout(function () {
    console.log(msg);
  }, 1000);
}
wait('May the force be with you.');
  • 定时器
  • 事件监听器
  • 数组常用方法 forEach、map…
  • promise
  • Ajax 请求

模块

模块是一个公共函数调用后返回内部私有函数和变量引用的一种代码模式。

function Matrix() {
  var name1 = 'Neo';
  var name2 = 'Trinity';

  function Neo() {
    console.log(name1);
  }

  function Trinity() {
    console.log(name2);
  }

  return {
    Neo,
    Trinity,
  };
}

var m = Matrix();

m.Neo(); // 'Neo'
m.Trinity(); // 'Trinity'

模块模式需要具备两个必要条件:

  • 必须有外部的包装函数,该函数必须至少被调用一次,每次调用都会创建一个新的模块实例。
  • 包装函数必须返回至少一个内部函数,这样就会创建涵盖整个包装函数内部作用域的闭包。返回的内部函数就是模块的 API。

ES6 之前的模块使用,以 jQuery 为例,我们使用 script 标签引入 jQuery 模块后,就可以直接使用模块中暴露的 jQuery $ 等标识符。

除此之外,还可以依赖于模块加载器,比如基于 AMD (异步模块定义)实现的 RequireJS,提供了 require define 方法用于引入模块和定义模块,定义模块的核心概念是这样的:

var Modules = (function () {
  var modules = {};

  function define(name, deps, impl) {
    for (var i = 0; i < deps.length; i++) {
      deps[i] = modules[deps[i]];
	}
    modules[name] = impl.apply(impl, deps);
  }

  function get(name) {
    return modules[name];
  }

  return {
    define,
    get,
  };
})();

这里的包装函数是一个立即执行函数,执行后返回了包装函数内部定义的 define get 函数,这两个函数一直引用着包装函数的内部作用域,这样就产生了闭包。

ES6 为模块增加了语法支持,模块必须在独立的文件中定义,即一个文件一个模块,ES6 会将文件当作独立的模块来处理。每个模块可以导入其他模块的 API,也可以导出自己的 API。

import Call from 'call.js';

const name = 'Mr.Anderson';

function Smith() {
  Call(name);
}

export Smith;

可以把整个文件看作是一个包装函数,导出的 Smith 函数一直引用着包装函数的内部作用域,形成闭包。

总结一下:

  • 模块的 API 和包装函数的作用域共同组成闭包。
  • ES6 之前的模块是基于函数的模块,通过 RequireJS 这类模块加载器可以实现模块的异步加载。
  • ES6 模块是基于文件的模块,因为是语法层面的支持,浏览器默认的模块加载器就可以异步加载模块文件。

饭后甜点

  • 下面这段代码的执行结果是什么?
function createFunctions() {
  var result = new Array();
  for (var i = 0; i < 10; i++) {
    result[i] = function() {
      return i;
    };
  }
  return result;
}

var funcs = createFunctions();

for (var i = 0; i < funcs.length; i++) {
  console.log(funcs[i]());
}
  • 创建一个函数就会形成闭包吗?

以上。

标签:闭包,function,函数,作用域,词法,模块,搞懂
From: https://blog.csdn.net/fehub/article/details/139454049

相关文章

  • 一文搞懂雷达脉冲压缩和匹配滤波器
    目录1.前言2.脉冲压缩原理3.匹配滤波器4.频域相乘法5.举例微信公众号获取更多FPGA相关源码:1.前言为了解决传统单频脉冲雷达面临的作用距离和空间分辨力之间的矛盾,脉冲压缩理论被提出。在接收端设计一个和发射信号能够“共轭匹配”的网络来实现脉冲压缩。接收到的回......
  • [转]一文彻底搞懂ssh的端口转发
    原文地址:一文彻底搞懂ssh的端口转发_ssh端口转发-CSDN博客背景端口转发是突破网络域隔离的一个手段。在学习这个知识的时候需要不断自问为什么需要端口转发?应用场景是什么呢?什么是端口转发?SSH隧道或SSH端口转发可以用来在客户端和服务器之间建立一个加密的SSH连接如下图,通......
  • 【PL理论】(20) 函数式语言:定义函数 Func = Var × E × Env | 闭包 (Closure) | 定义
    ......
  • vue3 dom ref 实现,子组件ref实现,defineExpose暴露子组件作用域
    示例代码App.vue<template><header><imgalt="Vuelogo"class="logo"src="@/assets/logo.svg"width="125"height="125"/><divclass="wrapper"><HelloWorld......
  • 一文搞懂什么是OTA(空中升级)
    一、概述OTA(Over-The-Air,空中升级)是一种通过无线通信技术实现远程更新设备固件或软件的方法。这项技术广泛应用于现代物联网(IoT)设备、智能手机、汽车、嵌入式系统等领域,提供了一种无需物理连接的便捷更新方式。OTA更新的核心在于使设备能够自动、可靠、安全地从远程服务器获取......
  • Go变量作用域精讲及代码实战
    关注作者,复旦AI博士,分享AI领域与云服务领域全维度开发技术。拥有10+年互联网服务架构、AI产品研发经验、团队管理经验,同济本复旦硕博,复旦机器人智能实验室成员,国家级大学生赛事评审专家,发表多篇SCI核心期刊学术论文,阿里云认证的资深架构师,项目管理专业人士,上亿营收AI产品研发负责......
  • Jmeter元件执行顺序和作用域
    执行顺序配置元件前置处理器定时器取样器后置处理器断言监听器注意:   1.前置、后置处理器和断言等元件对取样器作用,如果在他们的作用域内没有任何取样器,则不会执行。   2.如果在同一作用域范围内有多个同一类型的元件,则这些元件按照他们在测试计划中的上下顺序......
  • Go 语言中的闭包和递归【GO 基础】
    〇、什么是闭包和递归什么是闭包?闭包就是每次调用外层函数时,临时创建的函数作用域对象。因为内层函数作用域链中包含外层函数的作用域对象,且内层函数被引用,导致内层函数不会被释放,同时它又保持着对父级作用域的引用,这个时候就形成了闭包。所以闭包通常是在函数嵌套中形成的。/......
  • 闭包
    defhello(username):username='大西瓜'defworld(status):returnusername+statusreturnworlddefnihao(username):username='小西瓜'defworld(status):returnusername+statusreturnworld#world1=hello('张三')#world2=hello(......
  • 2024-06-06 闭包、常用函、类和实例
    一、闭包1.定义闭包是一个函数内部定义的内部函数,且可以访问外部函数的变量。常用与数据隐藏和信息封装。defhello():username='小小奇'defvoi()://内部函数变量returnusernamereturnvoi2.数据隐藏将变量封装在内部函数......