午夜视频在线网站,日韩视频精品在线,中文字幕精品一区二区三区在线,在线播放精品,1024你懂我懂的旧版人,欧美日韩一级黄色片,一区二区三区在线观看视频

分享

JavaScript變量作用域之殤

 quasiceo 2013-09-10

如果你愛上了JavaScript這門詭異的語言,那我相信你一定在與其戀愛期間飽受了其變量作用域所引發(fā)的一系列問題的不少摧殘。對于任何一門編程語言,變量作用域都是一個關(guān)切的話題。正如David Herman在《Effective JavaScript》中的形象比喻,“Scope is like oxygen to a programmer”。當(dāng)你“呼吸順暢”的時候,你并不會意識到變量作用域的重要性;然而當(dāng)你“呼吸受阻”的時候,你便會體會到它的輕重高低。

全局作用域

絕大多數(shù)編程語言都有全局作用域的概念。全局作用域是指常量、變量、函數(shù)等對象的作用范圍在整個應(yīng)用程序中都是可見的。對于不同的編程語言,全局作用域承擔(dān)著不同的角色,也因此遭受了不少的罵名。但對于JavaScript,我并不認(rèn)為它一無是處。我們要做的便是理解它并正確地使用它。

考慮下這樣一個場景。Bill和Peter在同一家公司工作,他們的薪水由兩部分組成:a和b。以下是表示他們薪水組成的數(shù)據(jù)結(jié)構(gòu)。

1
var emps = [{name:"Bill", parts:[{name:"a", salary:3000}, {name:"b", salary:2000}]}, {name:"Peter", parts:[{name:"a", salary:2500}, {name:"b", salary:2000}]}];

現(xiàn)在,我們希望能計算出Bill和Peter的平均薪水。以下是一段可能的程序片段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var i, n, sum;
function salary(emp) {
  sum = 0;?
  for (i = 0, n = emp.parts.length; i < n; i++) {
    sum += emp.parts[i].salary;
  }
  return sum;
}
function averageSalary(emps) {
  sum = 0; ?
  for (i = 0, n = emps.length; i < n; i++) {
    sum += salary(emps[i]);
  }
  return sum / n;
}
averageSalary(emps);

輸出的結(jié)果并不是你口算的4750,而是2500。這是因為變量i、n和sum都是全局變量,在執(zhí)行salary(emps0)之后i的值變?yōu)榱?,再回到averageSalary函數(shù)的循環(huán)體中時emps數(shù)組已然越界,最終sum的值只計算了emps數(shù)組中的第一個元素。

如果這樣的全局作用域問題并不會困擾你,那下面的問題似乎應(yīng)當(dāng)引起你的一些警覺。因為與此相比,它有點(diǎn)意想不到。

1
2
3
4
5
function swap(a, i, j) {
  temp = a[i]; // global
  a[i] = a[j];
  a[j] = temp;
}

問題并不是出在交換數(shù)組元素上,而是我們無意間創(chuàng)建了一個全局的變量temp。這要完全歸功于JavaScript的語言規(guī)范——JavaScript會將未使用var聲明的變量視為全局變量。慶幸的是,我們可以借助于類似Lint這樣的代碼檢測工具幫我們盡早地發(fā)現(xiàn)這類問題。

雖然全局變量有很多問題,然而它在支撐JavaScript模塊之間數(shù)據(jù)共享、協(xié)同合作方面確實(shí)承擔(dān)了重要的角色。此外,程序員在某些不支持ECMAScript 5的環(huán)境中利用其特性檢查的功能來填補(bǔ)一些ES5特有的特性確實(shí)受益良多。

1
2
3
4
5
6
if (!this.JSON) {
  this.JSON = {
    parse: ...,
    stringify: ...
  };
}

詞法作用域和動態(tài)作用域

在程序設(shè)計語言中,變量可分為自由變量與約束變量兩種。簡單來說,局部變量和參數(shù)都被認(rèn)為是約束變量;而不是約束變量的則是自由變量。 在馮?諾依曼計算機(jī)體系結(jié)構(gòu)的內(nèi)存中,變量的屬性可以視為一個六元組:(名字,地址,值,類型,生命期,作用域)。地址屬性具有明顯的馮?諾依曼體系結(jié)構(gòu)的色彩,代表變量所關(guān)聯(lián)的存儲器地址。類型規(guī)定了變量的取值范圍和可能的操作。生命期表示變量與某個存儲區(qū)地址綁定的過程。根據(jù)生命期的不同,變量可以被分為四類:靜態(tài)、棧動態(tài)、顯式堆動態(tài)和隱式堆動態(tài)。作用域表征變量在語句中的可見范圍,分為詞法作用域和動態(tài)作用域兩種。

在詞法作用域的環(huán)境中,變量的作用域與其在代碼中所處的位置有關(guān)。由于代碼可以靜態(tài)決定(運(yùn)行前就可以決定),所以變量的作用域也可以被靜態(tài)決定,因此也將該作用域稱為靜態(tài)作用域。在動態(tài)作用域的環(huán)境中,變量的作用域與代碼的執(zhí)行順序有關(guān)。下面這段代碼的輸出會是什么?

1
2
3
4
5
6
7
8
9
10
11
x=1
function g () {
  echo $x ;
  x=2 ;
}
function f () {
  local x=3 ;
  g ;
}
f
echo $x

如果你的回答是1, 2或3, 1都沒有錯,因為這取決于該段代碼所處的環(huán)境。如果處于詞法作用域中,答案便是1, 2;如果處于動態(tài)作用域中,答案便是3, 1。

詞法作用域允許程序員根據(jù)簡單的名稱替換就能推導(dǎo)出對象引用,例如常量、參數(shù)、函數(shù)等。這使得程序員在編寫模塊化的代碼是多么的得心應(yīng)手。同時,這可能也是動態(tài)作用域令人感覺到晦澀的原因之一。詞法作用域最早可以追溯到ALGOL語言。盡管最早的Lisp解釋器和早期的Lisp變種都采用動態(tài)作用域,但隨后的動態(tài)作用域語言都支持了詞法作用域。Common Lisp和Perl的語言演化就是最好的證明。JavaScript和C都是詞法作用域語言。不過值得一提的是,不像JavaScript,深受ALGOL語言影響的C語言并不支持嵌套函數(shù)。這對后來的C族語言影響深遠(yuǎn)。除了晦澀難懂之外,現(xiàn)代程序設(shè)計語言很少支持動態(tài)作用域的原因是動態(tài)作用域使得引用透明的所有好處蕩然無存。

臭名昭著的with語句

如果你還在使用類似下面的代碼為with語句找借口,那這正好是放棄它的真正原因。

1
2
3
4
5
6
7
8
function status(info) { ?
  var widget = new Widget();
  with (widget) {
    setFontSize(13); ?
    setText("Status: " + info);
    show();
  }
}

JavaScript會將with語句中的對象插入到詞法作用域的鏈表頭。這將使得status函數(shù)非常脆弱。例如,

1
2
3
status("connecting");
Widget.prototype.info = "[[widget info]]";
status("connected");

第二次status函數(shù)調(diào)用并不會得到預(yù)期的結(jié)果“Status:connected”而是“Status:widget info”。這是因為在第二次status函數(shù)調(diào)用之前,我們修改了widget的原型對象(增加了一個info屬性)。這將導(dǎo)致status函數(shù)的參數(shù)info會被處于詞法作用域鏈表頭的widget對象的原型對象中的info屬性所屏蔽。除此之外,with語句還會導(dǎo)致性能問題。這與在采用鏈地址法解決散列沖突的散列表中查找關(guān)鍵字是異曲同工的。下面是修正的代碼。

1
2
3
4
5
6
function status(info) { ?
  var w = new Widget();?
  w.setFontSize(13); ?
  w.setText("Status: " + info);
  w.show();
}

變量聲明提升(hoisting)

JavaScript支持詞法作用域,但并不支持塊級作用域,即變量定義的作用域并不是離其最近的封閉語句或代碼塊,而是包含它們的函數(shù)。下面的代碼片段詮釋了這一特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var emps = [{name:"Bill", salary: 5000}, {name:"Peter", salary: 3000}];
var ben = {name:"ben", salary: 6000};
function isHighestSalary(emp, others) {
  var highest = 0;?
  for (var i = 0, n = others.length; i < n; i++) {
    var emp = others[i];?
    if (emp.salary > highest) {
      highest = emp.salary;
    }
  }
  return emp.salary > highest;
}
isHighestSalary(ben, emps);

該代碼段在for循環(huán)體內(nèi)聲明了一個局部變量emp。但是由于JavaScript中的變量是函數(shù)級作用域,而不是塊級作用域,所以在內(nèi)部聲明的emp變量簡單地重聲明了一個已經(jīng)在作用域內(nèi)的變量(即參數(shù)emp)。該循環(huán)的每次迭代都會重寫這一變量。因此,return語句將emp視為others的最后一個元素,而不是此函數(shù)最初的emp參數(shù)。

可以將JavaScript的變量聲明行為看作由兩部分組成,即聲明和賦值。JavaScript隱式地提升(hoists)聲明部分到封閉函數(shù)的頂部,而將賦值留在原地。

閉包

可能有這樣一個需求,程序需要計算一個數(shù)的平方。你可能定義下面這樣一個函數(shù)。

1
2
3
function square(num) {
  return Math.pow(num, 2);
}

程序又需要計算一個數(shù)的立方。你可能又會定義下面這樣一個函數(shù)。

1
2
3
function cube(num) {
  return Math.pow(num, 3);
}

當(dāng)你還在考慮是否為計算一個數(shù)的四次方創(chuàng)建一個函數(shù)的時候,可能有人在草稿紙上寫了這樣的代碼。

1
2
3
4
5
function pow(power) {
  return function(num) {
    return Math.pow(num, power);
  };
}

是的,這就是閉包。函數(shù)是一等公平,可以作為一個函數(shù)的返回對象。你可以像下面的代碼一樣計算一個數(shù)的平方和立方。

1
2
3
4
var square = pow(2);
var cube = pow(3);
console.info(square(3));
console.info(cube(3));

掌握J(rèn)avaScript的閉包,除了理解這樣一個事實(shí)(即使外部函數(shù)已經(jīng)返回,當(dāng)前函數(shù)仍然可以引用在外部函數(shù)所定義的變量)外,還需要理解閉包存儲的是外部變量的引用。我們來看這樣一個例子。

1
2
3
4
5
6
7
8
9
10
11
12
function doubleArray(a) {
  var result = [];
  for (var i = 0, n = a.length; i < n; i++) {
    (function(j) {
      result[i] = function() {
        return a[j] * 2;
      };
    })(i);
  }
  return result;
}
doubleArray([1, 2, 3, 4, 5])[0]();

程序期望輸出的結(jié)果是2,即給定數(shù)組第一個元素的2倍。但結(jié)果并不是這樣。因為result數(shù)組中存儲的所有閉包引用的都是同一個引用i。很容易想到的一個解決方法便是使用立即調(diào)用的函數(shù)表達(dá)式來提供類似塊作用域的功能。

1
2
3
4
5
6
7
function doubleArray(a) {
  var result = [];
  for (var i = 0, n = a.length; i < n; i++) {
    (function(j) { result[i] = function() { return a[j] * 2; }; })(i);
  }
  return result;
}

ES6塊作用域

在年底即將發(fā)布的ES6標(biāo)準(zhǔn)中將會發(fā)布一個新的關(guān)鍵字let。它在語法上與var相似,但不同的是,它將在當(dāng)前塊中定義變量。

1
2
3
4
5
6
7
8
function log(msg) { ... }
function f(x) {
  if (...) {
    let { log, sin, cos } = Math;
    ... log(x) ...
  }
  log("done computing f()");
}

上面閉包引用外部變量問題,也可以通過它解決。

1
2
3
4
5
6
for (i = 0; i < n; i++) {
  let x = a[i];
  element.onclick = function() {
    ... x ...
  };
}



0


    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多