JavaScript中的作用域与闭包

作用域

  • JavaScript引擎从头到尾负责JavaScript程序的编译以及执行。
  • 编译器:负责语法分析以及代码生成等。
  • 作用域:收集并维护所有编译阶段所声名的变量组成一系列严格的查询规则(其实就是作用域链),用来确定当前所执行的代码是否拥有对这些变量的访问权限

作用域链

作用域链中的每个作用域都包含了在该作用域中声明的变量,以及对外部作用域的引用。这些引用形成了一个链条,允许引擎按照特定的规则(LHS、RHS等)在不同的作用域中(逐级向上)查找变量的值。

  • LHS:在赋值操作中查找变量的存放位置,为赋值操作寻找目标。

  • RHS:查找变量中存放的。(字面量不会进行RHS查询,比如数字" 5 ")。

    JavaScript引擎、作用域与编译器之间的关系

词法作用域

作用域共有两种主要的工作模型。第一种是最为普遍的,被大多数编程语言所采用的词法作用域。另外一种叫作动态作用域,仍有一些编程语言在使用(比如Bash脚本、Perl中的一些模式等),而JavaScript所采用词法作用域

对词法作用域的理解

在代码编写后,JavaScript 解析器会对代码进行分析,识别出变量声明和作用域嵌套关系。这个过程在语法分析阶段完成。在这个阶段,解析器会创建作用域链,并确定变量在不同作用域中的可见性和访问权限。

因此,词法作用域是在代码编写阶段就确定的,它在代码执行之前就已经被建立起来,换句话说,词法作用域是由你在写代码时将变量和块作用域(ES6)写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情况下是这样的,因为有evalwith这俩小子搞特殊😫 )。这也是为什么 JavaScript 的作用域是词法作用域(也叫静态作用域)的原因,因为作用域的规则是在代码编写时静态决定的,而不是在代码执行时动态确定的。

注意:

​ JavaScript解析器 整个JavaScript引擎

示例代码 ​

javascript 复制代码
 function foo(a) {
     var b = a * 2;
     function bar(c) {
       console.log(a, b, c); // 2,4 12
     }
     bar(b * 3);
  }
 foo(2);

作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见第一个匹配的标识符为止。(有先找自己的,自己没有就找别人的,别人没有那就玩完了😏,此时报错在向你招手😍。)

函数作用域与块作用域

函数作用域

函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(嵌套的作用域中也可以使用,毕竟有作用域链查找)

​ 示例代码 ​

javascript 复制代码
 function foo(a) { // <-- 开端
     var b = a;
     console.log(b);
  }// <-- 结尾
 foo(2);

注意:JavaScript中var声名的变量只存在函数作用域全局作用域,而块作用域是在ES6letconst中才存在。

块作用域

​ 示例代码 ​

javascript 复制代码
 if (666) {
   var a = 10;
   console.log(a); // 10
 }
 console.log(a); // 依旧可以访问到 a ===> 10

而let与const不存在此问题

javascript 复制代码
  if (666) { // <-- 块作用域Start
   let a = 10;
   console.log(a); // 10
 }// <-- 块作用域End
 console.log(a); // Uncaught ReferenceError: a is not defined
javascript 复制代码
  if (666) {// <-- 块作用域Start
   const b = 20;
   console.log(b); // 20
 }// <-- 块作用域End
 console.log(b); // Uncaught ReferenceError: b is not defined

for循环与块作用域

javascript 复制代码
 for (var i = 1; i <= 3; i++) {
   setTimeout(function timer() {
     console.log(i);
   }, i * 1000);
 }

执行了3次没错,但是为什么都是4?这是由于var出手了😁,请看下图

那该如何做呢?

用let😙

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

使用let关键字声明变量i,会在每次循环迭代时创建一个新的块级作用域(变量i在循环过程中不止被声明一次)。每个迭代都会创建一个新的作用域,每个迭代都会使用上一个迭代结束时的初始化 i这个变量,确保在定时器回调函数内部捕获到的i值是该迭代中的值。

var的变量提升

变量和函数(函数首先会被提升,然后才是变量)在内的所有声明都会在编译阶段,也就是任何代码被执行前首先被处理。

只有声明本身会被提升,而赋值或其他运行逻辑会留在原地。

var声名提升示例代码 ​

javascript 复制代码
 console.log(a);
 var a = 2;

提升后:

javascript 复制代码
 var a; // 声名提升
 console.log(a); // undefined
 a = 2; // 赋值操作留在原地

​ 函数声名提升示例代码 ​

javascript 复制代码
 foo(); // 1
 var foo;
 function foo() {
   console.log(1);
 }
 foo = function () {
   console.log(2);
 };

提升后:

javascript 复制代码
 function foo() {
   console.log(1);
 }
 var foo; // 变量声明被提升,但由于函数声明存在,被忽略(因为重名了)
 foo(); // 1
 foo = function () {
   console.log(2);
 };

var foo被忽略是因为函数声明的提升优先于变量声明的提升,因此在遇到这种情况时,函数声明会覆盖同名的变量声明。

另一种情况: 存在多个同名的函数声明,后面的函数声明会覆盖前面的函数声明。

javascript 复制代码
 foo(); // 3
 function foo() {
   console.log(1);
 }
 var foo = function () {
   console.log(2);
 };
 function foo() {
   console.log(3);
 }

提升后

javascript 复制代码
 // 后来居上!(被覆盖了)
 function foo() {
   console.log(1);
 } 
 function foo() {
   console.log(3);
 }
 var foo; // 变量声明被提升,但由于函数声明存在,被忽略(因为重名了)
 foo();// 3
 foo = function () {
   console.log(2);
 };

闭包

  • 当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
  • 无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
javascript 复制代码
 function foo() {
   var a = 2;
   function bar() {
     console.log(a);
   }
   return bar;
 }
 var baz = foo();
 baz(); // 2 

函数bar()的词法作用域能够访问foo()的内部作用域,然后将bar()函数本身当作一个值类型(baz)进行传递。赋值给变量baz并调用baz(),实际上只是通过不同的标识符引用调用了内部的函数bar()。

基于bar()所声明的词法作用域位置,它拥有涵盖foo()内部作用域的引用,使得该作用域(foo)能够一直存活,以供bar()在之后任何时间进行引用。

bar()依然持有对该作用域的引用,而这个引用就叫作闭包。

参考资料来源:《你不知道的JavaScript 上卷》------ Kyle Simpson

相关推荐
Dragon Wu1 分钟前
前端 Canvas 绘画 总结
前端
CodeToGym6 分钟前
Webpack性能优化指南:从构建到部署的全方位策略
前端·webpack·性能优化
~甲壳虫7 分钟前
说说webpack中常见的Loader?解决了什么问题?
前端·webpack·node.js
~甲壳虫11 分钟前
说说webpack proxy工作原理?为什么能解决跨域
前端·webpack·node.js
Cwhat12 分钟前
前端性能优化2
前端
熊的猫1 小时前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
瑶琴AI前端1 小时前
uniapp组件实现省市区三级联动选择
java·前端·uni-app
会发光的猪。2 小时前
如何在vscode中安装git详细新手教程
前端·ide·git·vscode
别拿曾经看以后~3 小时前
【el-form】记一例好用的el-input输入框回车调接口和el-button按钮防重点击
javascript·vue.js·elementui
我要洋人死3 小时前
导航栏及下拉菜单的实现
前端·css·css3