目录
什么是作用域
理解作用域
词法作用域
全局作用域和函数作用域
全局作用域
函数作用域
变量提升
块级作用域
作用域链
作用域与执行上下文
解释阶段
执行阶段
总结
首页 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/imghw/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中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热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 尊渡假赌尊渡假赌尊渡假赌

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

c语言中typedef struct的用法 c语言中typedef struct的用法 May 09, 2024 am 10:15 AM

typedef struct 在 C 语言中用于创建结构体类型别名,简化结构体使用。它通过指定结构体别名将一个新的数据类型作为现有结构体的别名。优点包括增强可读性、代码重用和类型检查。注意:在使用别名前必须定义结构体,别名在程序中必须唯一并且仅在其声明的作用域内有效。

java中的variable expected怎么解决 java中的variable expected怎么解决 May 07, 2024 am 02:48 AM

Java 中的变量期望值异常可以通过以下方法解决:初始化变量;使用默认值;使用 null 值;使用检查和赋值;了解局部变量的作用域。

js中闭包的优缺点 js中闭包的优缺点 May 10, 2024 am 04:39 AM

JavaScript 闭包的优点包括保持变量作用域、实现模块化代码、延迟执行和事件处理;缺点包括内存泄漏、增加了复杂性、性能开销和作用域链影响。

c++中的include什么意思 c++中的include什么意思 May 09, 2024 am 01:45 AM

C++ 中的 #include 预处理器指令将外部源文件的内容插入到当前源文件中,以复制其内容到当前源文件的相应位置。主要用于包含头文件,这些头文件包含代码中需要的声明,例如 #include <iostream> 是包含标准输入/输出函数。

C++ 智能指针:全面剖析其生命周期 C++ 智能指针:全面剖析其生命周期 May 09, 2024 am 11:06 AM

C++智能指针的生命周期:创建:分配内存时创建智能指针。所有权转移:通过移动操作转移所有权。释放:智能指针离开作用域或被明确释放时释放内存。对象销毁:所指向对象被销毁时,智能指针成为无效指针。

c++中函数的定义和调用可以嵌套吗 c++中函数的定义和调用可以嵌套吗 May 06, 2024 pm 06:36 PM

可以。C++ 允许函数嵌套定义和调用。外部函数可定义内置函数,内部函数可在作用域内直接调用。嵌套函数增强了封装性、可复用性和作用域控制。但内部函数无法直接访问外部函数的局部变量,且返回值类型需与外部函数声明一致,内部函数不能自递归。

vue中let和var的区别 vue中let和var的区别 May 08, 2024 pm 04:21 PM

在 Vue 中,let 和 var 声明变量时在作用域上存在差异:作用域:var 具有全局作用域,let 具有块级作用域。块级作用域:var 不创建块级作用域,let 创建块级作用域。重新声明:var 允许在同一作用域内重新声明变量,let 不允许。

js中this的指向有几种情况 js中this的指向有几种情况 May 06, 2024 pm 02:03 PM

JavaScript 中,this 的指向类型有:1. 全局对象;2. 函数调用;3. 构造函数调用;4. 事件处理程序;5. 箭头函数(继承外层 this)。此外,可以使用 bind()、call() 和 apply() 方法显式设置 this 的指向。

See all articles