国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

Lexical environments: Common Theory

羅志環 / 3381人閱讀

摘要:調用棧意味著中的執行環境棧,激活記錄意味著的激活對象。此外,所有的函數是一等公民。換句話說,自由變量并不存在自身的環境中,而是周圍的環境中。值得注意的是,函數并沒有用到自由變量。在后面的情形中,我們將綁定對象稱為環境幀。

原文

ECMA-262-5 in detail. Chapter 3.1. Lexical environments: Common Theory.

簡介

在這一章,我們將討論詞法環境的細節——一種被很多語言應用管理靜態作用域的機制。為了能夠更好地理解這個概念,我們也會討論一些別的——動態作用域(ECMAScript中并沒有直接使用)。我們將看到環境是如何管理嵌套的代碼結構和閉包。ECMA-262-5規范介紹了詞法環境,盡管這是一個和ECMAScript相獨立的概念,被應用于很多函數。實際上,部分和這個話題相關的技術部分,我們已經在之前的ES3系列中討論過了,例如變量和激活對象,作用域鏈。嚴格地來說,詞法環境相比ES3中的概念只是更加理論化,更加抽象。但這是一個ES5的年代,我建議用這些新的定義來討論和解釋ECMAScript。盡管,更加普遍的概念,例如激活記錄(activation record)(ES3中的激活對象)的調用棧(call-stack)(ES中執行環境棧),等等,已經在低級抽象的層面上討論過了。這一章節致力于環境的一般理論,也會涉及程序語言理論(programming languages theory)的部分。我們將從不同的角度,用不同的語言實現,來理解為什么詞法作用域是需要的,以及這些結構是如何被創建的。事實上,如果我們完全理解來作用域的一般理論,那些ES中的作用域問題也就消失了。

一般理論

ES中的概念(激活對象,作用域鏈,詞法環境)都與作用域的概念相關。ES中提到的定義是作用域的一種本地實現,及相關術語。

作用域

作用域是用來在程序的不同部分中管理變量的可見行和可訪問性。一些
封裝的抽象概念(例如命名空間,模塊)都和作用域相關,作用域被用來使系統更加模塊化以及避免命名變量的沖突。函數有本地變量,代碼塊有本地變量,作用域封裝了內在的數據,提升了抽象程度。作用域使我們在一個程序中使用相同的變量,但是代表不同的含義,擁有不同的值。從這個角度來說,作用域是個閉合的上下文,里面的變量都與值相關聯。我們也可以說,作用域是某個變量它某個含義的邏輯邊界。例如,全局變量,局部變量等,都反映了這個變量的聲明周期。代碼塊和函數讓我們擁有了一個主要的作用域的屬性——嵌套其他作用域或者被嵌套。因此,我們可以看到并不是所有的實現都支持函數嵌套,同樣不是所有的實現都提供塊級作用域。讓我們來考慮以下的C代碼:

// global "x"
int x = 10;
 
void foo() {
   
  // local "x" of "foo" function
  int x = 20;
 
  if (true) {
    // local "x" of if-block
    int x = 30;
    printf("%d", x); // 30
  }
 
  printf("%d", x); // 20
 
}
 
foo();
 
printf("%d", x); // 10

它可以被下圖表示

ECMAScript在版本6之前并不支持塊級作用域

var x = 10;
 
if (true) {
  var x = 20;
  console.log(x); // 20
}
 
console.log(x); // 20

ES6標準中let關鍵字可以創建塊級變量

let x = 10;
if (true) {
  let x = 20;
  console.log(x); // 20
}
 
console.log(x); // 10

這個塊級作用域可以通過匿名自調用函數來實現

var x = 10;
 
if (true) {
  (function (x) {
    console.log(x); // 20
  })(20);
}
 
console.log(x); // 10
靜態(詞法)作用域

在靜態作用域中,標識符指向最近的詞法環境。單詞“lexical”在這個場合下指的是程序書寫的屬性,詞法上變量出現的源文字,變量被聲明的地方。在那個作用域中,變量將會在運行時被解析。單詞“static”意味著決定標識符作用域是在程序的詞法分析(parsing)的過程中。這也就是說,在程序啟動之前,我們通過閱讀代碼,就能判斷在哪個作用域下,變量將被解析。舉個例子

var x = 10;
var y = 20;
 
function foo() {
  console.log(x, y);
}
 
foo(); // 10, 20
 
function bar() {
  var y = 30;
  console.log(x, y); // 10, 30
  foo(); // 10, 20
}
 
bar();

在這個例子中,變量x在全局變量中被定義——意味著,運行時,它也將在全局對象中被解析。變量y有兩個定義,我們說過,考慮擁有變量最近的詞法作用域。變量自身所在的作用域擁有最高的優先級。因此,在bar函數中,變量y被解析為30。bar函數中局部變量y覆蓋來同名的全局變量y。但是,同名變量y在foo函數中依然被解析為20,即使它在bar函數的內部被調用,而且在bar函數內部還有變量y。變量的解析和環境的調用是相互獨立的(in this case bar is a caller of foo, and foo is a callee)。因為foo函數被定義的位置,最近的含有變量y的詞法環境就是全局環境。如今,靜態作用域已經被很多語言應用:C, Java, ECMAScript, Python, Ruby, Lua等等。

動態作用域

動態作用域并不在詞法環境中解析變量,而是動態形成變量棧。每當碰到變量聲明,就把變量放進棧中。變量的聲明周期結束時,將變量從棧中彈出。來看一段偽代碼。

// *pseudo* code - with dynamic scope
 
y = 20;
 
procedure foo()
  print(y)
end
 
 
// on the stack of the "y" name
// currently only one value 20
// {y: [20]}
 
foo() // 20, OK
 
procedure bar()
 
  // and now on the stack there
  // are two "y" values: {y: [20, 30]};
  // the first found (from the top) is taken
 
  y = 30
 
  // therefore:
  foo() // 30!, not 20
 
end
 
bar()

環境的調用影響了變量的解析。[譯者注:不是重點不譯了]

名稱綁定

在高級語言中,我們不在操作地址,這個地址指向內存中的數據,我們直接使用變量名來指代那些數據。名稱綁定是標識符和對象的關聯。一個標識符可以綁定或解綁。如果標識符被綁定了個對象,那么它就指向這個對象。

重新綁定

// bind "foo" to {x: 10} object
var foo = {x: 10};

console.log(foo.x); // 10

// bind "bar" to the same object
// as "foo" identifier is bound

var bar = foo;

console.log(foo === bar); // true
console.log(bar.x); // OK, also 10

// and now rebind "foo"
// to the new object

foo = {x: 20};

console.log(foo.x); // 20

// and "bar" still points
// to the old object

console.log(bar.x); // 10
console.log(foo === bar); // false

可變性
// bind an array to the "foo" identifier
var foo = [1, 2, 3];
 
// and here is a *mutation* of
// the array object contents
foo.push(4);
 
console.log(foo); // 1,2,3,4
 
// also mutations
foo[4] = 5;
foo[0] = 0;
 
console.log(foo); // 0,2,3,4,5

環境

在這一部分,我們將提到詞法作用域實現的技術。我們將操作更抽象的實體,討論詞法作用域,在以后的解釋中,我們將用環境而不是作用域,因為ES5中也是這個術語,全局環境,函數的本地環境等等。正如我們提到的,環境說明了表達式中標識符的含義。ECMAScript用調用棧(call-stack)來管理函數的執行。來考慮一些通用的模型來保存變量。一些事情很有趣,有閉包的系統和沒有閉包的系統。

激活記錄模型

如果沒有一等函數,或者不允許內部函數,最簡單存儲本地變量的方式就是調用棧本身。一個特殊的調用棧的數據結構叫做激活記錄(activation record),被用來保存環境綁定。有時候也叫調用棧幀(call-stack frame)。每當函數被調用,一個激活記錄(包含參數和本地變量)被壓入棧中。因此,當函數調用其他函數,另一個棧幀被壓入棧中。當上下文結束來,激活記錄從棧中彈出,意味這所有本地變量被銷毀。這個模型在c語言中被使用。
例如

void foo(int x) {
  int y = 20;
  bar(30);
}
 
void bar(x) {
  int z = 40;
}
 
foo(10);

調用棧會有如下變化

callStack = [];
 
// "foo" function activation
// record is pushed onto the stack
 
callStack.push({
  x: 10,
  y: 20
});
 
// "bar" function activation
// record is pushed onto the stack
 
callStack.push({
  x: 30,
  z: 40
});
 
// callStack at the moment of
// the "bar" activation
 
console.log(callStack); // [{x: 10, y: 20}, {x: 30, z: 40}]
 
// "bar" function ends
callStack.pop();
 
// "foo" function ends
callStack.pop();

當bar函數被調用時

很多相似的函數執行的邏輯方法在ECMAScript被使用。然后,有些很重要的不同。調用棧意味著ES中的執行環境棧,激活記錄意味著ES3的激活對象。和C不同的是,ECMAScript不會從內存中移除激活對象如果有個閉包。當這個閉包是個內部函數,是用來外部函數中創建的變量,然后這個內部函數被返回到了外面。這就意味著激活對象不應該存在棧中,而是堆中(動態分配內存)。它會一直被保存,當閉包的引用使用激活對象中的變量。更重要的是,不僅是一個激活對象被保存,如果需要,所有的父級的激活對象。

var bar = (function foo() {
  var x = 10;
  var y = 20;
  return function bar() {
    return x + y;
  };
})();
 
bar(); // 30

如果foo函數創建了一個閉包,即使foo執行結束了,它的幀不會從內存中移除,因為閉包中有它的引用。

環境幀模型

和c不同,ECMAScript含有內部函數和閉包。此外,所有的函數是一等公民。

一等函數

一等函數被當作普通的對象,可以被作為參數,可以作為返回值。一個簡單的例子

// create a function expression
// dynamically at runtime and
// bind it to "foo" identifier
 
var foo = function () {
  console.log("foo");
};
 
// pass it to another function,
// which in turn is also created
// at runtime and called immediately
// right after the creation; result
// of this function is again bound
// to the "foo" identifier
 
foo = (function (funArg) {
 
  // activate the "foo" function
  funArg(); // "foo"
 
  // and return it back as a value
  return funArg;
 
})(foo);
函數參數和高階函數

當一個函數被作為參數,稱之為“funarg”-- functional argument的縮寫。將函數作為參數的函數稱為高階函數(higher-order function),和數學上的算子概念相似。

自由變量

自由變量是函數中使用的變量,既不是函數的參數,也不是函數的本地變量。換句話說,自由變量并不存在自身的環境中,而是周圍的環境中。

// Global environment (GE)
 
var x = 10;
 
function foo(y) {
 
  // environment of "foo" function (E1)
 
  var z = 30;
 
  function bar(q) {
    // environment of "bar" function (E2)
    return x + y + z + q;
  }
 
  // return "bar" to the outside
  return bar;
 
}
 
var bar = foo(20);
 
bar(40); // 100

在這個例子中,我們有三個環境:GE,E1和E2,分別對應于全局對象,foo函數和bar函數。因此,對于bar函數來說,變量x,y,z是自由變量,它們既不是函數參數,也不是bar的本地變量。值得注意的是,foo函數并沒有用到自由變量。但是變量x在內部的bar函數中被使用,另外在foo函數運行的過程中創建bar函數,盡管如此,還是保存了父環境的綁定,為了能夠將x的綁定傳遞給內部嵌套的函數。正確的并期望出現100,當執行bar函數后,這意味著bar函數記住了foo函數激活時的環境,即使foo函數已經結束了。這就是與基于棧的激活記錄模型的不同。當我們允許內部函數,同時希望靜態詞法作用域,同時將函數作為一等公民,我們應該保存函數需要的所有自由變量,當函數被創建的時候。

環境定義

最直接最簡單的方法去實現這樣的算法是保存完整的父環境,在這個父環境中,函數被創建。然后,在函數自己執行時,我們創建自己的環境,保存自己的本地變量和參數,然后設置我們的外部環境為之前保存的那個,為了能夠在那找到自由變量。我們用術語環境指代多帶帶綁定對象,或者所有的綁定對象依據嵌套的深度。在后面的情形中,我們將綁定對象稱為環境幀。一個環境是一些列幀,每個幀是個記錄綁定,將變量名和值關聯起來。我們用抽象的概念記錄,而沒有具體說明它的實現結構,它可能是堆中的哈希表,棧內存,虛擬機注冊(registers of the virtual machine)等。例如,例子中,環境E2有三個幀:自己的bar,foo的和全局的。環境E1有兩個幀:foo自己的,和全局的。全局環境GE只有一個幀:全局。

一個幀中任何變量至多只有一個綁定。每個幀都有個指針,指向圍繞它的環境。全局幀的外部環境的鏈接是null。變量相對于環境的值是在包含該變量的綁定的環境中的第一幀中的變量的綁定給出的值(The value of a variable with respect to an environment is the value given by the binding of the variable in the first frame in the environment that contains a binding for that variable)(源自谷歌翻譯)。

var x = 10;
 
(function foo(y) {
   
  // use of free-bound "x" variable
  console.log(x);
 
  // own-bound "y" variable
  console.log(y); // 20
   
  // and free-unbound variable "z"
  console.log(z); // ReferenceError: "z" is not defined
 
})(20);

一系列的環境幀形成了我們所稱的作用域鏈。一個環境可能會包裹多個內部環境。

// Global environment (GE)
 
var x = 10;
 
function foo() {
 
  // "foo" environment (E1)
 
  var x = 20;
  var y = 30;
 
  console.log(x + y);
 
}
 
function bar() {
   
  // "bar" environment (E2)
 
  var z = 40;
 
  console.log(x + z);
}

偽代碼

// global
GE = {
  x: 10,
  outer: null
};
 
// foo
E1 = {
  x: 20,
  y: 30,
  outer: GE
};
 
// bar
E2 = {
  z: 40,
  outer: GE
};

變量x相對于環境E1的綁定遮蔽了同名變量在全局環境中的綁定。

函數創建和應用法則

一個函數是相對于給定的環境創建的。這導致函數對象是由函數本身的代碼(函數體)和指向創建函數本身的環境的指針構成。

// global "x"
var x = 10;
 
// function "foo" is created relatively
// to the global environment
 
function foo(y) {
  var z = 30;
  console.log(x + y + z);
}

相當于偽代碼

// create "foo" function
 
foo = functionObject {
  code: "console.log(x + y + z);"
  environment: {x: 10, outer: null}
};

注意,函數指向它的環境,其中一個環境與函數自身相綁定。一個函數被調用,一系列的參數構成了新的幀,在這個幀中綁定了本地變量,然后在創建的新的環境中執行函數體。

// function "foo" is applied
// to the argument 20
 
foo(20);

與之對應的偽代碼

// create a new frame with formal 
// parameters and local variables
 
fooFrame = {
  y: 20,
  z: 30,
  outer: foo.environment
};
 
// and evaluate the code
// of the "foo" function 
 
execute(foo.code, fooFrame); // 60

閉包

閉包是由函數代碼和創建函數的環境構成的。閉包被發明用來解決函數參數的問題。

函數參數問題

當返回一個函數到外面,這個函數使用了創建它的父環境中的自由變量怎么辦?

(function (x) {
  return function (y) {
    return x + y;
  };
})(10)(20); // 30

正如我們所知,詞法作用域在堆中保存封閉的幀。這是問題的關鍵。像c一樣用棧來保存綁定是不可行的。被保存的代碼塊和環境就是個閉包。當我們把函數作為參數傳遞到其他函數中,這個函數參數中的自由變量是如何被解析的,是在函數定義的作用域中,還是函數執行的作用域中?

var x = 10;
 
(function (funArg) {
 
  var x = 20;
  funArg(); // 10, not 20
 
})(function () { // create and pass a funarg
  console.log(x);
});

回答這個問題的關鍵就是詞法作用域。

組合環境幀模型

很明顯,如果一些變量不被內部函數需要,就沒有必要保存它們。

// global environment
 
var x = 10;
var y = 20;
 
function foo(z) {
 
  // environment of "foo" function
  var q = 40;
 
  function bar() {
    // environment of "bar" function
    return x + z;
  }
 
  return bar;
 
}
 
// creation of "bar"
var bar = foo(30);
 
// applying of "bar"
bar();

沒有函數使用變量y,因此,我們不需要在foo和bar的閉包中保存它。全局變量x沒有在函數foo中使用,但是我們仍應該保存它,因為更深的內部函數bar需要它。

bar = closure {
  code: <...>,
  environment: {
    x: 10,
    z: 30,
  }
}

[譯者注:后面是python等其他語言閉包的例子,不譯了]

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。

轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/89602.html

相關文章

  • JavaScript 進階 從實現理解閉包(校對版)

    摘要:嵌套函數在一個函數中創建另一個函數,稱為嵌套。這在很容易實現嵌套函數可以訪問外部變量,幫助我們很方便地返回組合后的全名。更有趣的是,嵌套函數可以作為一個新對象的屬性或者自己本身被。 來源于 現代JavaScript教程閉包章節中文翻譯計劃本文很清晰地解釋了閉包是什么,以及閉包如何產生,相信你看完也會有所收獲 關鍵字Closure 閉包Lexical Environment 詞法環境En...

    wuyangnju 評論0 收藏0
  • Lexical environments: ECMAScript implementation

    摘要:全局環境是作用域鏈的終點。環境記錄類型定義了兩種環境記錄類型聲明式環境記錄和對象環境記錄聲明式環境記錄聲明式環境記錄是用來處理函數作用域中出現的變量,函數,形參等。變量環境變量環境就是存儲上下文中的變量和函數的。解析的過程如下 原文 ECMA-262-5 in detail. Chapter 3.2. Lexical environments: ECMAScript implement...

    roadtogeek 評論0 收藏0
  • 談談Javascript中的delete操作符

    摘要:你覺得下列代碼中,哪些操作能成功人肉判斷一下,不要放進瀏覽器里執行。故對于解析而言,得到的為上述所有的屬性在下均為。那么又有什么玄機呢的操作可理解為對于,調用其內部的方法。幾乎所有的都是不可刪除的。 你覺得下列代碼中,哪些delete操作能成功?人肉判斷一下,不要放進瀏覽器里執行。 // #1 a = hello world; delete a; // #2 var b = hel...

    antz 評論0 收藏0
  • 溫故而知新:JS 變量提升與時間死區

    摘要:這意味著引擎在源代碼中聲明它的位置計算其值之前,你無法訪問該變量。因此它們同樣會受到時間死區的影響。正確理解提升機制將有助于避免因變量提升而產生的任何未來錯誤和混亂。 開始執行腳本時,執行腳本的第一步是編譯代碼,然后再開始執行代碼,如圖 showImg(https://segmentfault.com/img/bVbnPrh?w=1233&h=141); 另外,在編譯優化方面來說,最開...

    codecraft 評論0 收藏0
  • [譯] 透過重新實作來學習參透閉包

    摘要:不過到底是怎麼保留的另外為什麼一個閉包可以一直使用區域變數,即便這些變數在該內已經不存在了為了解開閉包的神秘面紗,我們將要假裝沒有閉包這東西而且也不能夠用嵌套來重新實作閉包。 原文出處: 連結 話說網路上有很多文章在探討閉包(Closures)時大多都是簡單的帶過。大多的都將閉包的定義濃縮成一句簡單的解釋,那就是一個閉包是一個函數能夠保留其建立時的執行環境。不過到底是怎麼保留的? 另外...

    CoXie 評論0 收藏0

發表評論

0條評論

羅志環

|高級講師

TA的文章

閱讀更多
最新活動
閱讀需要支付1元查看
<