Home > Article > Web Front-end > An in-depth analysis of scope in JavaScript
This article will give you an in-depth understanding of JavaScript scope. It has certain reference value. Friends in need can refer to it. I hope it will be helpful to everyone.
This article is more appropriately called notes. The content comes from the first part of "JavaScript You Don't Know (Volume 1)" Scope and Closure . Very well told and well worth watching.
Scope is a set of rules for finding variables by name.
First understand some basic concepts:
Let’s take a look at the execution process of the following code:
var a = 2;
When encountering var a, the compiler will ask Scope Whether variable a
exists in the same scope collection. If it exists, the compiler will ignore the declaration and continue compilation; otherwise, the scope will be asked to declare a new variable in the current scope collection and name it a
NextThe compiler will generate the code required at runtime for the engine to handle the assignment operation of a = 2. When the engine is running, it will first ask the scope whether the variable a
exists in the current scope set. If it is, the engine will use the variable; if it does not exist, the engine will continue to look for the variable
Ifthe engine finds a variable, it will assign 2 Give it or the engine throws an error.
Summary: The assignment operation of a variable will perform two actions. First, the compiler will declare a variable in the current scope, and then the runtime engine will search for the variable in the scope. Variable, assign a value to it if it can be found.
The compiler generates the code in the second step of the compilation process. When the engine executes it, it will determine whether it has been declared by looking for the variable a
. The search process is assisted by the scope, but how the engine performs the search will affect the final search result.
In our example, the engine will perform an LHS query for variable a, and the other type of search is called RHS. "L" and "R" represent the left and right sides of an assignment operation respectively. LHS queries are performed when the variable appears on the left side of the assignment, and RHS queries are performed when it appears on the right side.
LHS: Try to find the container of the variable itself so that it can be assigned a value; RHS: Simply find the value of a variable.
console.log(a);
The reference to a is an RHS reference, because a is not assigned a task value here. Accordingly, it is necessary to find and obtain the value of a, so that the value can be passed to console.log(...)
a = 2;
The reference to a here is an LHS reference, because in fact we don't care what the current value is, we just want to find the target for the assignment operation = 2.
funciton foo(a) { console.log(a) } foo(2);
foo
function, 2
will be assigned to the parameter a
, in order to give the parametera
Assigns the value (implicitly), requiring a LHS
query. console.log(...)
. console.log(...)
itself also needs a reference to execute, so it will query the console object RHS
and check whether one of the obtained values is called log
method. If the RHS query cannot find the required variables in all nested scopes, the engine will throw a ReferenceError exception. A variable is found during RHS query, but you try to perform unreasonable operations on the value of this variable, such as trying to call a non-function type value, which refers to a property in a null or undefined type value, then the engine will Throws an exception TypeError of another type.
If the engine cannot find the variable when executing the LHS query, it will create one in the global scope. However, in strict mode, a global variable is not automatically created, but a ReferenceError exception is thrown.
SupplementarySeveral common error types in JS
A brief summary is as follows :
作用域是一套规则,用于确定在哪里找,怎么找到某个变量。如果查找的目的是对变量进行赋值,那么就会使用 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
函数作用域是指在函数内声明的所有变量在函数体内始终是可见的。外部作用域无法访问函数内部的任何内容。
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="https://img.php.cn/upload/image/252/331/635/1622453204885970.png" title="1622453204885970.png" alt="An in-depth analysis of scope in JavaScript">
气泡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 = '外部的a'; function foo() { var a = 'foo内部的a'; console.log(a); // foo内部的a } foo();
JavaScript的执行分为:解释和执行两个阶段
作用域在函数定义时就已经确定了,而不是在函数调用时确定,但执行上下文是函数执行之前创建的。
作用域就是一套规则,用于确定在哪里找以及怎么找到某个变量。
词法作用域在你写代码的时候就确定了。JavaScript是基于词法作用域的语言,通过变量定义的位置就能知道变量的作用域。ES6引入的let和const声明的变量在块级作用域中。
声明提升是指声明会被视为存在与其所出现的作用域的整个范围内。
查找变量的时候会先从内部的作用域开始查找,如果没找到,就往上一级进行查找,依次类推。
作用域在函数定义时就已经确定了,执行上下文是函数执行之前创建的。
更多编程相关知识,请访问:编程视频!!
The above is the detailed content of An in-depth analysis of scope in JavaScript. For more information, please follow other related articles on the PHP Chinese website!