ホームページ  >  記事  >  ウェブフロントエンド  >  JavaScript のスコープの詳細な分析

JavaScript のスコープの詳細な分析

青灯夜游
青灯夜游転載
2021-05-31 17:28:372061ブラウズ

この記事では、JavaScript のスコープについて詳しく理解します。一定の参考値があるので、困っている友達が参考になれば幸いです。

JavaScript のスコープの詳細な分析

この記事はメモと呼ぶのが適切であり、内容は「あなたが知らない JavaScript (第 1 巻)」の「スコープとクロージャ」の最初の部分から抜粋したものです。非常によく語られており、見る価値があります。

スコープとは

スコープは、名前で変数を検索するための一連のルールです

スコープについて理解する

まず、いくつかの基本的な概念を理解します。

  • エンジン: JavaScript プログラム全体のコンパイルと実行を最初から担当します。プロセスを終了します。
  • コンパイラ: 構文分析とコード生成を担当します。この部分では、JavaScript コードがどのように実行されるのかも確認できます。
  • スコープ: 宣言されたすべての識別子 (変数) で構成される一連のクエリを収集および維持し、一連の非常に厳格なルールを実装します。これらの識別子に対する現在実行中のコードのアクセス権を決定します。

次のコードの実行プロセスを見てみましょう:

var a = 2;
  • var a に遭遇すると、コンパイラは次の質問をします。 スコープ 変数 a が同じスコープ コレクション内に存在するかどうか。存在する場合、コンパイラはその宣言を無視してコンパイルを続行します。それ以外の場合、スコープは現在のスコープ コレクションで新しい変数を宣言し、その変数に a

  • # という名前を付けるように求められます。 # #Next

    コンパイラは、エンジンが a = 2 の代入演算を処理するために実行時に必要なコードを生成します。エンジンの実行中は、まず変数 a が現在のスコープ セットに存在するかどうかをスコープに問い合わせます。存在する場合、エンジンはその変数を使用します。変数が存在しない場合、エンジンは変数を探し続けます

  • If

    エンジンが変数を見つけた場合、2 を割り当てます。そうしないと、エンジンがエラーをスローします。

概要: 変数の代入操作では 2 つのアクションが実行されます。まず、コンパイラが現在のスコープで変数を宣言し、次にランタイム エンジンがその変数を検索します。スコープ変数。見つかった場合は値を割り当てます。

コンパイラはコンパイル プロセスの 2 番目のステップでコードを生成し、エンジンがそのコードを実行すると、変数

a を検索してコードが宣言されているかどうかを判断します。検索プロセスはスコープによって支援されますが、エンジンが検索をどのように実行するかが最終的な検索結果に影響します。

この例では、エンジンは変数 a に対して LHS クエリを実行し、他のタイプの検索は RHS と呼ばれます。 「L」と「R」はそれぞれ代入演算の左辺と右辺を表します。変数が代入の左側に表示される場合は LHS クエリが実行され、変数が右側に表示される場合は RHS クエリが実行されます。

LHS: 値を割り当てることができるように、変数自体のコンテナを検索してみます。RHS: 単に変数の値を検索します。
console.log(a);
ここでは a にタスク値が割り当てられていないため、a への参照は RHS 参照です。したがって、値をコンソールに渡すためには、a の値を検索して取得する必要があります。 log(...)

a = 2;

ここでの a への参照は LHS 参照です。実際には、現在の値が何であるかは気にせず、代入操作のターゲット = 2 を見つけたいだけであるためです。 。

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

foo(2);

    最後の行の foo 関数の呼び出しには、foo への RHS 参照が必要です。foo の値を見つけて、それを私に渡します。
  1. 暗黙的な a = 2 演算コード内で可能です。見落としがちですが、この操作は 2 がパラメーターとして
  2. foo 関数に渡されたときに発生し、2 はパラメーター a# に割り当てられます。 ##、パラメータを与えるためにa LHS クエリを必要とする値を (暗黙的に) 割り当てます。 a への RHS 参照もあり、取得された値は
  3. console.log(...)
  4. に渡されます。 console.log(...) 自体も実行するには参照が必要なので、コンソール オブジェクト RHS にクエリを実行し、取得した値のいずれかが ## と呼ばれるかどうかを確認します。 #log メソッド。 RHS クエリがすべてのネストされたスコープで必要な変数を見つけることができない場合、エンジンは ReferenceError 例外をスローします。 RHS クエリ中に変数が見つかりましたが、この変数の値に対して無理な操作 (null または未定義の型値のプロパティを参照する非関数型値を呼び出そうとするなど) を実行しようとすると、エンジンがwill 別の型の例外 TypeError をスローします。
  5. LHS クエリの実行時にエンジンが変数を見つけられない場合、グローバル スコープ内に変数が作成されます。ただし、厳密モードでは、グローバル変数は自動的に作成されず、ReferenceError 例外がスローされます。次のように :###

    作用域是一套规则,用于确定在哪里找,怎么找到某个变量。如果查找的目的是对变量进行赋值,那么就会使用 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="https://img.php.cn/upload/image/252/331/635/1622453204885970.png" title="1622453204885970.png" alt="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 = &#39;外部的a&#39;;
function foo() {
    var a = &#39;foo内部的a&#39;;
    console.log(a); // foo内部的a
}
foo();

作用域与执行上下文

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

解释阶段

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

执行阶段

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

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

总结

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

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

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

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

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

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

以上がJavaScript のスコープの詳細な分析の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はsegmentfault.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。