首頁  >  文章  >  web前端  >  Javascript作用域的深入解析(程式碼範例)

Javascript作用域的深入解析(程式碼範例)

不言
不言轉載
2018-11-23 14:54:051470瀏覽

這篇文章帶給大家的內容是關於Javascript作用域的深入解析(程式碼範例),有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。

作用域

作用域是一套規則,用於決定在何處以及如何尋找變數(標識符)。如果查找的目的是對變數進行賦值,那麼就會使用 LHS 查詢;如果目的是取得變數的值,就會使用 RHS 查詢。賦值操作符會導致 LHS 查詢。 = 操作符或呼叫函數時傳入參數的操作都會導致關聯作用域的賦值操作。

JavaScript 引擎會先在程式碼執行前編譯,在這個過程中,像var a = 2 這樣的宣告會被分解成兩個獨立的步驟:

  1. 首先, var a 在其作用域中宣告新變數。這會在最開始的階段,也就是程式碼執行前進行。

  2. 接下來, a = 2 會查詢(LHS 查詢)變數 a 並對其進行賦值。

LHS 和RHS 查詢都會在目前執行作用域中開始,如果有需要(也就是說它們沒有找到所需的識別碼),就會向上級作用域繼續尋找目標標識符,這樣每次上升一級作用域(一層樓),最後抵達全域作用域(頂層),無論找到或沒找到都將停止。

不成功的 RHS 引用會導致拋出 ReferenceError 例外。不成功的 LHS 參考會導致自動隱式地創建一個全域變數(非嚴格模式下),該變數使用 LHS 引用的目標作為標識符,或拋出 ReferenceError 異常(嚴格模式下)。

詞法作用域

詞法作用域意味著作用域是由書寫程式碼時函數宣告的位置來決定的。編譯的詞法分析階段基本上能夠知道全部標識符在哪裡以及是如何聲明的,從而能夠預測在執行過程中如何對它們進行查找。

JavaScript 中有兩個機制可以「欺騙」詞法作用域: eval(..) 和 with 。前者可以對一段包含一個或多個聲明的「代碼」字串進行演算,並藉此來修改已經存在的詞法作用域(在執行時)。後者本質上是透過將一個物件的引用當作作用域來處理,將物件的屬性當作作用域中的識別符來處理,從而創建了一個新的詞法作用域(同樣是在運行時)。

這兩個機制的副作用是引擎無法在編譯時對作用域查找進行最佳化,因為引擎只能謹慎地認為這樣的最佳化是無效的。使用這其中任何一個機制都會導致程式碼運行變慢。不要使用它們。

因為 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 函數名稱(參數:可選){ 函數體}
函數表達式:function 函數名稱(可選)(參數:可選){ 函數體}

#辨別:

  • 如果不宣告函數名稱,它肯定是表達式。

  • 如果function foo(){}是作為賦值表達式的一部分的話,那它就是一個函數表達式,如果function foo(){}被包含在一個函數體內,或者位於程式的最頂部的話,那它就是一個函數宣告。

  • 被括號括住的(function 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 分句中具有區塊作用域。

在 ES6 中引入了 let 關鍵字( var 關鍵字的表親),用來在任意程式碼區塊中宣告變數。
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中文網其他相關文章!

陳述:
本文轉載於:segmentfault.com。如有侵權,請聯絡admin@php.cn刪除