|
범위는 엔진이 현재 범위와 중첩 범위의 식별자를 기반으로 변수를 쿼리하는 방법을 정의하는 규칙 집합을 정의합니다. 반면, N개의 범위로 구성된 범위 체인은 함수 범위 내의 식별자로 찾은 값을 결정합니다.
다음과 같이 요약할 수 있습니다. 범위는 현재 컨텍스트에 정의된 변수의 가시성을 결정합니다. 즉, 하위 수준 범위가 해당 변수에 액세스할 수 있습니다. 그리고 스코프 체인(Scope Chain)은 현재 컨텍스트에서 식별자의 값을 찾는 방법도 결정합니다.
Scope는 Lexical Scope와 Dynamic Scope로 구분됩니다. 어휘 범위는 문자 그대로 어휘 단계에서 정의된 범위입니다. 즉, 어휘분석기가 소스 코드를 처리할 때 소스 코드 내 변수 및 블록의 위치를 기준으로 범위가 설정됩니다. JavaScript는 어휘 범위를 사용합니다.
변수에 대한 액세스 규칙:
변수 a가 함수 내부에 정의된 경우 함수 내부의 다른 변수는 변수 a에 액세스할 수 있는 권한이 있지만 함수 외부의 코드는 변수 a에 액세스할 수 있는 권한이 없습니다. 따라서 같은 범위에 있는 변수는 서로 접근할 수 있습니다. 즉, a, b, c는 같은 범위에 있으면 서로 접근할 수 있습니다. 마치 엄마 닭이 새끼를 낳은 것과 같습니다. 새끼 닭들은 서로 놀 수 있지만 다른 닭들은 함께 놀 수 없습니다. 왜일까요? 마더치킨이 허락하지 않으니까~o(^∀^)o.
let a = 1
function foo () {
let b = 1 + a
let c = 2
console.log(b) // 2
}
console.log(c) // error 全局作用无法访问到 c
foo()
변수 a가 전역 범위(window/global)에 정의된 경우 전역 범위 아래 로컬 범위에서 실행된 코드 또는 expression은 변수 a의 값에 접근할 수 있습니다. 지역 변수에서 동일한 이름(a)을 가진 변수는 전역 변수 a에 대한 액세스를 자릅니다. (여기서 변수 a는 사육자와 동일하며, 사육자는 적절한 시기에 닭에게 먹이를 주게 된다. 그러나 비용 절감을 위해 사육자는 근처에 있는 닭에게 먹이를 주어야 한다고 사육자는 규정한다. 사육자 1이 닭을 떠나면 , 최근에는 다른 사육자들이 닭에게 먹이를 주기 위해 압록강을 건너는 여행을 할 수 없게 될 것입니다)
let a = 1
let b = 2
function foo () {
let b = 3
function too () {
console.log(a) // 1
console.log(b) // 3
}
too()
}
foo()
다시 한번, JavaScript 범위는 액세스 가능한 변수 범위를 엄격하게 제한합니다. 즉, 위치를 기준으로 합니다. 소스 코드의 코드 및 블록 중 중첩 범위는 중첩 범위에 액세스할 수 있습니다. (이 규칙은 팜 전체에 규칙이 있어 반대 방향으로 공급할 수 없음을 나타냅니다.)
Scope Chain
스코프 체인은 현재 환경과 상위 환경의 일련의 스코프로 구성됩니다. 현재 실행 환경에서 접근 권한을 준수하는 변수 및 함수에 접근합니다.
위의 설명은 약간 모호합니다. 저처럼 뇌가 약한 사람은 머리 속으로 여러 번 '읽어야' 이해할 수 있습니다. 그렇다면 스코프 체인은 무엇을 위한 것일까요? 간단히 말해서 범위 체인은 식별자를 구문 분석하고 표현식이 실행될 때 해당 표현식이 의존하는 변수의 값을 반환하는 역할을 합니다. 더 간단한 대답: 범위 체인은 변수를 찾는 데 사용됩니다. 범위 체인은 직렬로 연결된 일련의 범위입니다.
함수 실행 중에 변수가 발견될 때마다 식별자 확인 프로세스를 거쳐 데이터를 가져오고 저장할 위치를 결정합니다. 이 과정은 현재 실행 중인 함수의 범위인 스코프 체인의 선두부터 시작하여(아래 그림의 왼쪽부터), 동일한 이름의 식별자를 찾으면 해당 식별자에 해당하는 값을 찾는다. 찾을 수 없으면 계속해서 범위 체인의 다음 범위를 검색합니다. 범위가 없으면 식별자가 정의되지 않은 것으로 간주됩니다. 함수 실행 중에 구문 분석할 가치가 있는 각 식별자는 이 검색 프로세스를 거쳐야 합니다.
문제를 구체적으로 분석하기 위해 스코프 체인은 배열(Scope Array)이고, 배열 구성원은 일련의 변수 개체로 구성되어 있다고 가정할 수 있습니다. 배열의 단방향 채널을 사용할 수 있습니다. 즉, 위 그림은 변수 객체의 식별자를 왼쪽에서 오른쪽으로 쿼리하는 것을 시뮬레이션하므로 상위 범위의 변수에 액세스할 수 있습니다. 최상위 수준(전역 범위)으로 이동하고 일단 발견되면 검색이 중지됩니다. 따라서 내부 변수는 동일한 이름을 가진 외부 변수를 보호할 수 있습니다. 생각해 보세요. 변수를 내부에서 검색하지 않으면 전체 언어 설계가 N 복잡해질 것입니다(아기 닭이 먹이를 찾으려면 복잡한 규칙 세트를 설계해야 합니다)
여전히 위의 밤은 다음과 같습니다.
let a = 1
let b = 2
function foo () {
let b = 3
function too () {
console.log(a) // 1
console.log(b) // 3
}
too()
}
foo()
Scope 중첩된 구조는 다음과 같습니다:
栗子中,当 javascript 引擎执行到函数 too 时, 全局、函数 foo、函数 too 的上下文分别会被创建。上下文内包含它们各自的变量对象和作用域链(注意: 作用域链包含可访问到的上层作用域的变量对象,在上下文创建阶段根据作用域规则被收集起来形成一个可访问链),我们设定他们的变量对象分别为VO(global),VO(foo), VO(too)。而 too 的作用域链,则同时包含了这三个变量对象,所以 too 的执行上下文可如下表示:
too = {
VO: {...}, // 变量对象
scopeChain: [VO(too), VO(foo), VO(global)], // 作用域链
}
我们可以直接用scopeChain
来表示作用域链数组,数组的第一项scopeChain[0]为作用域链的最前端(当前函数的变量对象),而数组的最后一项,为作用域链的最末端(全局变量对象 window )。所有作用域链的最末端都为全局变量对象。
再举个栗子:
let a = 1
function foo() {
console.log(a)
}
function too() {
let a = 2
foo()
}
too() // 1
这个栗子如果对作用域的特点理解不透彻很容易以为输出是2。但其实最终输出的是 1。 foo() 在执行的时候先在当前作用域内查找变量 a 。然后根据函数定义时的作用域关系会在当前作用域的上层作用域里查找变量标识符 a,所以最后查到的是全局作用域的 a 而不是 foo函数里面的 a 。
变量对象、执行上下文会在后面介绍。
闭包
在 JavaScript 中,函数和函数声明时的词法作用域形成闭包。我们来看个闭包的例子
let a = 1
function foo() {
let a = 2
function too() {
console.log(a)
}
return too
}
foo()() // 2
这是一个闭包的栗子,一个函数执行后返回另一个可执行函数,被返回的函数保留有对它定义时外层函数作用域的访问权。foo()()
调用时依次执行了 foo、too 函数。too 虽然是在全局作用域里执行的,但是too定义在 foo 作用域里面,根据作用域链规则取最近的嵌套作用域的属性 a = 2。
再拿农场的故事做比如。农场主发现还有一种方法会更节约成本,就是让每个鸡妈妈作为家庭成员的‘饲养员’, 从而改变了之前的‘饲养结构’。
关于闭包会在后面的章节里也会有介绍。
从作用域链的结构可以发现,javascript
引擎在查找变量标识符时是依据作用域链依次向上查找的。当标识符所在的作用域位于作用域链的更深的位置,读写的时候相对就慢一些。所以在编写代码的时候应尽量少使用全局代码,尽可能的将全局的变量缓存在局部作用域中。
不加强记忆很容记错作用域与后面将要介绍的执行上下文的区别。代码的执行过程分为编译阶段和解释执行阶段。始终应该记住javascript
作用域在源代码的编码阶段就确定了,而作用域链是在编译阶段被收集到执行上下文的变量对象里的。所以作用域、作用域链都是在当前运行环境内代码执行前就确定了。这里暂且不过多的展开执行上下文的概念,可以关注后续文章。
相信看了本文案例你已经掌握了方法,更多精彩请关注php中文网其它相关文章!
推荐阅读:
PromiseA+的实现步骤详解
EasyCanvas绘图库在Pixeler项目开发中使用实战总结