宿主對象
JavaScript中有關變量的規則定義得十分清楚,但也不乏一些例外情況,比如自動定義的變量,以及由宿主環境(瀏覽器等)創建并提供給JavaScript引擎的變量——所謂的“宿主對象”(包括內建對象和函數)。
var a = document.createElement( "div" );
typeof a; // "object"--正如所料
Object.prototype.toString.call( a ); // "[object HTMLDivElement]"
a.tagName; // "DIV"
上例中,a不僅僅是一個object,還是一個特殊的宿主對象,因為它是一個DOM元素。其內部的[[Class]]值(為“HTMLDivElement”)來自預定義的屬性(通常也是不可更改的)。
其他需要注意的宿主對象的行為差異有:
- 無法訪問正常的object內建方法,如toString();
- 無法寫覆蓋;
- 包含一些預定義的只讀屬性;
- 包含無法將this重載為其他對象的方法;
- 其他......
在針對運行環境進行編碼時,宿主對象扮演著一個十分關鍵的角色,但要特別注意其行為特性,因為它們常常有別于普通的JavaScript object。
在我們經常打交道的宿主對象中,console及其各種方法(log(..)、error(..)等)是比較值得一提的。console對象由宿主環境提供,以便從代碼中輸出各種值。
console在瀏覽器中是輸出到開發工具控制臺,而在Node.js和其他服務器JavaScript環境中,則是指向JavaScript環境系統進程的標準輸出(stdout)和標準錯誤輸出(stderr)。
全局DOM變量
你可能已經知道,聲明一個全局變量(使用var或者不使用)的結果并不僅僅是創建一個全局變量,而且還會在global對象(在瀏覽器中為window)中創建一個同名屬性。
還有一個不太為人所知的事實是:由于瀏覽器演進的歷史遺留問題,在創建帶有id屬性的DOM元素時也會創建同名的全局變量:
<div id="foo"></div>
if (typeof foo == "undefined") {
foo = 42; // 永遠也不會運行
}
console.log(foo); // HTML元素
你可能認為只有JavaScript代碼才能創建全局變量,并且習慣使用typeof或..in window來檢測全局變量。但是如上例所示,HTML頁面中的內容也會產生全局變量,并且稍不注意就很容易讓全局變量檢查錯誤百出。
這也是盡量不要使用全局變量的一個原因。如果確實要用,也要確保變量名的唯一性,從而避免與其他地方的變量產生沖突,包括HTML和其他第三方代碼。
原生原型
一個廣為人知的JavaScript的最佳實踐是:不要擴展原生原型。
如果向Array.prototype中加入新的方法和屬性,假設它們確實有用,設計和命名都很得當,那它最后很有可能會被加入到JavaScript規范當中。這樣一來你所做的擴展就會與之沖突。
所以,首先不要擴展原生方法,除非你確信代碼在運行環境中不會有沖突。如果對此你并非100%確定,那么進行擴展是非常危險的。這需要你自己仔細權衡利弊。
其次,在擴展原生方法時需要加入判斷條件(因為你可能無意中覆蓋了原來的方法)。對于前面的例子,下面的處理方式要更好一些:
if (!Array.prototype.push) {
// Netscape 4沒有Array.push
Array.prototype.push = function (item) {
this[this.length - 1] = item;
};
}
shim/polyfill
polyfill(或者shim)能有效地為不符合最新規范的老版本瀏覽器填補缺失的功能,讓你能夠通過可靠的代碼來支持所有你想要支持的運行環境。
ES5-Shim(https://github.com/es-shims/es5-shim) 是一個完整的shim/polyfill
集合,能夠為你的項目提供ES5 基本規范支持。同樣,ES6-Shim(https://
github.com/es-shims/es6-shim)提供了對ES6 基本規范的支持。雖然我們可
以通過shim/polyfill 來填補新的API,但是無法填補新的語法??梢允褂?br> Traceur(https://github.com/google/traceur-compiler/wiki/GettingStarted) 這樣
的工具來實現新舊語法之間的轉換。
對于將來可能成為標準的功能,按照大部分人贊同的方式來預先實現能和將來的標準兼容的polyfill,我們成為prollyfill(probably fill)。
<script>
絕大部分網站/Web應用程序的代碼都存放在多個文件中,通常可以在網頁中使用<scriopt src=..></script>
來加載這些文件,或者使用<script> .. </script>
來包含內斂代碼。
這些文件和內聯代碼是相互獨立的JavaScript程序還是一個整體呢?
答案是它們的運行方式更像是相互獨立的JavaScript程序,但是并非總是如此。
它們共享global對象(在瀏覽器中在是window),也就是說這些文件中的代碼在共享的命名空間中運行,并相互交互。
如果某個script中定義了函數foo(),后面的script代碼就可以訪問并調用foo(),就像foo()在其內部被聲明過一樣。
但是全局變量作用域的提升機制在這些便捷中不適用,因此無論是<script> .. </script>
還是<script src=..></script>
,下面的代碼都無法運行(因為foo()還未被聲明):
<script>foo();</script>
<script>
function foo() { .. }
</script>
但是下面的兩端代碼則沒問題:
<script>
foo();
function foo() { .. }
</script>
和:
<script>
function foo() { .. }
</script>
<script>foo();</script>
如果script中的代碼(無論是內聯代碼還是外部代碼)發生錯誤,它會像獨立的JavaScript程序那樣停止,但是后續的script中的代碼(仍然共享global)依然會接著運行,不會受影響。
你可以使用代碼來動態創建script,將其加入到頁面的DOM中,效果是一樣的:
var greeting = "Hello World";
var el = document.createElement("script");
el.text = "function foo(){ alert( greeting );\
} setTimeout( foo, 1000 );";
document.body.appendChild(el);
如果將el.src的值設置為一個文件URL,就可以通過
<script src=""></script>
動態加載外部文件。
內聯代碼和外部文件中的代碼之間有一個區別,即在內聯代碼中不可以出現</script>字符串,一旦出現即被視為代碼結束。因此對于下面這樣的代碼需要非常小心:
<script>
var code = "<script>alert( ‘Hello World’ )</script>";
</script>
上述代碼看似沒什么問題,但是字符串常量中的</script>將會被當作結束標簽來處理,因此會導致錯誤。常用的變通方法是:
"</sc" + "ript>";
另外需要注意的一點是,我們是根據代碼文件的字符集屬性(UTF-8、ISO-8859-8等)來解析外部文件中的代碼(或者默認字符集),而內聯代碼則使用其所在頁面文件的字符集(或者默認字符集)。
現實中的限制
JavaScript規范對于函數中參數的個數,以及字符串常量的長度等并沒有限制;但是由于JavaScript引擎實現各異,規范在某些地方有一些限制。
function addAll() {
var sum = 0;
for (var i = 0; i < arguments.length; i++) {
sum += arguments[i];
}
return sum;
}
var nums = [];
for (var i = 1; i < 100000; i++) {
nums.push(i);
}
addAll(2, 4, 6); // 12
addAll.apply(null, nums); // 應該是: 499950000
在一些JavaScript引擎中你會得到正確答案499950000,而另外一些引擎(如Safari 6.x)中則會產生錯誤“RangeError: Maximum call stack size exceeded”。
下面列出一些已知的限制:
- 字符串常量中允許的最大字符數(并非只是針對字符串值);
- 可以作為參數傳遞到函數中的數據大小(也稱為棧大小,以字節為單位);
- 函數聲明中的參數個數;
- 未經優化的調用棧(例如遞歸)的最大層數,即函數調用鏈的最大長度;
- JavaScript程序以阻塞方式在瀏覽器中運行的最長時間(秒);
- 變量名的最大長度。