搜索
首页web前端js教程深入解析JavaScript中的作用域

深入解析JavaScript中的作用域

May 31, 2021 pm 05:28 PM
javascript作用域

本篇文章带大家深入理解JavaScript作用域。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。

深入解析JavaScript中的作用域

这篇文章称为笔记更为合适一些,内容来源于 《你不知道的JavaScript(上卷)》第一部分 作用域和闭包。讲的很不错,非常值得一看。

什么是作用域

作用域是根据名称查找变量的一套规则

理解作用域

先来理解一些基础概念:

  • 引擎:从头到尾负责整个JavaScript程序的编译及执行过程。
  • 编译器:负责语法分析和代码生成。这部分也可以看 JavaScript代码是如何被执行的
  • 作用域:负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。

接下来来看看下面代码的执行过程:

var a = 2;
  • 遇见 var a,编译器 会问 作用域 变量a是否存在于同一个作用域集合中。如果存在,编译器会忽略声明,继续编译;否则,会要求作用域在当前作用域集合中声明一个新的变量,并命名为 a

  • 接下来 编译器 会为 引擎 生成运行时所需的代码,用来处理 a = 2 这个赋值操作。引擎运行时会先问作用域,当前作用域集中是否存在变量a。如果是,引擎就会使用该变量;如果不存在,引擎会继续查找该变量

  • 如果 引擎 找到了a 变量,就会将 2 赋值给它,否则引擎就抛出一个错误。

总结:变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量,然后在运行时引擎就会会作用域中查找该变量,如果能够找到就对它赋值。

编译器在编译过程的第二步中生成了代码,引擎执行它时,会通过查找变量 a来判断它是否已声明过。查找的过程中由作用域进行协助,但是引擎执行怎么样的查找,会影响最终的查找结果。

在我们的例子中,引擎会为变量 a 进行 LHS 查询,另外一个查找的类型叫做 RHS。 ”L“ 和 "R" 分别代表一个赋值操作左侧和右侧。当变量出现在赋值操作的左侧时进行 LHS 查询,出现在右侧时进行 RHS 查询。

LHS:试图找到变量的容器本身,从而可以对其赋值;RHS: 就是简单地查找某个变量的值。
console.log(a);

对 a 的引用是一个 RHS 引用,因为这里 a 并没有赋予任务值,相应地需要查找并取得 a 的值,这样才能将值传递给 console.log(...)

a = 2;

这里对 a 的引用是 LHS 引用,因为实际上我们并不关心当前的值是什么,只是想要为 = 2这个赋值操作找到目标。

funciton foo(a) {
    console.log(a)
}

foo(2);
  1. 最后一行 foo 函数的调用需要对 foo 进行 RHS 引用,去找 foo的值,并把它给我
  2. 代码中隐式的 a = 2 操作可能很容易被你忽略掉,这操作发生在 2 被当做参数传递给 foo 函数时,2 会被分配给参数 a,为了给参数 a (隐式地) 分配值,需要进行一次 LHS 查询。
  3. 这里还有对 a 进行的 RHS 引用,并且将得到的值传给了 console.log(...)console.log(...) 本身也需要一个引用才能执行,因此会对 console对象进行 RHS 查询,并且检查得到的值中是否有一个叫做 log的方法。

RHS查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出 ReferenceError 异常。进行RHS查询找到了一个变量,但是你尝试对这个变量的值进行不合理的操作,比如试图对一个非函数类型的值进行调用,后者引用null或 undefined 类型的值中的属性,那么引擎会抛出一个另外一种类型的异常 TypeError。
引擎执行 LHS 查询时如果找不到该变量,则会在全局作用域中创建一个。但是在严格模式下,并不是自动创建一个全局变量,而是会抛出  ReferenceError 异常

补充JS几种常见的错误类型

简单总结如下:

作用域是一套规则,用于确定在哪里找,怎么找到某个变量。如果查找的目的是对变量进行赋值,那么就会使用 LHS查询; 如果目的是获取变量的值,就会使用 RHS 查询;
JavaScript 引擎执行代码前会对其进行编译,这个过程中,像 var a = 2 这样的声明会被分解成两个独立的步骤

  • var a 在其作用域中声明变量,这会在最开始的阶段,也就是代码执行前进行

  • 接下来,a = 2 会查询 (LHS查询)变量 a 并对其进行赋值。

词法作用域

词法作用域是你在写代码时将变量写在哪里来决定的。编译的词法分析阶段基本能够知道全局标识符在哪里以及是如何声明的,从而能够预测在执行过程中如果对他们查找。

有一些方法可以欺骗词法作用域,比如 eval, with, 这两种现在被禁止使用,1是严格模式和非严格模式下表现不同 2是有性能问题, JavaScript引擎在编译阶段会做很多性能优化,而其中很多优化手段都依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到识别符,eval, with会改变作用域,所以碰到它们,引擎将无法做优化处理。

全局作用域和函数作用域

全局作用域

  • 在最外层函数和最外层函数外面定义的变量拥有全局作用域
var a = 1;
function foo() {

}

变量a 和函数声明 foo 都是在全局作用域中的。

  • 所有未定义直接赋值的变量自动声明为拥有全局作用域

var a = 1;
function foo() {
    b = 2;
}
foo();
console.log(b); // 2
  • 所有 window 对象的属性拥有全局作用域

函数作用域

函数作用域是指在函数内声明的所有变量在函数体内始终是可见的。外部作用域无法访问函数内部的任何内容。

function foo() {
    var a = 1;
    console.log(a); // 1
}
foo();
console.log(a); // ReferenceError: a is not defined
只有函数的{}构成作用域,对象的{}以及 if(){}都不构成作用域;

变量提升

提升是指声明会被视为存在与其所出现的作用域的整个范围内。

JavaScript编译阶段是找到找到所有声明,并用合适的作用域将他们关联起来(词法作用域核心内容),所以就是包含变量和函数在内的所有声明都会在任何代码被执行前首先被处理。

每个作用域都会进行提升操作。

function foo() {
    var a;
    console.log(a); // undefined
    a = 2;
}
foo();
注意,函数声明会被提升,但是函数表达式不会被提升。

关于 块级作用域和变量提升的内容之前在 从JS底层理解var、let、const这边文章中详细介绍过,这里不再赘述。

块级作用域

我们来看下面这段代码

for(var i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i);
    })
}
console.log(`当前的i为${i}`); // 当前的i为5

上面这段代码我们希望是输出 0,1, 2, 3, 4 ,但是实际上输出的是 5,5, 5, 5, 5。我们在 for 循环的头部直接定义了变量 i,通常是因为只想在 for 循环内部的上下文中使用 i,但是实际上 此时的 i 被绑定在外部作用域(函数或全局)中。

,块级作用域是指在指定的块级作用域外无法访问。在ES6之前是没有块级作用域的概念的,ES6引入了 let 和 const。我们可以改写上面的代码,使它按照我们想要的方式运行。

for(let i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i);
    })
}
// 0 1 2 3 4
console.log(`当前的i为${i}`); // ReferenceError: i is not defined

此时 for 循环头部的 let 不仅将 i 绑定到了 for 循环的迭代中,事实上将它重新绑定到了循环的每一个迭代中,确保使用上一次循环迭代结束的值重新进行赋值。

let声明附属于一个新的作用域而不是当前的函数作用域(也不属于全局作用域)。但是其行为是一样的,可以总结为:任何声明在某个作用域内的变量,都将附属于这个作用域。
const也是可以用来创建块级作用域变量,但是创建的是固定值。

作用域链

JavaScript是基于词法作用域的语言,通过变量定义的位置就能知道变量的作用域。全局变量在程序中始终都有都定义的。局部变量在声明它的函数体内以及其所嵌套的函数内始终是有定义的。

每一段 JavaScript 代码都有一个与之关联的作用域链(scope chain)。这个作用域链是一个对象列表或者链表。当 JavaScript 需要查找变量 x 的时候(这个过程称为变量解析),它会从链中的第一个变量开始查找,如果这个对象上依然没有一个名为 x 的属性,则会继续查找链上的下一个对象,如果第二个对象依然没有名为 x 的属性,javaScript会继续查找下一个对象,以此类推。如果作用域链上没有任何一个对象包含属性 x, 那么就认为这段代码的作用域链上不存在 x, 并最终抛出一个引用错误 (Reference Error) 异常。

下面作用域中有三个嵌套的作用域。

function foo(a) {
    var b = a * 2;
    function bar(c) {
        console.log(a, b, c)
    }
    bar( b * 3);
}
foo(2);

<img src="/static/imghwm/default1.png" data-src="https://img.php.cn/upload/image/252/331/635/1622453204885970.png" class="lazy" title="1622453204885970.png" alt="1.png">

气泡1包含着整个全局作用域,其中只有一个标识符:foo;
气泡2包含着foo所创建的作用域,其中有三个标识符:a、bar 和 b;
气泡3包含着 bar所创建的作用域,其中只有一个标识符:c

执行 console.log(...),并查找 a,b,c三个变量的引用。下面我们来看看查找这几个变量的过程.
它首先从最内部的作用域,也就是 bar(..) 函数的作用域气泡开始找,引擎在这里无法找到 a,因此就会去上一级到所嵌套的 foo(...)的作用域中继续查找。在这里找到了a,因此就使用了这个引用。对b来说也一样,而对 c 来说,引擎在 bar(..) 中就找到了它。

如果 a,c都存在于 bar(...) 内部,console.log(...)就可以直接使用 bar(...) 中的变量,而无需到外面的 foo(..)中查找。作用域会在查找都第一个匹配的标识符时就停止。

在多层的嵌套作用域中可以定义同名的标识符,这叫”遮蔽效应“。

var a = &#39;外部的a&#39;;
function foo() {
    var a = &#39;foo内部的a&#39;;
    console.log(a); // foo内部的a
}
foo();

作用域与执行上下文

JavaScript的执行分为:解释和执行两个阶段

解释阶段

  • 词法分析
  • 语法分析
  • 作用域规则确定

执行阶段

  • 创建执行上下文
  • 执行函数代码
  • 垃圾回收

作用域在函数定义时就已经确定了,而不是在函数调用时确定,但执行上下文是函数执行之前创建的。

总结

  • 作用域就是一套规则,用于确定在哪里找以及怎么找到某个变量。

  • 词法作用域在你写代码的时候就确定了。JavaScript是基于词法作用域的语言,通过变量定义的位置就能知道变量的作用域。ES6引入的let和const声明的变量在块级作用域中。

  • 声明提升是指声明会被视为存在与其所出现的作用域的整个范围内。

  • 查找变量的时候会先从内部的作用域开始查找,如果没找到,就往上一级进行查找,依次类推。

  • 作用域在函数定义时就已经确定了,执行上下文是函数执行之前创建的。

更多编程相关知识,请访问:编程视频!!

以上是深入解析JavaScript中的作用域的详细内容。更多信息请关注PHP中文网其他相关文章!

声明
本文转载于:segmentfault。如有侵权,请联系admin@php.cn删除
了解JavaScript引擎:实施详细信息了解JavaScript引擎:实施详细信息Apr 17, 2025 am 12:05 AM

理解JavaScript引擎内部工作原理对开发者重要,因为它能帮助编写更高效的代码并理解性能瓶颈和优化策略。1)引擎的工作流程包括解析、编译和执行三个阶段;2)执行过程中,引擎会进行动态优化,如内联缓存和隐藏类;3)最佳实践包括避免全局变量、优化循环、使用const和let,以及避免过度使用闭包。

Python vs. JavaScript:学习曲线和易用性Python vs. JavaScript:学习曲线和易用性Apr 16, 2025 am 12:12 AM

Python更适合初学者,学习曲线平缓,语法简洁;JavaScript适合前端开发,学习曲线较陡,语法灵活。1.Python语法直观,适用于数据科学和后端开发。2.JavaScript灵活,广泛用于前端和服务器端编程。

Python vs. JavaScript:社区,图书馆和资源Python vs. JavaScript:社区,图书馆和资源Apr 15, 2025 am 12:16 AM

Python和JavaScript在社区、库和资源方面的对比各有优劣。1)Python社区友好,适合初学者,但前端开发资源不如JavaScript丰富。2)Python在数据科学和机器学习库方面强大,JavaScript则在前端开发库和框架上更胜一筹。3)两者的学习资源都丰富,但Python适合从官方文档开始,JavaScript则以MDNWebDocs为佳。选择应基于项目需求和个人兴趣。

从C/C到JavaScript:所有工作方式从C/C到JavaScript:所有工作方式Apr 14, 2025 am 12:05 AM

从C/C 转向JavaScript需要适应动态类型、垃圾回收和异步编程等特点。1)C/C 是静态类型语言,需手动管理内存,而JavaScript是动态类型,垃圾回收自动处理。2)C/C 需编译成机器码,JavaScript则为解释型语言。3)JavaScript引入闭包、原型链和Promise等概念,增强了灵活性和异步编程能力。

JavaScript引擎:比较实施JavaScript引擎:比较实施Apr 13, 2025 am 12:05 AM

不同JavaScript引擎在解析和执行JavaScript代码时,效果会有所不同,因为每个引擎的实现原理和优化策略各有差异。1.词法分析:将源码转换为词法单元。2.语法分析:生成抽象语法树。3.优化和编译:通过JIT编译器生成机器码。4.执行:运行机器码。V8引擎通过即时编译和隐藏类优化,SpiderMonkey使用类型推断系统,导致在相同代码上的性能表现不同。

超越浏览器:现实世界中的JavaScript超越浏览器:现实世界中的JavaScriptApr 12, 2025 am 12:06 AM

JavaScript在现实世界中的应用包括服务器端编程、移动应用开发和物联网控制:1.通过Node.js实现服务器端编程,适用于高并发请求处理。2.通过ReactNative进行移动应用开发,支持跨平台部署。3.通过Johnny-Five库用于物联网设备控制,适用于硬件交互。

使用Next.js(后端集成)构建多租户SaaS应用程序使用Next.js(后端集成)构建多租户SaaS应用程序Apr 11, 2025 am 08:23 AM

我使用您的日常技术工具构建了功能性的多租户SaaS应用程序(一个Edtech应用程序),您可以做同样的事情。 首先,什么是多租户SaaS应用程序? 多租户SaaS应用程序可让您从唱歌中为多个客户提供服务

如何使用Next.js(前端集成)构建多租户SaaS应用程序如何使用Next.js(前端集成)构建多租户SaaS应用程序Apr 11, 2025 am 08:22 AM

本文展示了与许可证确保的后端的前端集成,并使用Next.js构建功能性Edtech SaaS应用程序。 前端获取用户权限以控制UI的可见性并确保API要求遵守角色库

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
1 个月前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳图形设置
1 个月前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您听不到任何人,如何修复音频
1 个月前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.聊天命令以及如何使用它们
1 个月前By尊渡假赌尊渡假赌尊渡假赌

热工具

WebStorm Mac版

WebStorm Mac版

好用的JavaScript开发工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

EditPlus 中文破解版

EditPlus 中文破解版

体积小,语法高亮,不支持代码提示功能

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

VSCode Windows 64位 下载

VSCode Windows 64位 下载

微软推出的免费、功能强大的一款IDE编辑器