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

JavaScript スコープの詳細な分析 (コード例)

不言
不言転載
2018-11-23 14:54:051466ブラウズ

この記事の内容は Javascript スコープの詳細な分析 (コード例) です。必要な方は参考にしていただければ幸いです。

スコープ

スコープは、変数 (識別子) をどこでどのように見つけるかを決定する一連のルールです。検索の目的が変数に値を割り当てることである場合は LHS クエリが使用され、変数の値を取得することである場合は RHS クエリが使用されます。代入演算子は LHS クエリを引き起こします。 = 演算子、または関数呼び出し時にパラメーターを渡す操作を行うと、関連付けられたスコープで代入操作が行われます。

JavaScript エンジンは、実行前にまずコードをコンパイルします。このプロセスでは、var a = 2 のようなステートメントが 2 つの別々のステップに分割されます。

  1. 最初。 , var a は、そのスコープ内で新しい変数を宣言します。これは、コードが実行される前の最初の段階で発生します。

  2. 次に、 a = 2 は変数 a をクエリ (LHS クエリ) し、それに値を割り当てます。

LHS クエリと RHS クエリは現在の実行スコープで開始され、必要に応じて (つまり、必要な識別子が見つからない場合)、上位スコープの検索を続けます。ターゲット識別子は、スコープが 1 レベル (1 フロア) 上がって、最終的にグローバル スコープ (最上位) に到達するたびに、見つかっても見つからなくても停止します。

RHS 参照が失敗すると、ReferenceError 例外がスローされます。 LHS 参照が失敗すると、LHS 参照のターゲットを識別子として使用するグローバル変数 (非厳密モードの場合) が自動的かつ暗黙的に作成されるか、ReferenceError 例外 (厳密モードの場合) が発生します。

字句スコープ

字句スコープとは、コードを記述するときの関数宣言の位置によってスコープが決定されることを意味します。コンパイルの字句解析段階では基本的に、すべての識別子がどこでどのように宣言されているかがわかっているため、実行中に識別子がどのように検索されるかを予測できます。

JavaScript には、字句スコープを「チート」する 2 つのメカニズムがあります: eval(..) と with です。前者は、1 つ以上の宣言を含む「コード」の文字列を評価し、それによって既存の字句スコープを (実行時に) 変更します。後者は基本的に、オブジェクトへの参照をスコープとして扱い、オブジェクトのプロパティをスコープ内の識別子として扱うことによって、新しい字句スコープを (やはり実行時に) 作成します。

これら 2 つのメカニズムの副作用は、エンジンがコンパイル時にスコープ ルックアップを最適化できないことです。これは、エンジンがそのような最適化を無効であると慎重に判断することしかできないためです。これらのメカニズムのいずれかを使用すると、コードの実行が遅くなります。使用しないでください。

JavaScript は字句スコープを使用するため、関数のスコープは関数の定義時に決定されます。

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();//local scope
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();//local scope
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}

var foo = checkscope();
foo();//local scope

関数式と関数宣言

関数宣言:関数関数名(パラメータ:任意){関数本体}
関数式:function 関数名 (オプション) (パラメータ: オプション){関数本体}

識別:

  • 関数名が宣言されていない場合は、次のようにする必要があります。表現。

  • 関数 foo(){} が代入式の一部である場合、それは関数 foo(){} が関数本体に含まれている場合、または関数式です。プログラムの先頭は関数宣言です。

  • (関数 foo(){}) は括弧で囲まれています。これが式である理由は、括弧 () がグループ化演算子であり、その中には式のみを含めることができるためです。

  function foo(){} // 声明,因为它是程序的一部分

  (function(){
    function bar(){} // 声明,因为它是函数体的一部分
  })();

  var bar = function foo(){}; // 表达式,因为它是赋值表达式的一部分
  new function bar(){}; // 表达式,因为它是new表达式
  (function foo(){}); // 表达式:包含在分组操作符内
  
  try {
    (var x = 5); // 分组操作符,只能包含表达式而不能包含语句:这里的var就是语句
  } catch(err) {
    // SyntaxError
  }
  • 関数宣言は条件文で使用できますが、標準化されていません。関数式を使用することをお勧めします。

    #関数宣言は変数宣言をオーバーライドしますが、変数割り当てはオーバーライドしません
  • function value(){
        return 1;
    }
    var value;
    alert(typeof value);    //"function"
    関数スコープとブロック スコープ
関数は、JavaScript ドメイン単位で最も一般的な役割です。本質的に、関数内で宣言された変数または関数は、それが配置されているスコープから「隠蔽」されます。これは、意図的な優れたソフトウェア設計原則です。ただし、関数だけがスコープの単位ではありません。

ブロック スコープとは、変数と関数が、それらが配置されているスコープに属するだけでなく、特定のコード ブロック (通常は { .. } 内) にも属することができることを意味します。

ES3 以降、try/catch コンストラクトの catch 句にはブロック スコープが含まれます。

let キーワード (var キーワードの親戚) は、コードの任意のブロックで変数を宣言するために ES6 で導入されました。

if(..) { let a = 2; } は、if { .. } ブロックをハイジャックする変数を宣言し、その変数をこのブロックに追加します。

ブロック スコープは関数スコープの完全な置き換えとして使用されるべきではないと考える人もいます。両方の関数が同時に存在する必要があります。開発者は、読みやすく保守しやすい優れたコードを作成するために、ニーズに応じて使用するスコープを選択できますし、選択する必要があります。

提升

我们习惯将 var a = 2; 看作一个声明,而实际上 JavaScript 引擎并不这么认为。它将 var a和 a = 2 当作两个单独的声明,第一个是编译阶段的任务,而第二个则是执行阶段的任务。

这意味着无论作用域中的声明出现在什么地方,都将在代码本身被执行前首先进行处理。可以将这个过程形象地想象成所有的声明(变量和函数)都会被“移动”到各自作用域的最顶端,这个过程被称为提升。

声明本身会被提升,而包括函数表达式的赋值在内的赋值操作并不会提升。
要注意避免重复声明,特别是当普通的 var 声明和函数声明混合在一起的时候,否则会引
起很多危险的问题!

var a;
if (!("a" in window)) {
    a = 1;
}
alert(a);

作用域闭包

通常,程序员会错误的认为,只有匿名函数才是闭包。其实并非如此,正如我们所看到的 —— 正是因为作用域链,使得所有的函数都是闭包(与函数类型无关: 匿名函数,FE,NFE,FD都是闭包), 这里只有一类函数除外,那就是通过Function构造器创建的函数,因为其[[Scope]]只包含全局对象。 为了更好的澄清该问题,我们对ECMAScript中的闭包作两个定义(即两种闭包):

ECMAScript中,闭包指的是:

从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
从实践角度:以下函数才算是闭包:
即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
在代码中引用了自由变量

循环闭包

for (var i=1; i<=5; i++) {
    (function(j) {
        setTimeout( function timer() {
        console.log( j );
        }, j*1000 );
    })( i );
}

for (var i=1; i<=5; i++) {
    let j = i; // 是的,闭包的块作用域!
    setTimeout( function timer() {
    console.log( j );
    }, j*1000 );
}

for (let i=1; i<=5; i++) {
    setTimeout( function timer() {
    console.log( i );
    }, i*1000 );
}
var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();//3
data[1]();//3
data[2]();//3

模块

模块有两个主要特征:(1)为创建内部作用域而调用了一个包装函数;(2)包装函数的返回
值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭
包。

现代模块机制

var MyModules = (function Manager() {
    var modules = {};
    function define(name, deps, impl) {
        for (var i=0; i<deps.length; i++) {
            deps[i] = modules[deps[i]];
        }
        modules[name] = impl.apply( impl, deps );
    }
    function get(name) {
        return modules[name];
    }
    return {
        define: define,
        get: get
    };
})();

未来模块机制

//bar.js
function hello(who) {
    return "Let me introduce: " + who;
}
export hello;
//foo.js
// 仅从 "bar" 模块导入 hello()
import hello from "bar";
var hungry = "hippo";
function awesome() {
    console.log(
        hello( hungry ).toUpperCase()
    );
}
export awesome;
baz.js
// 导入完整的 "foo" 和 "bar" 模块
module foo from "foo";
module bar from "bar";
console.log(
    bar.hello( "rhino" )
); // Let me introduce: rhino
foo.awesome(); // LET ME INTRODUCE: HIPPO

块作用域替代方案

Google 维护着一个名为 Traceur 的项目,该项目正是用来将 ES6 代码转换成兼容 ES6 之前的环境(大部分是 ES5,但不是全部)。TC39 委员会依赖这个工具(也有其他工具)来测试他们指定的语义化相关的功能。

{
    try {
        throw undefined;
    } catch (a) {
        a = 2;
        console.log( a );
    }
}
console.log( a )

上下文

EC(执行环境或者执行上下文,Execution Context)

EC={
    VO:{/* 函数中的arguments对象, 参数, 内部的变量以及函数声明 */},
    this:{},
    Scope:{ /* VO以及所有父执行上下文中的VO */}
}

ECS(执行环境栈Execution Context Stack)

//ECS=[Window]
A(//ECS=[Window,A]
    B(//ECS=[Window,A,B]
        //run B 
    )
    //ECS=[Window,A]
)
//ECS=[Window]

VO(变量对象,Variable Object)

var a = 10;
function test(x) {
  var b = 20;
};
test(30);
/*
VO(globalContext)
  a: 10,
  test: 
VO(test functionContext)
  x: 30
  b: 20
*/

AO(活动对象,Active Object)

function test(a, b) {
  var c = 10;
  function d() {}
  var e = function _e() {};
  (function x() {});
} 
test(10);
/*
AO(test) = {
  a: 10,
  b: undefined,
  c: undefined,
  d: <reference to FunctionDeclaration "d">
  e: undefined
};
*/

scope chain(作用域链)和[[scope]]属性

Scope = AO|VO + [[Scope]]

例子

var x = 10;
 
function foo() {
  var y = 20;
 
  function bar() {
    var z = 30;
    alert(x +  y + z);
  }
 
  bar();
}
 
foo(); // 60
  • 全局上下文的变量对象是:

globalContext.VO === Global = {
  x: 10
  foo: <reference to function>
};
  • 在“foo”创建时,“foo”的[[scope]]属性是:

foo.[[Scope]] = [
  globalContext.VO
];
  • 在“foo”激活时(进入上下文),“foo”上下文的活动对象是:

fooContext.AO = {
  y: 20,
  bar: <reference to function>
};
  • “foo”上下文的作用域链为:

fooContext.Scope = fooContext.AO + foo.[[Scope]] // i.e.:
 
fooContext.Scope = [
  fooContext.AO,
  globalContext.VO
];
  • 内部函数“bar”创建时,其[[scope]]为:

bar.[[Scope]] = [
  fooContext.AO,
  globalContext.VO
];
  • 在“bar”激活时,“bar”上下文的活动对象为:

barContext.AO = {
  z: 30
};
  • “bar”上下文的作用域链为:

barContext.Scope = barContext.AO + bar.[[Scope]] // i.e.:
 
barContext.Scope = [
  barContext.AO,
  fooContext.AO,
  globalContext.VO
];
  • 对“x”、“y”、“z”的标识符解析如下:

- "x"
-- barContext.AO // not found
-- fooContext.AO // not found
   globalContext.VO // found - 10

- "y"
-- barContext.AO // not found
   fooContext.AO // found - 20

- "z"
   barContext.AO // found - 30

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

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