>  기사  >  웹 프론트엔드  >  범위 체인의 JavaScript 상세 분석

범위 체인의 JavaScript 상세 분석

WBOY
WBOY앞으로
2022-11-03 16:02:482208검색

이 기사에서는 범위 체인의 관련 내용을 주로 소개하는 JavaScript에 대한 관련 지식을 제공합니다. 범위는 선언된 모든 식별자(변수)를 수집하고 유지 관리하는 일련의 쿼리이며 매우 엄격한 규칙입니다. 현재 실행 중인 코드의 이러한 식별자에 대한 액세스 권한을 결정하기 위해 일련의 규칙이 구현되어 있습니다. 함께 살펴보겠습니다. 모든 사람에게 도움이 되기를 바랍니다.

범위 체인의 JavaScript 상세 분석

【관련 추천: JavaScript 비디오 튜토리얼, 웹 프론트엔드

1.

범위는 선언된 모든 식별자(변수)로 구성된 일련의 쿼리를 수집 및 유지 관리하고 현재 실행 중인 코드의 이러한 식별자에 대한 액세스 권한을 결정하는 매우 엄격한 규칙 집합을 시행하는 규칙 집합입니다.

범위가 무엇인지 더 이해하기 쉬운 용어로 설명하면 다음과 같습니다.

  • 범위는 먼저 규칙 집합입니다作用域首先是一套规则
  • 该规则的用处是用来存储变量,以及如何有限制的获取变量。

用一个不一定完全恰当的形容来类比作用域就是,存在一个国际银行,你将手里各国的货币存入其中,当你要取出一些钱时,它有一套规则限定你只能在可使用货币的当地才能取出相应的该货币。  这个银行和它制定的规则,就是作用域对于变量的作用。

2. 词法作用域与动态作用域

在JavaScript中,所使用的作用域是词法作用域,也称为静态作用域。它是在编译前就确定的。JavaScript本质上是一门编译型语言,只不过它编译发生的时间节点是在代码执行前的几微秒。不同于其他编译型语言是在构建的过程中编译,所以才看上去更像是解释型语言。

关于编译和解释暂且不论,只需要理解到词法作用域就是静态作用域。 理解静态的含义就是当代码在书写下定义时就已经确定了。也就是人所读到代码中变量和函数被定义在什么范围,该范围就是它们的作用域。

用一个简单的例子来理解:

var a = 1function foo() {  console.log(a)
}function bar(b) {  var a = 2
  console.log(a)  foo()  function baz() {      console.log(b)
  }  return baz
}var c = bar(a)c()

对于定义的三个函数foo, bar, baz以及变量a,它们在书写时作用域就已经定义。

因而在代码执行时, bar函数先调用传入变量a的值, 在第一个输出变量a值时,会先询问自身作用域是否定义过变量a, 定义过则询问是否存在a的值,存在着输出变量a为2.

然后开始调用foo函数, foo中只有输出变量a的值, 同样也会询问自身作用域是否定义过变量a, foo中未定义, 则会往上寻找自身定义时的作用域询问是否定义过变量a, 全局作用域定义过并且存在a值, 因而输出a为1。其实这其中已经涉及到了作用域链,但暂且不议。

之后进入到c函数调用也就是baz函数, baz中输出变量b的值,b会询问自身作用域是否存在定义过变量b, baz未定义, 则往上查找自身定义时的作用域也就是bar函数作用域是否定义过变量b, bar实际隐含在参数中为变量b定义并且赋值为1, 因而最终输出为1。

这就是静态作用域,只需要看变量和函数的书写位置,即可确定它们都作用域范围。

与之相对的是动态作用域, 在JavaScript中涉及到动态作用域的只有this指向,这在之后复习this时会涉及。

假设JavaScript是动态作用域,同样看上述例子里的代码执行过程。

bar先调用并传入变量a, 在第一个输出变量a值时, 完全获取到变量a因而输出2。在调用foo函数时, 由于自身作用域没有变量a, 则会从自身被调用的位置的作用域去往上查找,则此时为函数bar的作用域,因而输出的a值为2。

c函数调用也就是baz函数调用时,也同样是自身不存在变量b,去寻找自身被调用的位置的作用域,也就是全局作用域,全局作用域中同样未定义过变量b, 则直接报错。

3. 作用域的分类

作用域的分类可以按照上述所说的静动分为:

  • 静态作用域
  • 动态作用域
  • 이 규칙의 목적은 변수를 저장하고 변수를 얻는 방법입니다. 제한이 있습니다.

범위를 설명하는 데 완전히 적합하지 않을 수 있는 비유를 사용하려면 국제 은행이 있습니다. 손에 있는 여러 나라의 통화를 거기에 입금하고 싶을 때. 일련의 규칙이 있습니다. 해당 통화가 사용 가능한 경우에만 해당 통화를 인출할 수 있습니다. 이 은행과 그것이 공식화하는 규칙은 변수에 대한 범위의 역할입니다. 🎜

2. 어휘 범위와 동적 범위🎜🎜JavaScript에서 사용되는 범위는 정적 범위라고도 하는 어휘 범위입니다. 컴파일 전에 결정됩니다. JavaScript는 본질적으로 컴파일된 언어이지만 코드가 실행되기 몇 마이크로초 전에 컴파일이 발생합니다. 다른 컴파일 언어와 달리 생성 과정에서 컴파일되기 때문에 해석된 언어에 더 가깝습니다. 🎜🎜 컴파일과 해석에 관계없이 어휘 범위는 정적 범위라는 점만 이해하면 됩니다. 정적의 의미는 코드가 작성되고 정의될 때 결정됩니다라는 것입니다. 즉, 사람들이 읽는 코드에서 변수와 함수가 정의되는 범위가 해당 범위입니다. 🎜🎜이해를 위해 간단한 예를 사용하세요. 🎜
var name = 'xavier'function foo() {  var name = 'parker'
  var age = 18

  function bar() {    var name = 'coin'
    return age
  }  return bar()
}foo()console.log(age) // 报错
🎜 정의된 세 가지 함수 foo, bar, baz 및 변수 a의 경우 해당 범위는 작성 시 정의되었습니다. 🎜🎜그래서 코드가 실행되면 bar 함수는 먼저 전달된 변수 a의 값을 호출합니다. 변수 a의 값이 처음으로 출력되면 먼저 변수 a가 정의되어 있는지 묻습니다. 자체 범위가 정의된 경우 a의 값이 존재하는지 묻습니다. 2인 출력 변수 a가 있습니다.🎜🎜 그런 다음 foo 함수 호출을 시작하면 출력 변수 a의 값만 있습니다. foo, 또한 변수 a가 정의되었지만 foo에 정의되어 있지 않은지 는 자체 범위를 묻고 정의되었을 때 범위를 찾습니다. > 변수 a가 정의되었는지 묻고 전역 범위가 정의되었으며 값이 존재하므로 출력 a는 1입니다. 사실 스코프 체인은 이미 포함되어 있지만 지금은 이에 대해 논의하지 않겠습니다. 🎜🎜그런 다음 baz 함수인 c 함수 호출을 입력합니다. baz는 변수 b가 자체 범위에 정의되어 있는지 묻습니다. 정의된 의 함수입니다. Domain은 변수 b가 bar 함수 범위에 정의되었는지 여부를 나타냅니다. bar는 실제로 매개변수의 변수 b에 대해 암시적으로 정의되고 값 1이 할당됩니다. 출력은 1입니다. 🎜🎜이것은 정적 범위입니다. 범위를 결정하려면 변수와 함수의 쓰기 위치만 보면 됩니다. 🎜🎜그 반대는 동적 범위입니다. JavaScript에서 동적 범위와 관련된 유일한 것은 이 포인터입니다. 이에 대해서는 나중에 검토할 때 다루겠습니다. 🎜🎜JavaScript가 동적 범위라고 가정하고 위의 예에서 코드 실행 프로세스를 살펴보세요. 🎜🎜bar가 먼저 호출되어 변수 a에 전달됩니다. 변수 a의 값이 처음 출력되면 변수 a가 완전히 얻어지고 2가 출력됩니다. foo 함수를 호출하면 자신의 스코프에 변수 a가 없기 때문에 호출된 위치의 스코프에서 조회하게 됩니다. 이때는 함수의 스코프입니다. bar이므로 출력 a의 값은 2입니다. 🎜🎜c 함수가 호출될 때, 즉 baz 함수가 호출될 때 변수 b가 존재하지 않는데, 이는 전역변수인 호출된 위치의 범위를 찾는다. 전역 범위에서도 마찬가지입니다. 변수 b가 정의되지 않은 경우 오류가 직접 보고됩니다. 🎜

3. 범위 분류 🎜🎜범위 분류는 위에서 언급한 것처럼 정적 범위와 동적 범위로 나눌 수 있습니다. 🎜🎜🎜정적 범위🎜 🎜 동적 범위🎜🎜🎜정적 범위, 즉 어휘 범위에서는 특정 범위에 따라 다음과 같이 나눌 수도 있습니다. 🎜
  • 全局作用域
  • 函数作用域
  • 块级作用域

3.1 全局作用域

全局作用域可以理解为所有作用域的祖先作用域, 它包含了所有作用域在其中。也就是最大的范围。反向理解就是除了函数作用域和被{}花括号包裹起来的作用域之外,都属于全局作用域

3.2 函数作用域

之所以在全局作用域外还需要函数作用域,主要是有几个原因:

  • 可以存在一个更小的范围存放自身内部的变量和函数,外部无法访问
  • 由于外部无法访问,所以相当于隐藏了内部细节,仅提供输入和输出,符合最小暴露原则
  • 同时不同的函数作用域可以各自命名相同的变量和函数,而不产生命名冲突
  • 函数作用域可以嵌套函数作用域,就像俄罗斯套娃一样可以一层套一层,最终形成了作用域链

用一个例子来展示:

var name = 'xavier'function foo() {  var name = 'parker'
  var age = 18

  function bar() {    var name = 'coin'
    return age
  }  return bar()
}foo()console.log(age) // 报错

当代码执行时, 最终会报错表示age查找不到。 因为变量age是在foo函数中定义, 属于foo函数作用域中, 验证了第一点外部无法访问内部。

而当代码只执行到foo函数调用时, 其实foo函数有执行过程, 最终是返回了bar函数的调用,返回的结果应该是18。 在对于编写代码的人来说,其实只需要理解一个函数的作用是什么, 然后给一个需要的输入,最后得出一个预期所想的输出,而不需要在意函数内部到底是怎么编写的。验证了第二点只需要最小暴露原则。

在这代码中, 对name变量定义过三次, 但每次都在各自的作用域中而不会产生覆盖的结果。在那个作用域里调用,该作用域就会返回相应的值。这验证了第三点规避命名冲突。

最终bar函数是在foo函数内部定义的,foo函数获取不到bar内部的变量和函数,但是bar函数可以通过作用域链获取到其父作用域也就是foo里的变量与函数。这验证了第四点。

3.3 块级作用域

块级作用域在ES6之后才开始普及,对于是var声明的变量是无效的,仅对let和const声明的变量有效。以{}包裹的代码块就会形成块级作用域, 例如if语句, try/catch语句,while/for语句。但声明对象不属于。

let obj = {  a: 1,   // 这个区域不叫做块级作用域}  

if (true) {  // 这个区域属于块级作用域
  var foo = 1
  let bar = 2}console.log(foo)  // 1console.log(bar)  // 报错

用一个大致的类比来形容全局作用域,函数作用域和块级作用域。一个家中所有的范围就称为全局作用域,而家中的各个房间里的范围则是函数作用域, 甚至可能主卧中还配套有单独的卫生间的范围也属于函数作用域,拥有的半开放式厨房则是块级作用域。

假设你要在家中寻找自己的猫,当它在客厅中,也就是全局作用域里,你可以立马找到。但如果猫在房间里,而没发出声音。你在客厅中是无法判断它在哪里,也就是无法找到它。这就是函数作用域。但是如果它在半开放式厨房里,由于未完全封闭,它是能跑出来的,所以你还是能找得到它。 反之你在房间里,如果它也在,那么可以直接找到。但如果你在房间而它在客厅中,则你可以选择开门去客厅寻找,一样也能找到。

4. 执行上下文和作用域的关系

上述的过程过于理论化,因而现在通过对于实质的情况也就是内存中的情况来讨论。

之前上一篇说过在ES3中执行上下文都有三大内容:

  • 变量对象
  • 作用域链
  • this

实际在内存中,对于全局作用域来说,它所涵盖的范围就是全局对象GO。因为全局对象保存了所有关于全局作用域中的变量和方法。

而对于函数来说,当函数被调用时所创建出的函数执行上下文里的活动对象AO所涵盖的范围就是函数作用域, 并且函数本身存在有一个内部属性[[scope]], 它是用来保存其父作用域的,而父作用域实际上也是另一个变量对象。

对于块级代码来说,就不能用ES3这套来解释,而是用ES6中词法环境和变量环境来解释。块级代码会创建出块级执行上下文,但块级执行上下文里只存在词法环境,不存在变量环境,因而这词法环境里的环境记录就是块级作用域。

相同的解释对于全局和函数也一样,对于ES6中,它们执行上下文里的词法环境和变量环境的环境记录涵盖的范围就是它们的作用域。

用一段代码来更好的理解:

var a = 'a'function foo() {    let b = 'b'
    console.log(c)
}if (true) {    let a = 'c'
    var c = 'c'
    console.log(a)
}foo()console.log(a)

对于这段代码刚编译完准备开始执行,也就是代码创建时,此刻执行上下文栈和内存中的图为:

当开始进行到if语句时,会创建块级执行上下文,并执行完if语句时执行上下文栈和内存图为:

当if语句执行完后, 就会被弹出栈,销毁块级执行上下文。然后开始调用foo函数,创建函数执行上下文,此时执行栈和内存图为:

当foo执行时,变量b被赋值为'b',同时输出c时会在自身环境记录中寻找,但未找到,因而往上通过自身父作用域,也就是全局作用域的环境记录中寻找,找到c的值为'c',输出'c'。

5. 作用域链

通过上文阐述的各个知识点,作用域链就很好理解了,在ES3中就是执行上下文里其变量对象VO + 自身父作用域,然后每个执行上下文依次串联出一条链路所形成的就是作用域链。

而在ES6中就是执行上下文里的词法环境里的环境记录+外部环境引用。外部环境引用依次串联也会形成一条链路,也属于作用域链。

它的作用在于变量的查找路径。当代码执行时,遇到一个变量就会通过作用域链不断回溯,直到找到该值又或者是到了全局作用域这顶层还是不存在,则会报错。

以及之后关于闭包的产生,也是由于作用域链的存在所导致的。这会在之后的复习里涉及到。

6. 一些练习

6.1 自己设计一道简单的练习题

var a = 10let b = 20const c = {  d: 30}function foo() {  console.log(a)  let e = 50
  return b + e
  a = 40}function bar() {  console.log(f)  var f = 60
  let a = 70
  console.log(f)  return a + c.d}if (a <= 30) {  console.log(a)  let b = foo()  console.log(b)
} 

console.log(b)
c.d = bar()console.log(a)console.log(c.d)

【相关推荐:JavaScript视频教程web前端

위 내용은 범위 체인의 JavaScript 상세 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 juejin.im에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제