javascript学习笔记-常见问题及技巧

垃圾收集趣史:http://blog.csdn.net/KAI3000/article/details/314628

 

函数和操作符

 

 

标识符解析、作用域链、运行期上下文(又称为执行环境)、原型链、闭包

 

每一个JS 函数都表示为一个对象,该对象有一个内部属性[[Scope]],它包含了一个函数被创建的作用域中对象的集合,这个集合被称为函数的作用域链(Scope chain),它决定哪些数据能被函数访问。函数作用域中的每个对象被称为一个可变对象(variable object)。当一个函数创建后,它的作用域链会被创建此函数的作用域中可访问的数据对象所填充。

例如下面这个全局函数:

function add(num1, num2) {
var sum = num1 + num2;
return sum;
}

由于此函数是在全局作用域下创建的,所以函数add() 的作用域链中只填入了一个单独的可变对象——全局对象:

而执行此函数时则会创建一个称为“运行期上下文(execution context)”的内部对象,它定义了函数执行时的环境。函数每次执行时对应的运行期上下文都是独一无二的,所以多次调用同一个函数就会导致创建多个运行期上下文。当函数执行完毕,执行上下文就被销毁。

而我们刚刚讲的函数作用域链和这个运行期上下文有什么关系呢?是这样的,每个运行期上下文都有自己的作用域链,用于标识符解析,而它的作用域链引用的则是函数的内部对象[[Scope]] 所指向的作用域链。此外还会创建一个“活动对象(Activation object)”,该对象包含了函数的所有局部变量、命名参数、参数集合以及this,当函数执行时它又被当作可变对象,和前面是一回事。然后此对象会被推入作用域链的前端,就这样运行期上下文也就创建好了,当运行期上下文被销毁,活动对象也随之销毁(闭包除外,后面会讲到)。

例如前面add() 函数运行时对应的运行期上下文和作用域链:

函数执行时,每遇到一个变量都会进行一次标识符解析的过程,该过程从头至尾搜索作用域链,从当前运行函数的活动对象到全局对象,直至查找到同名的标识符,如果没找到则被认为是undefined。

 

 

标识符解析的性能

标识符解析是有代价的,在运行期上下文的作用链中,一个标识符所在的位置越深,它的读写速度也就越慢,显然对局部变量的读写是最快的,因为它所在的对象处于作用链的最前端。一个好的经验法则是:如果某个跨作用域的值在函数中被引用一次以上,那么就把它存储到局部变量里。

此外代码执行时临时改变作用域链也会影响标识符解析的性能,有两个语句会造成这种情况——with 和catch,这两条语句被执行时都会将一个新的可变对象(with 的是语句中指定的对象、catch 的是异常对象)推入作用域链的头部,这样原有的可访问对象都被往后推了一个层次,这使得它们的访问代价更高了。因此对于with 语句最好避免使用,catch 语句要用的话可以定义一函数来进行错误的处理以减少catch 内的语句数量。

下图便是一个在with 语句中改变后的作用域链的例子:

闭包、作用域和内存

理解了作用域链之后,闭包也就好懂了。闭包是JavaScript 最强大的特性之一,它允许函数访问局部作用域之外的数据。然而,有一种性能问题与闭包有关,思考如下代码:

function assignEvents() {
var id = 'xdi9592';
document.getElementById(
'save-btn').onclick = function(event) {
saveDocument(id);
}
}

函数内部的onclick 事件处理器就是一个闭包,为了能让该闭包访问函数assignEvents() 内的数据,闭包被创建时,它的[[Scope]] 属性被初始化为其外部函数运行时的作用域链中的对象,即闭包的[[Scope]] 属性包含了与运行期上下文作用域相同的对象的引用。

下图为函数assignEvents() 运行期上下文的作用域链和闭包:

通常来说,函数的活动对象会随同运行期上下文一同销毁。但引入闭包时,由于引用仍然存在于闭包的[[Scope]] 属性中,因此激活对象无法被销毁。这意味着脚本中的闭包与非闭包函数相比,需要更多的内存开销。

而当闭包被执行时,一个活动对象会为闭包自身所创建并被置于作用域链的最前端:

此时对于闭包外数据(如id、saveDocument 等) 的访问开销就更大了,因为它们在作用域链的位置均被推后了一个层次。这就是使用闭包最主要的性能关注点:你要经常访问大量跨作用域的标识符,每次访问都会导致性能损失。

对象成员解析、原型、原型链

对象成员指的是对象的属性或方法,当我们要访问TestObj.abc 这样一个对象成员时,首先在标识符解析的过程中找到了TestObj 对象,接下来要访问abc 属性(或是方法)则要进行对象成员解析的过程了。

JavaScript 中的对象是基于原型的,原型是其他对象的基础,它定义并实现一个新对象必须包含的成员列表。对象通过一个内部属性__proto__(这个属性在Firefox、Safari 和Chrome 中对开发者可见) 绑定到它的原型。

对象可以有两种成员类型:实例成员和原型成员。实例成员存在于对象实例中,原型成员则由对象原型继承而来。一旦创建了一个内置对象(如Object 和Array) 的实例,它们就会自动拥有一个Object 实例作为原型,如下面的代码:

var book = {
title :
'High Performance JavaScript',
publisher :
'Yahoo! Press'
}
alert(book.toString());
//"[object Object]"

这个例子中book 并没有定义toString() 方法,然而却能被顺利执行,原因是方法toString() 是由对象book 继承而来的原型成员:

注意book 原型中也有__proto__ 属性,前面也说到了JavaScript 的对象是基于原型的,既然每个对象都具有原型,这自然便形成了一个“链”,我们称之为原型链,原型链终止于原型为null 的那个对象上。而对象成员的解析实际上就是原型链的遍历过程,从实例成员开始查找到原型成员。

此外也可以定义并使用构造器来创建另一种类型的原型,这样则插入了新定义的原型对象至原型链中,考虑下面这个例子:

复制代码
function Book(title, publisher) {
this.title = title;
this.publisher = publisher;
}
Book.prototype.sayTitle
= function() {
alert(
this.title);
}
var book1 = new Book('High Performance JavaScript', 'Yahoo! Press');
var book2 = new Book('JavaScript: The Good Parts', 'Yahoo! Press');
复制代码

这里为Book 对象手动创建了一个原型,并定义了一个方法sayTitle(),对于实例book1 来说原型链是这样的:book1 的原型 -> Book.prototype, Book.prototype 的原型 -> Object, Object 的原型 -> null。

当要访问一个book1 中的一个成员时,检索其原型链的过程则是:首先查找book1 的实例成员(title, publisher),若没找到则接着查找book1 的原型Book.prototype 中的原型成员(sayTitle),若还没到则继续查找Book.prototype 的原型Object 的原型成员(toString, valueOf ...),若仍然没找到,则继续查找Object 的原型,但Object 的原型为null,则查找终止,此时该成员判定为undefined,若该过程中有查找到的话则立即中止查找并返回。原型链的关系如图:

对象成员解析的性能

和标识符解析一样,对象成员的解析也是有开销的,原型链的遍历过程中,每深入一层都会增加性能的损失,于是对象在原型链中存在的位置越深,找到它就越慢。

另外由于对象成员可能包含其他成员,例如window.location.href,每次遇到点操作符,该嵌套成员都会导致JavaScript 引擎搜索所有对象成员,显然对象成员嵌套得越深,访问速度就会越慢,因此尽量少用,例如执行location.href 总是要比window.location.href 要快。

很显然,当要频繁地访问对象成员时,最好用变量将它们缓存起来。

 

js内存泄露的几种情况详细探讨

内存泄露是指一块被分配的内存既不能使用,又不能回收,直到浏览器进程结束,由于浏览器垃圾回收方法有bug,会产生内存泄露,下面与大家详细探讨下内存泄露的几种情况
 

 内存泄露是指一块被分配的内存既不能使用,又不能回收,直到浏览器进程结束。在C++中,因为是手动管理内存,内存泄露是经常出现的事情。而现在流行的C#和Java等语言采用了自动垃圾回收方法管理内存,正常使用的情况下几乎不会发生内存泄露。浏览器中也是采用自动垃圾回收方法管理内存,但由于浏览器垃圾回收方法有bug,会产生内存泄露。

1、当页面中元素被移除或替换时,若元素绑定的事件仍没被移除,在IE中不会作出恰当处理,此时要先手工移除事件,不然会存在内存泄露。

复制代码 代码如下:

<div id="myDiv">
<input type="button" value="Click me" id="myBtn">
</div>
<script type="text/javascript">
var btn = document.getElementById("myBtn");
btn.onclick = function(){
document.getElementById("myDiv").innerHTML = "Processing...";
}
</script>


应改成下面

复制代码 代码如下:

<div id="myDiv">
<input type="button" value="Click me" id="myBtn">
</div>
<script type="text/javascript">
var btn = document.getElementById("myBtn");
btn.onclick = function(){
btn.onclick = null;
document.getElementById("myDiv").innerHTML = "Processing...";
}
</script>


或者采用事件委托

复制代码 代码如下:

<div id="myDiv">
<input type="button" value="Click me" id="myBtn">
</div>
<script type="text/javascript">
document.onclick = function(event){
event = event || window.event;
if(event.target.id == "myBtn"){
document.getElementById("myDiv").innerHTML = "Processing...";
}
}
</script>


2、

复制代码 代码如下:

var a=document.getElementById("#xx");
var b=document.getElementById("#xxx");
a.r=b;
b.r=a;

 

复制代码 代码如下:

var a=document.getElementById("#xx");
a.r=a;


对于纯粹的 ECMAScript 对象而言,只要没有其他对象引用对象 a、b,也就是说它们只是相互之间的引用,那么仍然会被垃圾收集系统识别并处理。但是,在 Internet Explorer 中,如果循环引用中的任何对象是 DOM 节点或者 ActiveX 对象,垃圾收集系统则不会发现它们之间的循环关系与系统中的其他对象是隔离的并释放它们。最终它们将被保留在内存中,直到浏览器关闭。
3、

复制代码 代码如下:

var elem = document.getElementById('test');
elem.addEventListener('click', function() {
alert('You clicked ' + elem.tagName);
});


这段代码把一个匿名函数注册为一个DOM结点的click事件处理函数,函数内引用了一个DOM对象elem,就形成了闭包。这就会产生一个循环引用,即:DOM->闭包->DOM->闭包...DOM对象在闭包释放之前不会被释放;而闭包作为DOM对象的事件处理函数存在,所以在DOM对象释放前闭包不会释放,即使DOM对象在DOM tree中删除,由于这个循环引用的存在,DOM对象和闭包都不会被释放。可以用下面的方法可以避免这种内存泄露

复制代码 代码如下:

var elem = document.getElementById('test');
elem.addEventListener('click', function() {
alert('You clicked ' + this.tagName); // 不再直接引用elem变量
});


4、

复制代码 代码如下:

function bindEvent()
{
var obj=document.createElement("XXX");
obj.onclick=function(){
//Even if it's a empty function
}
}


闭包非常容易构成循环引用。如果一个构成闭包的函数对象被指定给,比如一个 DOM 节点的事件处理器,而对该节点的引用又被指定给函数对象作用域中的一个活动(或可变)对象,那么就存在一个循环引用。
DOM_Node.onevent -<function_object.[[scope]] -<scope_chain -<Activation_object.nodeRef -<DOM_Node。

形成这样一个循环引用是轻而易举的,而且稍微浏览一下包含类似循环引用代码的网站(通常会出现在网站的每个页面中),就会消耗大量(甚至全部)系统内存。
解决之道,将事件处理函数定义在外部,解除闭包

复制代码 代码如下:

function bindEvent()
{
var obj=document.createElement("XXX");
obj.onclick=onclickHandler;
}
function onclickHandler(){
//do something
}


或者在定义事件处理函数的外部函数中,删除对dom的引用(题外,《JavaScript权威指南》中介绍过,闭包中,作用域中没用的属性可以删除,以减少内存消耗。)

复制代码 代码如下:

function bindEvent()
{
var obj=document.createElement("XXX");
obj.onclick=function(){
//Even if it's a empty function
}
obj=null;
}


5、

复制代码 代码如下:

a = {p: {x: 1}};
b = a.p;
delete a.p;


执行这段代码之后b.x的值依然是1.由于已经删除的属性引用依然存在,因此在JavaScript的某些实现中,可能因为这种不严谨的代码而造成内存泄露。所以在销毁对象的时候,要遍历属性中属性,依次删除。
6. 自动类型装箱转换
别不相信,下面的代码在ie系列中会导致内存泄露

复制代码 代码如下:

var s=”lalala”;
alert(s.length);


s本身是一个string而非object,它没有length属性,所以当访问length时,JS引擎会自动创建一个临时String对象封装s,而这个对象一定会泄露。这个bug匪夷所思,所幸解决起来相当容易,记得所有值类型做.运算之前先显式转换一下:

复制代码 代码如下:

var s="lalala";
alert(new String(s).length);


7、某些DOM操作
IE系列的特有问题 简单的来说就是在向不在DOM树上的DOM元素appendChild;IE7中,貌似为了改善内存泄露,IE7采用了极端的解决方案:离开页面时回收所有DOM树上的元素,其它一概不管。

 

 

 

 

Null是个对象

JavaScript众多类型中有个Null类型,它有个唯一的值null, 即它的字面量,定义为完全没有任何意义的值。其表现得像个对象,如下检测代码:

alert(typeof null); //弹出 'object'

 

NaN是个数值

NaN本意是表示某个值不是数值,但是其本身却又是数值,且不等于其自身,很奇怪吧,看下面的代码:

alert(typeof NaN); //弹出 'Number'
alert(NaN === NaN); //为 false

 

 

无关键字的数组等同于false

 

alert(new Array() == false); //为 true

 

 

replace()可以接受回调函数

这是JavaScript最鲜为人知的秘密之一,v1.3中首次引入。大部分情况下,replace()的使用类似下面:

alert('10 13 21 48 52'.replace(/\d+/g, '*')); //用 * 替换所有的数字

  这是一个简单的替换,一个字符串,一个星号。但是,如果我们希望在替换发生的时候有更多的控制,该怎么办呢?我们只希望替换30以下的数值,该怎么办呢?此时如果仅仅依靠正则表达式是鞭长莫及的。我们需要借助回调函数的东风对每个匹配进行处理。

alert('10 13 21 48 52'.replace(/\d+/g, function(match) {
return parseInt(match) <30?'*' : match;
}));

  当每个匹配完成的时候,JavaScript应用回调函数,传递匹配内容给match参数。然后,根据回调函数里面的过滤规则,要么返回星号,要么返回匹配本身(无替换发生)。

 

FireFox以RGB格式读与返回颜色而非Hex

 

双等号

==操作符比较时会进行类型的强制转换,这意味着它可以比较两个不同类型的对象,在执行比较之前它将会尝试把这两个对象转换成同一个类型,举一个例子:

"1" == 1 //true

然而,这样往往会误导我们,而且我们也不需要这样子来比较。在上面的例子中,我们完全可以先将字符串转换成数字型,然后利用对类型敏感的三重等号(===)来进行比较,如:

Number("1") === 1; //true

或者,更好的是,确保你放在首位的操作数的类型是正确的。

由于双等号具有强制类型转换的行为,所以它会打破一般的传递性规则,这点有点吓人,请看下面的列子:

"" == 0 //true - 空字符串会被强制转换为数字0.
0 == "0" //true - 数字0会被强制转换成字符串"0"
"" == "0" //false - 两操作数都是字符串所以不执行强制转换

如果使用三重等号,上面的三个比较都将返回false。

parseInt不把10作为数字基数

如果你忽略parseInt的第二个参数,那么数字的基数将由下面的规则所决定:

  • 默认基数为10,即按10进制解析
  • 如果数字以0x开头,那么基数为16,即按16进制解析
  • 如果数字以0开头,那么基数为8,即按8进制解析

一个常见的错误是我们让用户输入以0开头的数字,这时候它就按8进制的方式去解析了,于是我们就看到了如下的效果:

parseInt("8"); //8
parseInt("08"); //0

因此,我们很多时候都会指定parseInt的第二个参数,如下所示:

parseInt("8", 10); //8
parseInt("08", 10); //8

ECMAScript5方面的说明:ECMAScript已不再支持8进制的解析假设,另外,如果忽略parseInt的第二个参数将会引起JSLint的警告。

字符串替换

字符串替换函数仅仅会替换第一个匹配项,并不能替换你所期望的全部匹配项。如下代码:

"bob".replace("b", "x"); // "xob"
"bob".replace(/b/, "x"); // "xob" (使用了正则表达式)

如果要替换所有的匹配项,我们可以使用正则表达式,并为他它添加全局修饰符,如下代码:

"bob".replace(/b/g, "x"); // "xox"
"bob".replace(new RegExp("b", "g"), "x"); // "xox" (alternate explicit RegExp)

全局修饰符确保了替换函数找到第一个匹配项后不会停止对下一个匹配项的替换。

“+"操作符会执行相加操作和字符串连接操作

php作为另一种弱类型语言,可以使用”.“操作符对字符串进行连接。Javascript却不是这样的 - 所以当操作数是字符串的时候”a+b“通常是执行连接操作。如果你想执行数字相加那你就要引起注意了,因为输入的内容可能是字符串类型的,所以你在执行相 加操作前需要先将其转换成数字类型,代码如下:

1 + document.getElementById("inputElem").value; // 连接操作
1 + Number(document.getElementById("inputElem").value); // 相加操作

需要注意的是,相减操作会尝试将操作数转换成数字类型,代码如下:

"3" - "1"; // 2

尽管有时候你想用减法将字符串从另一个字符串中减掉,但这时候往往会产生一些逻辑错误。

很多时候我们用数字和空串相加来实现数字转换成字符串的操作,代码如下:

3 + ""; // "3"

但是这样做并不好,所以我们可以用String(3)来取代上面的方法。

typeof

typeof这会返回一个javascript基本类型的实例的类型。Array实际上不是基本类型,所以typeof Array对象将返回Object,代码如下:

typeof {} === "object" //true
typeof "" === "string" //true
typeof [] === "array"; //false

当你自己的对象实例使用这个操作符时将会得到相同的结果(typeof = "object")。

另外说明一点,”typeof null“将返回”object“,这个有点诡异。

instanceof

instanceof返回指定对象是否是由某个类构造的实例,这个对我们检查指定对象是否是自定义类型之一很有帮助,但是,如果你是用文本语法创建的内置类型那可能会得出错误的结果,代码如下:

"hello" instanceof String; //false
new String("hello") instanceof String; //true

由于Array实际上不是内置类型(只是伪装成内置类型 - 因此对它使用typeof不能得到预期的结果),但是使用instanceof就能得到预期效果了,代码如下所示:

["item1", "item2"] instanceof Array;  //true
new Array("item1", "item2") instanceof Array; //true

唉,不爽!总的来说,如果你想测试Boolean, String, Number, 或者Function的类型,你可以使用typeof,对于其他的任何类型,你可以使用instanceof测试。

哦,还有一点,在一个function中,有一个预定义变量叫“arguments”,它以一个array的形式传递给function。然而,它并不是真正的array,它只是一个类似array的对象,带有长度属性并且属性值从0-length。非常奇怪...你可以用下面的小伎俩将它转换成真正的数组:

var args = Array.prototype.slice.call(arguments, 0);

这个对由getElementsByTagName返回的NodeList对象也是一样的 - 它们都可以用以上的代码转换成合适的数组。

eval

eval 可以将字符串以javascript代码的形式来解析执行,但是一般来说我们不建议这么做。因为eval非常慢 - 当javascript被加载到浏览器中时,它会被编译成本地代码;然而执行的过程中每次遇到eval表达式,编译引擎都将重新启动执行编译,这样做的代价太大了。而且这样做也丑陋无比,有很多eval被滥用的例子。另外,在eval中的代码会在当前范围内执行,因此它可以修改局部变量,以及在你的范围内添加一些让你意想不到的东西。

JSON 转换是我们经常要做的;通常我们使用“var obj = eval(jsonText);”来进行转换。然而现在几乎所有的浏览器都支持本地JSON对象,你可以使用“var obj = JSON.parse(jsonText);”来替代前面的代码。相反你也可以用“JSON.stringify”将JSON对象转换成字符串。更妙的是,你可以使用“jQuery.parseJSON”来完成上述的工作。

setTimeout和setInterval函数的第一个参数可以用字符串作为函数体来解析执行,当然,我们也不建议这样做,我们可以用实际的函数来替代。

最后,Function的构造函数和eval非常像,唯一不同的是,Function构造函数是在全局范围内执行的。

with

with表达式将为你提供访问对象属性的速记方式,但我们是否应该使用它,仍然存在矛盾的观点。Douglas Crockford不太喜欢它。John Resig在他的书中有找了很多with的巧妙用法,但是他也承认这将会影响性能并且会产生一点混乱。来看看我们分离出来的with代码块,他不能准确地告诉我们现在正在执行什么,代码如下所示:

with (obj) {
bob = "mmm";
eric = 123;
}

我是否刚刚修改了一个叫bob的局部变量?或者我是否设置了obj.bob?如果obj.bob已经被定义,那么它将会被重置为“mmm”。否则,如果有 另一个bob在这个范围中,那么他将会被改变。否则,全局变量bob会被设置。最后,下面的写法可以非常明确地表达你的意思:

obj.bob = "mmm";
obj.eric = 123;

ECMAScript5说明:ES5严格的来说已经不支持with表达式。

类型和构造函数

使用“new”关键字构造内置类型

Javascript中有Object, Array, Boolean, Number, String, 和Function这些类型,他们各自都有各自的文字语法,所以就不需要显式构造函数了。

显式构造(不建议)文字语法(推荐)
var a = new Object();
a.greet = "hello";
var a = { greet: "hello" };
var b = new Boolean(true); var b = true;
var c = new Array("one", "two"); var c = ["one", "two"];
var d = new String("hello"); var d = "hello"
var e = new Function("greeting", "alert(greeting);"); var e = function(greeting) { alert(greeting); };

然而,如果你使用new关键字来构造上面其中的一种类型,你实际上将会得到一个类型为Object并且继承自你要构造的类型的原型的对象(Function类型除外)。所以尽管你用new关键字构造了一个Number类型,它也将是一个Object类型,如下代码:

typeof new Number(123); // "object"
typeof Number(123); // "number"
typeof 123; // "number"

上面的第三项是文本语法,为了避免冲突,我们应该使用这种方法来构造上面的这些类型。

使用“new”关键字来构造任何东西

如果你自写构造函数并且忘记了new关键字,那么悲剧就发生了:

复制代码
var Car = function(colour) {
this.colour = colour;
};

var aCar = new Car("blue");
console.log(aCar.colour); // "blue"

var bCar = Car("blue");
console.log(bCar.colour); // error
console.log(window.colour); //"blue"
复制代码

使用new关键字调用函数会创建一个新的对象,然后调用新对象上下文中的函数,最后再返回该对象。相反的,如果不使用new关键在调用函数,那它将会变成一个全局对象。

偶然忘记使用new关键字意味着很多可选择的对象构造模式已经出现可以完全删除使用这个关键字的需求的情况,尽管这超出了本文的范围,但我还是建议你去进一步阅读

没有Integer类型

数值计算是相对缓慢的,因为没有Integer类型。只有Number类型 - Number是IEEE标准中双精度浮点运算64位)类型。这就意味着Number会引起下面的精度舍入错误:

0.1 + 0.2 === 0.3 //false
1.1 + 0.2 === 1.3 //true

因为integers和floats没有区别,不像C#和JAVA下面代码是true:

0.0 === 0; //true

最后是一个关于Number的疑问,我们该如何实现下面的问题:

a === b; //true
1/a === 1/b; //false

答案是按照Number的规范是允许出现+0和-0的,+0等于-0,但是正无穷大不等于负无穷大,代码如下:

var a = 0 * 1; // 这个结果为0
var b = 0 * -1; // 这个结果为-0 (你也可以直接"b=-0",但是你为何要这样做?)
a === b; //true: 0等于-0
1/a === 1/b; //false: 正无穷大不等于负无穷大

作用域

没有块作用域

因为你可能已经注意到上一个观点,javascript中没有块作用域的概念,只有函数作用域。可以试试下面的代码:

for(var i=0; i<10; i++) {
console.log(i);
}
var i;
console.log(i); // 10

当i被定义在for循环中,退出循环后它人被保留在这个作用域内,所以最后调用console.log输出了10。这里有一个JSLint警告来让你避免这个问题:强制将所有的变量定义在函数的开头。 我们有可能通过写一个立即执行的function来创建一个作用域:

复制代码
(function (){
for(var i=0; i<10; i++) {
console.log(i);
}
}());
var i;
console.log(i); // undefined
复制代码

当你在内部函数之前声明一个变量,然后在函数里重声明这个变量,那将会出现一个奇怪的问题,示例代码如下:

var x = 3;
(function (){
console.log(x + 2); // 5
x = 0; //No var declaration
}());

但是,如果你在内部函数中重新声明x变量,会出现一个奇怪的问题:

var x = 3;
(function (){
console.log(x + 2); //NaN - x is not defined
var x = 0; //var declaration
}());

这是因为在函数中x变量被重新定义了,这说明了翻译程序将var表达式移动到了函数顶部了,最终就变成这样执行了:

var x = 3;
(function (){
var x;
console.log(x + 2); //NaN - x is not defined
x = 0;
}());

这个实在是太有意义了!

全局变量

Javascript 有一个全局作用域,在为你的代码创建命名空间时一定要小心谨慎。全局变量会给你的应用增加一些性能问题,因为当你访问它们时,运行时不得不通过每一个作用域来建立知道找到它们为止。他们会因你的有意或者无意而被访问或者修改,这将导致另外一个更加严重的问题 - 跨站点脚本攻击。如果一个不怀好意的家伙在你的页面上找出了如何执行那些代码的方法,那么他们就可以通过修改全局变量非常容易地扰乱你的应用。缺乏经验的开发者在无意中会不断的将变量添加到全局作用域中,通过本文,将会告诉大家这样会发生什么意外的事情。

我曾经看到过下面的代码,它将尝试声明两个值相等的局部变量:

var a = b = 3;

这样非常正确的得到了a=3和b=3,但是a在局部作用域中而b在全局作用域中,”b=3“将会被先执行,全局操作的结果,3,再被分配给局部变量a。

下面的代码声明了两个值为3的变量,这样能达到预期的效果:

var a = 3,
b = a;

”this“和内部函数

”this“关键字通常指当前正在执行的函数所在的对象,然而,如果函数并没有在对象上被调用,比如在内部函数中,”this“就被设置为全局对象(window),如下代码:

复制代码
var obj = {
doSomething: function () {
var a = "bob";
console.log(this); // 当前执行的对象
(function () {
console.log(this); // window - "this" is reset
console.log(a); // "bob" - still in scope
}());
}
};
obj.doSomething();
复制代码

杂项

数据不存在:”null“和”undefined“

有两种对象状态来表明数据不存在:null和undefined。这会让那些从其他编程语言比如C#转过来的程序员变得相当混乱。也许你会期望下面的代码返回true:

var a;
a === null; //false
a === undefined; //true

”a“实际上是undefined的(尽管你用双等号==来与null比较会得出true的结果,但这只是表面上看起来正确的另一个错误)。

如果你想检查一个变量是否真的存在值,那你不能用双等号==去判断,要用下面的方法:

if(a !== null && a !== undefined) {
...
}

”哈“,你也许会说,既然null和undefined都是false,那么你可以这样去做:

if(a) {
...
}

当然,0是false,空字符串也是。那么如果这其中一个是a的正确的值的话,你就要用前者了。那种比较短小的比较方式,适合于比较objects, arrays, 和booleans类型。

重定义undefined

非常正确,你可以重定义undefined,因为它不是一个保留字:

undefined = "surprise!";

但是,你要通过给undefined变量分配一个值或者使用”void“操作符来取回值(否则这是相当没用的)。

undefined = void 0;

这就是为什么jquery脚本库的第一行要这样写了:

(function ( window, undefined ) {
... // jQuery library!
}(window));

这个函数被调用时是传入一个参数的,同时确保了第二个参数”undefined“实际上是undefined的。

顺便说一下,你不能重定义null - 但是你可以重定义NaN,Infinity和带构造函数的内置类型。可以这样尝试一下:

Array = function (){ alert("hello!"); }
var a = new Array();

当然,你可以在任何地方用文字语法声明Array。

可选的分号

Javascript代码中分号是可选的,所以初学者写代码就简单多了。但是很不幸的是如果忽略了分号并不会给任何人带来方便。结果是当解释器遇到错误时,必须追溯并尝试去猜测因为哪些分号漏写导致的问题。

这里有一个经典的例子:

return
{
a: "hello"
};

上面的代码并不会返回一个对象,而是返回了undefined - 但是也没有错误抛出。其实是因为分号自动加到了return语句后面,其他的代码都是非常正确的,但是就是什么都不执行,这就证明了在 javascript中,左花括号应该紧跟这一行而不该换行,这不只是一个编程风格的问题。下面的代码才会正确返回一个属性为a的对象:

return {
a: "hello"
};

NaN

NaN的类型是...Number

typeof NaN === "number" //true

另外NaN和任何东西比较都是false:

NaN === NaN; // false

因为NaN之间是不能比较的,唯一判断一个数字是否为NaN的方法是调用isNaN方法。

从另一个方面可以说明,我们也可以用函数isFinite,当其中一个操作数为NaN或者InFinity时返回false。

arguments对象

在一个函数中,我们可以引用arguments对象来遍历传入的参数列表,第一个比较怪异的地方是这个对象并不是Array,而是一个类似 Array的对象(有一个length属性,其值在0-length-1之间)。为了将其转换成array,我们可以array的splice函数来创建 其对应的array数组:

(function(){
console.log(arguments instanceof Array); // false
var argsArray = Array.prototype.slice.call(arguments);
console.log(argsArray instanceof Array); // true
}());

第二个比较怪异的地方是当一个函数的签名中有显式arguments参数时,它们是可以被重新分配的并且arguments对象也会被改变。这就表明了arguments对象指向了变量本身。你不能利用arguments对象来给出它们的初始值:

(function(a){
alert(arguments[0]); //1
a = 2;
alert(arguments[0]); //2
}(1));
posted @ 2012-12-02 16:45  awp110  阅读(338)  评论(0编辑  收藏  举报