PHP是我的第一門程式語言,之後通過像jQuery這樣的庫首次接觸JavaScript。由於JavaScript與PHP的工作原理不同,開始時總有一些JavaScript問題困擾著我。即使現在,仍有讓我感到疑惑的東西。我想分享一些開始使用JavaScript時我苦苦思考的問題。這些問題將涉及:全域性名稱空間、this、ECMAScript 3 和ECMAScript 5的區別、非同步呼叫、原型以及 簡單的JavaScript繼承。
全域性名稱空間
在PHP中,特別地,當你在類(或名稱空間塊)之外宣告一個函式變數時,你實質上往全域性名稱空間新增了一個函式。在JavaScript中,沒有所謂的名稱空間,然而所有的東西都附屬於全域性物件。在Web瀏覽器中,全域性物件就是windows物件。另一個主要區別是:在JavaScript中,函式和變數均是全域性物件的屬性,即我們通常指代的properties。
因為,當你覆蓋一個全域性函式或屬性,JavaScript不會給出任何警告,所以這會很麻煩,實際上是相當危險的。
1 2 3 4 5 6 7 8 9 10 11 12 |
function globalFunction() { console.log('I am the original global function'); } function overWriteTheGlobal() { globalFunction = function() { console.log('I am the new global function'); } } globalFunction(); //輸出 "I am the original global function" overWriteTheGlobal(); //重寫最初的全域性函式 globalFunction(); //輸出 "I am the new global function" |
JavaScript中的一種技術是立即執行函式表示式,一般稱為自執行匿名函式。它對於保持變數和函式的獨立性是非常有用的。通常,我通過傳入物件方式將函式暴露給外界。這是模組模式的一個變種。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
var module = {}; (function(exports){ exports.notGlobalFunction = function() { console.log('I am not global'); }; }(module)); function notGlobalFunction() { console.log('I am global'); } notGlobalFunction(); //輸出 "I am global" module.notGlobalFunction(); //輸出 "I am not global" |
在自執行匿名函式裡,所有的全域性作用域是封閉的,通過將之依附到module變數實現。從技術上講,你可以直接把屬性附加到module變數,但我們將它傳遞給函式的原因是:明確地表明函式所附加的地方。它允許我們給傳入函式的物件起別名。這裡的關鍵是:我們預先宣告依賴於模組變數,而不依賴於全域性變數。
你也許已經注意到了var關鍵字。如果你不知道如何使用它,基本的解釋是:在宣告變數前使用var,為離之最近的函式建立了一個屬性。如果省略var關鍵字,那麼意味著給現有變數分配一個新值,並提升了作用域鏈。這個作用域鏈可能是全域性範圍的。
1 2 3 4 5 6 7 8 9 10 |
var imAGlobal = true; function globalGrabber() { imAGlobal = false; return imAGlobal; } console.log(imAGlobal); //輸出 "true" console.log(globalGrabber()); //輸出 "false" console.log(imAGlobal); //輸出 "false" |
正如你所看到的一樣,在你的函式中依賴全域性變數是相當危險的,因為可能產生副作用,造成難以預料的衝突。當你使用var關鍵字時,會發生什麼呢?
1 2 3 4 5 6 7 8 9 10 |
var imAGlobal = true; function globalGrabber() { var imAGlobal = false; return imAGlobal; } console.log(imAGlobal); //輸出 "true" console.log(globalGrabber()); //輸出 "false" console.log(imAGlobal); //輸出 "true" |
JavaScript將var宣告變數提升到函式塊頂部,接著初始化變數。這就是所謂的變數提升。
總結:所有變數的作用於一個函式內(函式本身就是一個物件),並使用var宣告這些變數就確定它們的函式作用域,不使用var意味著宣告一個全域性變數。
讓我們來看看使用變數提升的情況:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function variableHoist() { console.log(hoisty); hoisty = 1; console.log(hoisty); var hoisty = 2; console.log(hoisty); } variableHoist(); //輸出 undefined (如果在作用域內不存在var宣告,將得到一個引用錯誤(ReferenceError)) //輸出 "1" //輸出 "2" try { console.log(hoisty); //輸出 ReferenceError (不存在全域性變數"hoisty") } catch (e) { console.log(e); } |
之所以像你所看到的一樣:把var宣告放在函式的哪個位置實際上並不重要,是因為屬性是在函式執行任何程式碼之前建立好的。在目前實踐中,通常把var宣告放在函式頂部,因為其作用域終止於該函式。在函式的頂部初始化變數也是完全可以接受的,只可以清楚事件的執行順序。
在JavaScript中,使用function關鍵字宣告的函式(不賦給變數)也會被提升。實際上,整個函式被提升,可供執行。
1 2 3 4 5 |
myFunction(); //輸出 "i exist" function myFunction() { console.log('i exist'); } |
當使用var形式來宣告函式時,整個函式不會被提升:
1 2 3 4 5 6 7 8 9 10 |
try { myFunction(); } catch (e) { console.log(e); //丟擲 "Uncaught TypeError: undefined is not a function" } var myFunction = function() { console.log('i exist'); } myFunction(); //輸出 "i exist" |
理解“this”
由於JavaScript使用了函式域,this的意義與PHP中的截然不同,引起了諸多疑惑。考慮下面的情形:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
console.log(this); // 輸出 window object var myFunction = function() { console.log(this); } myFunction(); //輸出 window object var newObject = { myFunction: myFunction } newObject.myFunction(); //輸出 newObject |
預設情況下,this指向包含在函式內的物件。由於myFunction()是全域性物件的一個屬性,this是全域性物件window的引用。現在,當我們把myFunction()封裝到newObject中,現在this指向newObject。在PHP和其他類似的語言中,this往往指向包含該方法的類的例項。你可能認為JavaScript在這裡幹著一些蠢事,但確實很多JavaScript語言的力量正來源於此。事實上,當我們使用call()或apply()來呼叫JavaScript函式時,甚至可以替換this的值。
1 2 3 4 5 6 7 8 |
var myFunction = function(arg1, arg2) { console.log(this, arg1, arg2); }; var newObject = {}; myFunction.call(newObject, 'foo', 'bar'); //輸出 newObject "foo" "bar" myFunction.apply(newObject, ['foo', 'bar']); //輸出 newObject "foo" "bar" |
但不要急著往下看,讓我們認真思考一下。這裡我們所做的是:通過傳入物件值作為函式內部this的替代值來呼叫函式myFunction函式。 call()與apply()的根本區別在於傳入引數的方式:call()函式接收第一個引數後,接著可接收無限量的引數;apply()函式則規定將引數陣列作為第二個引數。
像jQuery這樣的庫,採用上述方式進行呼叫,表現出驚人能力。讓我們看看jQuery的$.each()方法:
1 2 3 4 5 6 7 8 9 10 11 |
var $els = [$('div'), $('span')]; var handler = function() { console.log(this); }; $.each($els, handler); //迭代器 1 輸出包裹在div裡面的jquery dom元素 //迭代器 2 輸出包裹在tag裡面的jquery dom元素 handler.apply({}); //輸出 object |
jQuery經常重寫this的值,所以你要試圖理解this在jQuery事件處理程式上下文中或其他類似結構中的含義。
弄清楚ECMAScript 3和ECMAScript 5的區別
長久以來,ECMAScript 3已經成為大多數瀏覽器的標準,但最近ECMAScript 5融入大部分現代瀏覽器(IE瀏覽器仍然滯後)。ECMAScript 5向JavaScript中引入了很多常見的功能以及一些你以前只能靠某個庫提供的原生函式,如String.trim()和Array.forEach()。然而問題是:如果你使用Internet Explorer瀏覽器,這些方法在瀏覽器環境中仍不可用。
看看當你在IE8中試圖使用String.trim會發生什麼呢:
1 2 3 4 5 6 7 8 |
var fatString = " my string "; //in modern browsers console.log(fatString); //輸出 " my string " console.log(fatString.trim()); //輸出 "my string" //in IE 8 console.log(fatString.trim()); //error: Object doesn't support property or method 'trim' |
因此在此期間,我們可以像使用jQuery.trim方法來做到這一點。我相信如果瀏覽器支援,jQuery會回退到String.trim,以提高效能(瀏覽器原生函式更快)。
你可能不關心甚至不需要了解ECMAScript3和ECMAScript5之間的所有差異,但首先查閱作為函式參考手冊的Mozilla開發者網路(MDN),看看函式適用的語言版本:這通常是一個好主意。一般來說,如果你使用庫像jQuery或者underscore這樣的庫來處理,效果應該也不錯。 如果您有興趣在舊版瀏覽器中使用類似ECMAScript 5的外掛,請查閱https://github.com/kriskowal/es5-shim
瞭解非同步
在實踐中開始編寫JavaScript程式碼時,特別是jQuery,讓我吃盡苦頭的是一些非同步操作。有很多次遇到這樣的情況:我所編寫過程式程式碼希望立即返回一個結果,但並未發生。
看看下面的程式碼片段:
1 2 3 4 5 6 7 8 9 |
var remoteValue = false; $.ajax({ url: 'http://google.com', success: function() { remoteValue = true; } }); console.log(remoteValue); //輸出 "false" |
我花了一段時間才弄清楚,當進行非同步程式設計時,需要使用回撥函式來處理ajax返回的結果。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var remoteValue = false; var doSomethingWithRemoteValue = function() { console.log(remoteValue); //當呼叫成功時,輸出 true } $.ajax({ url: 'https://google.com', complete: function() { remoteValue = true; doSomethingWithRemoteValue(); } }); |
另一個厲害的東西是deferred物件(有時稱為promises),可用於傾向過程風格的程式設計:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
var remoteValue = false; var doSomethingWithRemoteValue = function() { console.log(remoteValue); } var promise = $.ajax({ url: 'https://google.com' }); //輸出 "true" promise.always(function() { remoteValue = true; doSomethingWithRemoteValue(); }); //輸出 "foobar" promise.always(function() { remoteValue = 'foobar'; doSomethingWithRemoteValue(); }); |
您可以使用promises完成鏈式回撥。在我看來,這種方式比巢狀回撥方式更容加易用,額外地,這些物件還提供了大量其他好處。
瀏覽器中的動畫也是非同步的,因此這也是容易混亂的地方。這裡不打算深究,但是你應該像處理ajax請求一樣,通過回撥函式來處理動畫。然而,我還不算是這方面的專家,所以請自行參考 jQuery .animate() method。
JavaScript的簡單繼承
粗略概括一下,JavaScript通過克隆物件來擴充套件它們,而PHP、Ruby、Python和Java使用、繼承類。在JavaScript中每個物件都擁有一個所謂的原型。事實上,所有的函式,字串,數字以及物件有一個共同的祖先:Object。關於原型有兩件事情要記住:藍本(blueprints)和鏈。
基本上,每個原型本身就是一個物件。在建立一個物件的例項時,它描述了可用的屬性。原型鏈就是允許原型繼承其他原型的東西。事實上,原型本身也有原型。當物件例項沒有某個方法或屬性時,那麼它會在該物件的原型、原型的原型中尋找。依此類推,直至發現該屬性不存在,最終報錯undefined。
值得慶幸的是,初學者一般不必要沾惹這東西,因為建立一個物件字面量相當容易,然後在執行時附加屬性即可。
1 2 3 4 5 6 7 |
var obj = {}; obj.newFunction = function() { console.log('I am a dynamic function'); }; obj.newFunction(); |
一直以來,我使用jQuery.extend()這種簡單的方法來繼承物件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var obj = { a: 'i am a lonely property' }; var newObj = { b: function() { return 'i am a lonely function'; } }; var finalObj = $.extend({}, obj, newObj); console.log(finalObj.a); //輸出 "i am a lonely property" console.log(finalObj.b()); //輸出 "i am a lonely function" |
ECMAScript 5提供了Object.create()方法,你可用它從對現有物件進行繼承。然而,如果你需要支援舊版瀏覽器,請勿使用。在建立屬性和設定屬性的屬性(是的,屬性也有屬性)方面,它確實具有明顯的優勢。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var obj = { a: 'i am a lonely property' }; var finalObj = Object.create(obj, { b: { get: function() { return "i am a lonely function"; } } }); console.log(finalObj.a); //輸出 "i am a lonely property" console.log(finalObj.b); //輸出 "i am a lonely function" |
由於JavaScript具有強大能力和靈活性,你可以深入JavaScript繼承方面。而好在這裡又不必深究。
意外陷阱:在for迴圈中忘記使用var
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
var i = 0; function iteratorHandler() { i = 10; } function iterate() { //迭代器僅執行一次 for (i = 0; i < 10; i++) { console.log(i); //輸出 0 iteratorHandler(); console.log(i); //輸出 10 } } iterate(); |
該例子是刻意的,但你可意識到這裡的危險。解決的辦法是,使用var宣告迭代器變數。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var i = 0; function iteratorHandler() { i = 10; } function iterate() { //迭代器會執行10次 for (var i = 0; i < 10; i++) { iteratorHandler(); console.log(i); } } iterate(); |
這一切可追溯到我們之前說的作用域規則。請記住,恰當使用var。
總結
JavaScript或許是唯一一門使用前不需要學習的語言,但最終你將陷入一些不明原因的麻煩之中。在這些日子裡,除了避免犯錯之外,學習JavaScript收穫良多,考慮到其重生與廣泛適用性。這篇博文並不是嘗試著解決所有問題的靈丹妙藥,但希望在人們被迫編寫可怕的JavaScript程式碼之前,可以幫助他們瞭解一些基本情況;暗地裡希望重回幸福之地 — 充滿著資料庫查詢的PHP後端專案。