作用域
在深入學習JavaScript作用域之前,首先要了解一下,究竟什麼是作用域。幾乎所有的編程語言都有作用域的概念,簡單的說,作用域就是變量與函數的可訪問範圍,即作用域控制着變量與函數的可見性和生命週期。
我們先了解一下JavaScript的工作原理,引擎,編譯器,作用域三者是如何協同工作來完成javascript代碼的執行的。
引擎:從頭到尾負責整個JavaScript程序的編譯及執行過程。
編譯器:負責詞法分析及代碼生成
作用域:負責收集並維護由所有聲明的變量組成的一系列查詢,並實施一套非常嚴格的規則,確定當前執行的代碼對這些變量的訪問權限。
我們看下最簡單的var index = 10;瞭解一下引擎、編譯器和作用域是如何協同工作的。
- var index;
- index = 10;
在上面的變量聲明的執行時,我們提到了引擎在作用域中查找變量的問題,我們將此分爲兩種方式,一種爲LHS查詢,一種爲RHS查詢,在上面的例子中,引擎在執行index = 10時,進行的是LHS查詢。
那麼究竟何爲LHS查詢和RHS查詢呢,簡單的,當變量出現在左側時,執行的是LHS查詢(L可以認爲是Left)。除了LHS查詢,剩下的就是RHS查詢。作進一步說明,LHS查詢是企圖找到變量的容器本身,即去尋找賦值操作的目標,而RHS查詢是找賦值操作的源頭,即獲取變量的值。
舉例說明:
- index = 10;
- console.log(index);
作用域嵌套 引擎從當前的執行作用域開始查找變量,如果找不到,就向上一級繼續查找,直至到最外層的全局作用域鏈,不管最終是否找到了變量,查找過程到到此結束。
如下:在執行index = 15時,引擎首先在當前作用域fun函數中查找index變量,沒有找到,那麼繼續向上查找,在par函數中也沒有找到,那麼繼續向上一級查找,最後在全局作用域中找到了該index.
- <script>
- var index = 10;
- function par(){
- function fun(){
- index = 15;
- }
- fun();
- }
- par();
- </script>
當引擎進行RHS查詢時,如果查詢到作用域鏈的頂層(全局作用域)依舊未找到index變量,那麼引擎就會拋出一個ReferenceError異常。
當引擎進行LHS查詢,在全局作用域中也未能找到目標變量(本例中的index),在非嚴格模式下,會在全局作用域中創建一個該名稱的變量。而在嚴格模式下,會同RHS查詢一樣,拋出一個ReferenceError異常。
作用域查找會在找到第一個匹配的標識符(變量)時停止,在多層嵌套作用域中可以定義同名的標識符,這也稱之爲"遮蔽效應",如上面的代碼中,如果fun函數中定義了index變量,那麼在fun中對index的賦值操作不會影響到全局變量中的index.因爲作用域查找始終是從運行時所處的最內部的作用域開始,逐級向上查找,直到找到匹配的標識符爲止。
詞法作用域是由寫代碼時將變量和函數寫在哪裏決定的,而不是由其調用的位置決定,JS提供了兩種機制修改詞法作用域,即:width和eval,鑑於這兩種機制都會導致性能的降低,在此不多作介紹,儘量避免使用即可。
初學者或多或少都會遇到一個問題:命名衝突。
命名衝突會導致變量的值被意外覆蓋。而這並非是我們想看到的。那麼如何規避衝突呢?
1.全局命名空間(類似於jQuery的實現)
如:我們在全局作用域重視聲明瞭一個名字足夠獨特的變量,通常是一個變量,如下面的carousel_yve,這個對象被稱爲庫的命名空間,所有需要暴露給外界的功能都會稱爲這個對象的屬性(如:index、defaults、init),避免將自己的標識符暴露在頂級的詞法作用域中。
- <script type = "text/javascript">
- var carousel_yve = {
- index: 0,
- defaults: { width: "1200px",
- height: "500px"},
- init: function(){
- console.log(this.defaults);
- }
- }
- carousel_yve.init(); //Object {width: "1200px", height: "500px"}
- console.log(carousel_yve.index); //10
- </script>
2.模塊模式
模塊模式分爲兩種,一種是每次調用都會創建一個新的模塊實例,另一種是單例模式,即只會創建一個實例。
如:
- <script type = "text/javascript">
- function carousel_yve(){
- var index = 0;
- var defaults = {width: "1200px",
- height: "500px"};
- function init(){
- console.log(defaults);
- }
- function doSomething(){
- console.log(index);
- }
- return {
- init: init,
- doSomething: doSomething
- }
- }
- var example = carousel_yve();
- example.init(); //Object {width: "1200px", height: "500px"}
- example.doSomething(); //0
- </script>
我們再來看一下單例模式:
- <script type = "text/javascript">
- var example = (function carousel_yve(){
- var index = 0;
- var defaults = {width: "1200px",
- height: "500px"};
- function init(){
- console.log(defaults);
- }
- function doSomething(){
- console.log(index);
- }
- return {
- init: init,
- doSomething: doSomething
- }
- })();
- example.init(); //Object {width: "1200px", height: "500px"}
- example.doSomething(); //0
- </script>
模塊模式需要具備兩個條件:
1.必須有外部的封閉函數,該函數必須至少被調用一次。
2.封閉函數必須返回至少一個內部函數,這樣內部函數才能在私有作用域中形成閉包,並且可以訪問或者修改私有的狀態。
鑑於本文的目的是研究javaScript的作用域問題,因此對於模塊模式不再做更多的擴展說明。
關於IIFE,有時我們想對外隱藏時,也可以簡單的使用此方式。通過這樣的方式,避免污染所在的作用域。
- <script type = "text/javascript">
- (function carousel_yve(){
- var index = 10;
- var defaults = {width: "1200px", height: "500px;"};
- var obj = document.getElementById("btn");
- obj.addEventListener("click", function(){/*code*/}, false);
- //code……
- })();
- </script>
包裝函數的聲明以(function開始,而不是以function開始,看起來區別很小,但是實際上卻完全不一樣,因爲(function開始會當做函數表達式,而function開頭是作爲標準的函數聲明。而函數聲明和函數表達式最重要的區別在於它們的名稱標識符被綁定在何處。上面的代碼中carousel_yve被綁定在自身的函數中,而不是所在的作用域中,其只能在自身函數的內部被訪問。
此外,對於匿名函數和具名函數還要做一點說明。
JavaScript中,函數表達式允許匿名,但是函數聲明不允許省略函數名,即不允許匿名。儘管匿名函數使用起來簡單快捷,但是匿名函數的幾個缺點需要考慮:
1.匿名函數在棧追蹤中不會顯示出有意義的函數名,使得調試困難。
2.如果沒有函數名,當函數需要調用自身時,只能使用過期的arguments.callee引用,比如在遞歸中。
3.匿名函數省略了對於代碼可讀性/可理解性很重要的函數名,一個描述性的名稱可以防止代碼不言自明。(代碼即註釋)
匿名或具名並不會影響函數的功能,因此始終給函數表達式命名是值得推崇的。如我們經常使用的setTimeout、setInterval中。給回調函數命一個形象的名字將使代碼的可讀性更強。
提升
- <script type = "text/javascript">
- console.log(a);
- var a = 2;
- </script>
- var a;
- console.log(a);
- a = 2;
- <script type = "text/javascript">
- console.log(a);
- a = 2;
- </script>
- <script type = "text/javascript">
- example();
- var example = function(){
- console.log(10);
- };
- function example(){
- console.log(20);
- }
- </script>
- function example(){
- console.log(20);
- }
- example();
- example = function(){
- console.log(10);
- };
- <script type = "text/javascript">
- example();
- var example = function(){
- console.log(10);
- };
- function example(){
- console.log(20);
- };
- function example(){
- console.log(30)
- }
- </script>
- <script type = "text/javascript">
- var a = true;
- if(a == true){
- function example(){
- console.log(10);
- };
- }else{
- function example(){
- console.log(20);
- }} example();
- <html lang="en">
- <head>
- <meta charset="UTF-8" />
- <title>Document</title>
- <style>
- #ullist li { display: block;width: 40px; height: 40px;
- border:1px solid #ccc; text-align: center;
- line-height: 40px; cursor: pointer; float: left;
- margin:10px;}
- </style>
- </head>
- <body>
- <ul id="ullist">
- <li id="li1">1</li>
- <li id="li2">2</li>
- <li id="li3">3</li>
- <li id="li4">4</li>
- <li id="li5">5</li>
- </ul>
- </body>
- <script>
- window.onload=function(){
- var ullist=document.getElementById("ullist");
- var listE=document.getElementsByTagName("li");
- for (var i=0; i<listE.length; i++){
- listE[i].onclick = function(i){
- alert(listE[i].innerHTML);
- };
- };
- }
- </script>
- </html>
- <script type = "text/javascript">
- window.οnlοad=function(){
- var ullist=document.getElementById("ullist");
- var listE=document.getElementsByTagName("li");
- for (var i=0; i<listE.length; i++){
- listE[i].onclick = (function(i){
- return function(){
- alert(listE[i].innerHTML);
- }
- })(i);
- };
- };
- </script>