当前位置: 首页 > news >正文

你不懂js系列学习笔记-作用域和闭包- 03

第3章:函数与块儿作用域

原文:You-Dont-Know-JS

正如我们在第二章中探索的,作用域由一系列“气泡”组成,这些“气泡”的每一个就像一个容器或篮子,标识符(变量,函数)就在它里面被声明。这些气泡整齐地互相嵌套在一起,而且这种嵌套是在编写时定义的。

创造一个新的气泡方式:

  1. 函数
  2. with: 从对象中创建的作用域仅存在于这个 with 语句的生命周期中,而不在外围作用域中。
  3. try/catch: JavaScript 在 ES3 中明确指出在 try/catchcatch 子句中声明的变量,是属于 catch 块儿的块儿作用域的。

1 函数中的作用域

你可以通过将变量和函数围在一个函数的作用域中来“隐藏”它们:

function foo(a) {
  var b = 2;
  // 一些代码
  function bar() {
    // ...
  }
  // 更多代码
  var c = 3;
}
// foo() 外面访问不到 a,b,c
复制代码

考虑一个函数的传统方式是,你声明一个函数,并在它内部添加代码。但是相反的想法也同样强大和有用:拿你所编写的代码的任意一部分,在它周围包装一个函数声明,这实质上“隐藏”了这段代码。

为什么“隐藏”变量和函数是一种有用的技术?

有多种原因驱使着这种基于作用域的隐藏。它们主要是由一种称为“最低权限原则”的软件设计原则引起的,有时也被称为“最低授权”或“最少曝光”。这个原则规定,在软件设计中,比如一个模块/对象的API,你应当只暴露所需要的最低限度的东西,而“隐藏”其他的一切。

这个原则可以扩展到用哪个作用域来包含变量和函数的选择。如果所有的变量和函数都在全局作用域中,它们将理所当然地对任何嵌套的作用域来说都是可访问的。但这回违背“最少……”原则,因为你(很可能)暴露了许多你本应当保持为私有的变量和函数,而这些代码的恰当用法是不鼓励访问这些变量/函数的。

例如:

function doSomething(a) {
  b = a + doSomethingElse(a * 2);
  console.log(b * 3);
}

function doSomethingElse(a) {
  return a - 1;
}

var b;

doSomething(2); // 15
复制代码

在这个代码段中,变量 b 和函数 doSomethingElse(..) 很可能是 doSomething(..) 如何工作的“私有”细节。允许外围的作用域“访问” bdoSomethingElse(..) 不仅没必要而且可能是“危险的”,因为它们可能会以种种意外的方式,有意或无意地被使用,而这也许违背了 doSomething(..) 假设的前提条件。

修改:

function doSomething(a) {
  function doSomethingElse(a) {
    return a - 1;
  }

  var b;

  b = a + doSomethingElse(a * 2);

  console.log(b * 3);
}

doSomething(2); // 15
复制代码

将变量和函数“隐藏”在一个作用域内部的另一个好处是,避免两个同名但用处不同的标识符之间发生无意的冲突。冲突经常导致值被意外地覆盖。

例如:

function foo() {
  function bar(a) {
    i = 3; // 在外围的for循环的作用域中改变`i`
    console.log(a + i);
  }

  for (var i = 0; i < 10; i++) {
    bar(i * 2); // 噢,无限循环!
  }
}

foo();
复制代码

bar(..) 内部的赋值 i = 3 意外地覆盖了在 foo(..) 的for循环中声明的 i。在这个例子中,这将导致一个无限循环,因为 i 被设定为固定的值 3,而它将永远 < 10

通过将变量和函数围在一个函数的作用域中“可以工作”,但它不一定非常理想。它引入了几个问题。首先是我们不得不声明一个命名函数 foo(),这意味着这个标识符名称 foo 本身就“污染”了外围作用域(在这个例子中是全局)。我们要不得不通过名称(foo())明确地调用这个函数来使被包装的代码真正运行。

解决方法(立即调用函数表达式 IIFE ):

var a = 2;

(function IIFE(global) {

  var a = 3;
  console.log(a); // 3
  console.log(global.a); // 2

})(window);

console.log(a); // 2
复制代码

得益于包装在一个 () 中,我们有了一个作为表达式的函数,我们可以通过在末尾加入另一个 () 来执行这个函数,就像 (function foo(){ .. })()。第一个外围的 ( ) 使这个函数变成表达式,而第二个 () 执行这个函数。

另一种(匿名函数表达式):

setTimeout(function () {
  console.log("I waited 1 second!");
}, 1000);
复制代码

有几个缺点需要考虑:

  1. 在栈轨迹上匿名函数没有有用的名称可以表示,这可能会使得调试更加困难。
  2. 没有名称的情况下,如果这个函数需要为了递归等目的引用它自己,那么就需要很不幸地使用 被废弃的arguments.callee 引用。另一个需要自引用的例子是,当一个事件处理器函数在被触发后想要把自己解除绑定。
  3. 匿名函数省略的名称经常对提供更易读/易懂的代码很有帮助。一个描述性的名称可以帮助代码自解释。

2 块儿作为作用域

JavaScript 没有块儿作用域的能力。

考虑这个for循环:

for (var i = 0; i < 10; i++) {
  console.log(i);
}
复制代码

这里使用 var 时,我们在何处声明变量是无关紧要的,因为它们将总是属于外围作用域。这个代码段实质上为了代码风格的原因“假冒”了块儿作用域,并依赖于我们要管好自己,不要在这个作用域的其他地方意外地使用 i。比如上面无限循环的例子。

为什么要用仅将(或者至少是,仅 应当)在这个 for 循环中使用的变量 i 去污染一个函数的整个作用域呢?

幸运的是,ES6 改变了这种状态,并引入了一个新的关键字 let,作为另一种声明变量的方式伴随着 var

let 关键字将变量声明附着在它所在的任何块儿(通常是一个 { .. })的作用域中。换句话说,let 为它的变量声明隐含地劫持了任意块儿的作用域。

var foo = true;

if (foo) {
  let bar = foo * 2;
  bar = something(bar);
  console.log(bar);
}

console.log(bar); // ReferenceError
复制代码

我们可以在一个语句是合法文法的任何地方,通过简单地引入一个 { .. } 来为 let 创建一个任意的可以绑定的块儿。在这个例子中,我们在 if 语句内部制造了一个明确的块儿,在以后的重构中将整个块儿四处移动可能会更容易,而且不会影响外围的 if 语句的位置和语义。

注意:使用 let 做出的声明将 不会 在它们所出现的整个块儿的作用域中提升。

{
  console.log(bar); // ReferenceError!
  let bar = 2;
}
复制代码

复习

区分函数声明与表达式的最简单的方法是,这个语句中(不仅仅是一行,而是一个独立的语句)“function”一词的位置。如果“function”是这个语句中的第一个东西,那么它就是一个函数声明。否则,它就是一个函数表达式。

在 JavaScript 中函数是最常见的作用域单位。在另一个函数内部声明的变量和函数,实质上对任何外围“作用域”都是“隐藏的”,这是优秀软件的一个有意的设计原则。

但是函数绝不是唯一的作用域单位。块儿作用域指的是这样一种想法:变量和函数可以属于任意代码块儿(一般来说,就是任意的 { .. }),而不是仅属于外围的函数。

从 ES3 开始,try/catch 结构在 catch 子句上拥有块儿作用域。

在 ES6 中,引入了 let 关键字(var 关键字的表兄弟)允许在任意代码块中声明变量。if (..) { let a = 2; } 将会声明变量 a,而它实质上劫持了 if{ .. } 块儿的作用域,并将自己附着在这里。

虽然有些人对此深信不疑,但是块儿作用域不应当被认为是 var 函数作用域的一个彻头彻尾的替代品。两种机能是共存的,而且开发者们可以并且应当同时使用函数作用域和块儿作用域技术 —— 在它们各自可以产生更好,更易读/易维护代码的地方。

转载于:https://juejin.im/post/5ad5746a51882555867fea69

相关文章:

  • Mac 平台下功能强大的Shimo软件使用指南
  • 初学redis分页缓存方法实现
  • Redis 安装及配置
  • chroot 命令小记
  • Bugzilla安装问题总结-神奇
  • js 原型链(转)
  • 理解PHP中会话控制
  • LAMP架构应用实战—Apache服务介绍与安装01
  • asp.net向页面注册脚本
  • Unity 3D调用Windows打开、保存窗口、文件浏览器
  • Snapchat为Mac、Windows平台推出AR新工具,方便用户创造实景物体
  • 2003系统创建软RAID磁盘阵列完全手册
  • 如何解决临时空间暴增导致磁盘满问题?
  • 网络负载平衡
  • Linux的基本指令--目录和文件和文件属性和文件用户组
  • 10个确保微服务与容器安全的最佳实践
  • Android Volley源码解析
  • bootstrap创建登录注册页面
  • conda常用的命令
  • Cookie 在前端中的实践
  • CSS 专业技巧
  • HTTP中GET与POST的区别 99%的错误认识
  • Java|序列化异常StreamCorruptedException的解决方法
  • leetcode378. Kth Smallest Element in a Sorted Matrix
  • Magento 1.x 中文订单打印乱码
  • vue-cli3搭建项目
  • 基于MaxCompute打造轻盈的人人车移动端数据平台
  • 力扣(LeetCode)965
  • 那些被忽略的 JavaScript 数组方法细节
  • 通过来模仿稀土掘金个人页面的布局来学习使用CoordinatorLayout
  • 树莓派用上kodexplorer也能玩成私有网盘
  • ​Linux Ubuntu环境下使用docker构建spark运行环境(超级详细)
  • ​Linux·i2c驱动架构​
  • # 执行时间 统计mysql_一文说尽 MySQL 优化原理
  • #pragma once与条件编译
  • #stm32整理(一)flash读写
  • #我与Java虚拟机的故事#连载04:一本让自己没面子的书
  • #我与Java虚拟机的故事#连载08:书读百遍其义自见
  • $(selector).each()和$.each()的区别
  • (1)(1.9) MSP (version 4.2)
  • (2)STL算法之元素计数
  • (webRTC、RecordRTC):navigator.mediaDevices undefined
  • (附源码)php新闻发布平台 毕业设计 141646
  • (考研湖科大教书匠计算机网络)第一章概述-第五节1:计算机网络体系结构之分层思想和举例
  • .describe() python_Python-Win32com-Excel
  • .helper勒索病毒的最新威胁:如何恢复您的数据?
  • .net core IResultFilter 的 OnResultExecuted和OnResultExecuting的区别
  • .Net 代码性能 - (1)
  • .net 受管制代码
  • .NET 中各种混淆(Obfuscation)的含义、原理、实际效果和不同级别的差异(使用 SmartAssembly)
  • @angular/cli项目构建--http(2)
  • @cacheable 是否缓存成功_让我们来学习学习SpringCache分布式缓存,为什么用?
  • [202209]mysql8.0 双主集群搭建 亲测可用
  • [Asp.net mvc]国际化
  • [BZOJ1008][HNOI2008]越狱