深入剖析js名稱空間函式namespace
在看阿里員工寫的開源資料庫連線池的druid的原始碼時,發現了其中在jquery的原始碼中又定義了一個名稱空間的函式:$.namespace(),其程式碼如下:
$.namespace = function() { var a=arguments, o=null, i, j, d; for (i=0; i<a.length; i=i+1) { d=a[i].split("."); o=window; for (j=0; j<d.length; j=j+1) { o[d[j]]=o[d[j]] || {}; o=o[d[j]]; } } return o; };
使用方法為:
<script type="text/javascript" src="js/jquery-1.11.1.min.js"></script> <script type="text/javascript"> $.namespace("druid.index"); druid.index=function(){ var i,j; // 定義變數 return { login:function(){ //login 方法的實現 }, submit:function(){ // submit 方法的實現 } }; }(); //使用名稱空間的函式 druid.index.login(); druid.index.submit();
這樣的話,就不會在全域性變數區,引入很多的函式,將所有要使用的函式已經變數都放入了名稱空間druid.index中,避免了不同js庫中的函式名的衝突。
但是namespace函式的定義如何理解呢?
$.namespace = function() { var a=arguments, o=null, i, j, d; for (i=0; i<a.length; i=i+1) { d=a[i].split("."); o=window; for (j=0; j<d.length; j=j+1) { o[d[j]]=o[d[j]] || {}; o=o[d[j]]; } } return o; };
思考了很久,思考的過程明白一個額外的知識點:window這個引用的是不可覆蓋的。比如我們看下面的程式碼:
console.log(window); window = {}; console.log(window); window = null; console.log(window); window = undefined; console.log(window);
列印的結果都是 window, 而不會是 null 或者 undefined。也就是說window這個名稱,實質上是個引用或者說指標,他指向heap上的全域性window物件,stack上的window引用指向heap上的全域性window物件,這個指向關係是不可覆蓋,不可修改的。上面我修改了stack上的window,檢視讓他指向Null物件,但是修改是無效的。
我們利用firebug來除錯看看名稱空間到底是如何實現的,我們一步一步的接近目標,先看如下程式碼:
(function(){ var o = window; console.log(o); // 列印Window o.druid={}; console.log(o); // 列印Window console.log(o.druid); // 列印 Object {} })();
firebug中顯示的物件為:
上面這個結果應該很好理解,因為 o指向了window,所以o.index = {}; 也就相當於 window.index = {}; 在window上定義了一個名叫index的物件。
下面我們在上面的程式碼上加碼,在前進一步,接著看:
(function(){ var o = window; console.log(o); // 列印Window o.druid={}; console.log(o); // 列印Window console.log(o.druid); // 列印 Object {} o = o.druid; console.log(o); // 列印 Object {} console.log(window); // 列印Window console.log(o.druid); // 列印 undefined })();
對應firebug中物件和上一步一樣,沒有變化:
上面的程式碼中:o = o.druid; 之後,因為 o 是指向 window,為什麼console.log(o); 列印 Object {};而 console.log(window); 列印輸出Window呢?這裡的原因是,沒有理解引用的含義。o 和 window 都是stack上的一個變數,他們都指向heap上的全域性window物件,我們修改 o 這個引用,讓它指向另外的一個空物件,而這並不會同時修改stack上的window這個引用的指向。也就是就像兩條繩子 a, b 都指向一條船,我讓其中的一條繩子b指向第二條船,並不會影響繩子a還指向第一條船。
o = o.druid; 執行之後,o 不再執行window物件了,而是指向了window.druid物件,那麼最後的console.log(o.druid);為什麼列印輸出 undefined 呢?很簡單,因為 o 已經指向了 window.druid; 而window.druid是個空物件,其下並沒有個druid的屬性,所以自然就列印輸出 undefined 了。
也就是說最後的console.log(o.druid); 就相當於 console.log(window.druid.druid);
好,理解了上面的程式碼,我們在加上一段程式碼:
(function(){ var o = window; console.log(o); // 列印Window o.druid={}; console.log(o); // 列印Window console.log(o.druid); // 列印 Object {} o = o.druid; console.log(o); // 列印 Object {} console.log(window); // 列印Window console.log(o.druid); // 列印 undefined o.index = {}; console.log(o.index); // 列印 Object {} o = o.index; console.log(o.index); // undefined })();
對應的firebug中顯示的物件為:
我們看到了已經形成了我們需要的名稱空間:window.druid.index ,其實名稱空間是使用物件鏈條來實現的。
因為 o = o.druid; 之後,o 已經指向了 window.druid ,那麼 o.index = {}; 就相當於 window.druid.index = {};
而 後面的 o = o.index; 又將 o 物件變成了一個空物件,不再指向 window.druid,列印一個空物件的 index 屬性自然就輸出 undefined.
到這裡已經就可以完全理解namespace函式的定義了。
其實核心知識點的有三條:
1)利用了 window 這個特殊引用的不可覆蓋性,不可修改;
2)名稱空間其實是物件鏈條來模擬的;
3)理解引用的含義:引用是個在stack上的變數,可以修改它指向不同的物件,要訪問或者說修改他指向的物件,必須使用 “.” 點操作符,比如 o.index ={}; 而單純的修改 o ,比如 o = {}; 並不會修改他指向的物件,因為 沒有訪問到他指向的物件,怎麼能修改到他指向的物件呢?
上面我們搞明白了$.namespace函式的來龍去脈,下面我們看看他的使用如何理解:
$.namespace("druid.index");
druid.index=function(){ var i,j; // 定義變數 return { login:function(){ //login 方法的實現 }, submit:function(){ // submit 方法的實現 } }; }();
首先 $.namespace("druid.index"); 定義了一個名稱空間:druid.index,它其實是 window.druid.index , 然後下面將一個匿名函式的呼叫的返回值賦值給window.druid.index:
druid.index=function(){ // ... }();
然後這個函式返回的是: return {} ,這是什麼?顯然我們在js中可以這樣定義一個物件: var obj = {}; 所以這裡返回的是一個js物件,那麼這個js物件的內容是什麼呢:{login:xxx, submit:xxx} 這是什麼??顯然這和我們的 json 格式是一模一樣的,所以返回的物件是一個json物件,當然也是一個js物件,只不過,該json物件的屬性的值,不是普通的字串或者json物件,而是一個函式,僅此而已。