宿主对象
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,但是无法填补新的语法。可以使用
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程序以阻塞方式在浏览器中运行的最长时间(秒);
- 变量名的最大长度。