JS异常处理与应用

一、简介
作为Web开发者,经常要跟JS打交道,可能由于网络超时、浏览器兼容性问题、缓存等原因,造成浏览器执行页面JS脚本时,会抛出异常。对Web开发者来说,JS异常很常见,但对于用户来说,JS异常却很陌生。因此如果能收集异常,提供统一的异常处理方式和友好的异常提示方式,将能大大提供用户体验。此外,如果能够及时的跟踪和统计页面的JS异常,对于开发者来说是一个福音,不仅能够快速地对异常进行响应还能对异常进行归档分析。

二、常见的JS异常

  • EvalError
  • RangeError
  • ReferenceError
  • SyntaxError
  • TypeError
  • URIError

三、JS异常捕获

捕获浏览器中的JS运行时错误,主要通过监听window.onerror事件和try/catch来实现。

1、window.onerror : 对于不同的脚本执行方式以及不同的浏览器,捕获到的信息会有区别

window.onerror 接收3个参数:

  • msg:错误描述,比如:a is not defined
  • url:出错脚本所在的url
  • lineNumber:出错脚本的行数

若使用window.addEventListener(‘error’,callback,false),callback的第一个参数并不是event对象而是Error对象。因此使用window.onerror是一个不错的选择,但使用.操作符进行事件监听是可以重载的,并且包含异常处理的脚本理论上要放在所有JS之前。

2、try/catch : 可通过e.stack打印异常信息

  try{
      fn()
  }catch(e){
      alert(e.stack);
  }  

本文将对不同浏览器和不同的脚本执行方式进行测试,并总结这些区别。

首先对于脚本的执行主要有以下几种方式:

  • 页面内嵌的<script>,需要执行的代码在<script>标签内
  • 使用<script src="external.js">的方式引入外部脚本,脚本为同域地址
  • 使用<script src="external.js">的方式引入外部脚本,脚本为不同域地址
  • 使用<script src="external.js">的方式引入外部脚本,脚本为本地地址
  • 使用eval方法来执行脚本
  • 动态地创建内嵌的<script>并设置其innerHTML为需要执行的代码

四、异常的利用

1、线上实际运行时,若出现JS错误,JS报错信息自动已邮件的方式发送至前端开发团队,团队成员每个人自己认领、解决问题。这种方式解决了最基本的问题,错误的及时跟踪和响应。不过也存在一个问题,如何避免同样的错误。

解决思路如下:

  • 以URL为单元,记录同一个页面的错误,方便统一解决
  • 记录错误信息: PageURL、User Agent、Script URL、Error Message和Line Number
  • 每个错误解决后,统一对错误进行统计和归档,形成知识库

2、注意点

  • 收集数据的时候使用POST方式: 有时候ErrorMessage可能会比较长,浏览器URL长度是有限制的,若错误信息不多的情况,可以使用GET发送,但通常来说POST可以把所有数据都发送到后台
  • 何时发送数据: 建议在触发onerror的时候发送
  • 数据入库以哪个作为索引比较好: 通常来说使用URL可能会比较适合多数网站,也可以使用Error作为索引,具体根据实际场景进行选择

五、简单示例

   window.onerror = function(msg,url,lineNumber){
        var msgText = ['####异常行为号: '+lineNumber,
          '异常信息为: '+msg,'异常发生的地址为: '+url].join('\n');
        if (window.console && window.console.log){
            window.console.log(msgText);
        }else{    
            alert(msgText);
        }
    };

六、附录

1、兼容多数浏览器的实现库

2、以下是各浏览器执行各类脚本的异常捕获情况
Chrome (23.0.1271.101)








page script external script ( same origin ) external script ( cross domain ) external script ( local ) eval dynamic page script
msg

✗(only Script error)
✗(only Script error)


url ✓(current page)

✗(””)
✗(””)
✓(current page)
✓(current page)
lineNumber ✓(from current page)

✗(0)
✗(0)
✓(from code)
✓(from its code)

Firefox (16.0.2)








page script external script ( same origin ) external script ( cross domain ) external script ( local ) eval dynamic page script
msg

✗(only Script error)
✗(only Script error)


url ✓(current page)



✓(file that call this eval)
✓(current page)
lineNumber ✓(from current page))

✗(0)
✗(0)
✓(position that calls this eval)
✓(from its code)

Safari (6.0.2 (8536.26.17))








page script external script ( same origin ) external script ( cross domain ) external script ( local ) eval dynamic page script
msg

✗(only Script error)



url ✓(current page)



✗(undefined)
✓(current page)
lineNumber ✓(from current page)

✗(0)

✓(from code)
✓(from its code)

Opera (12.11)








page script external script ( same origin ) external script ( cross domain ) external script ( local ) eval dynamic page script
msg

✗(only Script error)
-

url ✓(current page)

✗(””)
- ✗(””)
✓(current page)
lineNumber ✓(from current page)

✗(0)
- ✓(from code)
✓(from its code)

IE9








page script external script ( same origin ) external script ( cross domain ) external script ( local ) eval dynamic page script
msg





url ✓(current page)



✓(file path that call this eval)
✓(current page)
lineNumber ✓(from current page)



✓(position that calls this eval)
✓(from its code)

IE8








page script external script ( same origin ) external script ( cross domain ) external script ( local ) eval dynamic page script(not available)
msg




-
url ✓(current page)



✓(file path that call this eval)
-
lineNumber ✓(from current page)



✓(position that calls this eval)
-

IE7








page script external script ( same origin ) external script ( cross domain ) external script ( local ) eval dynamic page script(not available)
msg




-
url ✓(current page)



✓(file path that call this eval)
-
lineNumber ✓(from current page)



✓(position that calls this eval)
-

IE6








page script external script ( same origin ) external script ( cross domain ) external script ( local ) eval dynamic page script(not available)
msg




-
url ✓(current page)
✓(current page)
✓(current page)
✓(current page)
✓(current page)
-
lineNumber ✓(from current page)
✓(line number start from 1)
✓(line number start from 1)
✓(line number start from 1)
✓(position that calls this eval,line number start from 1)
-

高性能javascript之响应接口

响应接口

大多数浏览器有一个单独的处理进程,它由两个任务共享:Javascript任务和用户界面更新任务。每个时刻只有其中一个操作得以执行,也就是说当Javascript代码运行时用户界面不能对输入产生反应,反之亦然,当Javascript运行时,用户界面就被锁定了。

浏览器UI线程
JavaScript和UI更新共享的进程通常被称为浏览器UI线程。此UI线程维护着一个简单的队列系统,任务被保存到队列中直至进程空闲。一旦空闲,队列中的下一个任务将被检索和运行。这些任务不是运行Javascript代码就是执行UI更新,包括重绘和重排版。此进程中最令人感兴趣的部分是每次输入均导致一个或多个任务被加入。

<html> 
<head>
<title>Browser UI Thread Example</title>
 </head>
<body>
<button onclick="handleClick()">Click Me</button> <script type="text/javascript">
    function handleClick(){
    var div = document.createElement("div"); 
    div.innerHTML = "Clicked!";     
     document.body.appendChild(div);
} 
</script>
</body>
 </html>

当例子中按钮被点击时,它触发UI线程创建两个任务并添加到队列中。第一个任务是按钮UI更新,它需要做出相应的改变以指示它被按下了;第二个任务是Javascript运行任务,包含handleClick()的代码。假设UI线程空闲,第一个任务被检查并运行以更新按钮外观,然后Javascript任务被检查和运行。在运行过程中handleClick()创建了一个新的div元素,并追加到body元素上,其效果是引发另一次UI界面改变,也就是说在Javascript运行过程中,一个新的UI更新任务被添加队列中,当Javascript运行完后,UI还会再更新一次。
当所有UI线程任务执行之后,进程进入空闲状态,并等待更多任务添加到队列中。空闲状态是理想的。因为所有用户操作会立刻引发一次UI更新。
浏览器在Javascript运行时间上采取了限制,确保恶意代码编写者不能通过无尽的密集操作锁定用户和计算机。此类限制有两个:调用栈尺寸限制和长时间脚本限制。长运行脚本限制有时被称作长运行脚本定时器或者失控脚本控制器,但其基本思想是浏览器记录一个脚本的运行时间,一旦到达一定限度就终止它。

高性能javascript之DOM编程

DOM编程

对DOM操作代价昂贵,在富应用中通常是一个性能瓶颈。
文档对象模型(DOM)是一个独立于语言,使用XML和HTML文档操作的应用程序接口。在浏览器中,主要与HTML打交道,在网页应用中检索XML文档也很常见。DOM APIs主要用于访问这些文档中数据。
浏览器通常要求DOM实现和Javascript实现保持相互独立,例如在IE中,被称为JScript的Javascript实现位于库文件jscript.dll中,而DOM实现位于另一个库mshtml中。这样导致的问题是:简单来说,两个独立的部分以功能接口连接就会带来性能损耗。一个很形象的比喻是把DOM看成一个岛屿,把javascript看成另一个岛屿,两者之间以一座收费桥连接。每次ECMAScript需要访问DOM时,你需要过桥,交一次过桥费。你操作DOM次数越多,费用就越高。
简单来说,正如前面所讨论的那样,访问一个DOM元素的代价就是交一次过桥费。修改元素的费用可能更贵,因为它经常导致浏览器重新计算页面的几何变化。
为了给你一个关于DOM操作问题的量化印象,考虑下面的例子:

function innerHTMLoop(){
    for(var count = 0;count <= 15000; count++){
        document.getElementById('here').innerHTML += 'a';
    }
} 

此函数在循环中更新页面内容。这段代码的问题是,在每次循环单元中都对DOM元素访问两次:一次是读取innerHTML属性内容,另一次写入它。
一个更有效率版本是将使用局部变量存储更新后的内容,在循环结束后一次性写入:

function innerHTMLLoop2(){
   var content = '';
   for(var count = 0;count <= 15000; count++){
        content += 'a';
    }
    document.getElementById('here').innerHTML += content;

}

HTML集合

//无效的死循环
var alldivs = document.getElementsByTagName('div'); 
for (var i = 0; i < alldivs.length; i++) {
  document.body.appendChild(document.createElement('div'));
}  

//slow
function collectionGlobal() {
var coll = document.getElementsByTagName('div'), 
len = coll.length,
name = '';
for (var count = 0; count < len; count++) {
 name = document.getElementsByTagName('div')[count].nodeName; 
 name = document.getElementsByTagName_r('div')[count].nodeType; 
 name = document.getElementsByTagName_r('div')[count].tagName;
}
 return name; 
};

// faster
function collectionLocal() {
var coll = document.getElementsByTagName('div'), len = coll.length,
name = '';
for (var count = 0; count < len; count++) {
name = coll[count].nodeName; name = coll[count].nodeType; name = coll[count].tagName;
}
return name;
};

//fastest
function collectionNodesLocal() {
var coll = document.getElementsByTagName_r('div'), len = coll.length,
name = '',
el = null;
for (var count = 0; count < len; count++) {
el = coll[count]; name = el.nodeName; name = el.nodeType; name = el.tagName;
}
 return name;
};  

重绘和重排版
当浏览器下载完所有HTML标记,javascript、css,图片之后,它解析文件并创建两个内部数据结构:一个DOM树表示页面结构和一颗渲染树表示DOM节点如何显示。渲染树中为每个需要显示的DOM树节点存放至少一个节点。渲染树的节点称为”框”或者”盒”,符合CSS模型定义,将页面元素看作一个具有填充、边距、边框和位置的盒子。一旦DOM树和渲染数构造完毕,浏览器就可以显示(绘制)页面上的元素了。
当DOM改变影响到元素的几何属性(宽和高)-例如改变了边框高度或者在段落中添加文字,将发生一系列后续动作。浏览器需要重新计算元素的几何属性,而且其他元素的几何属性和位置也会因此改变受到影响。浏览器是渲染数上受到影响的部分失效,然后重构渲染数。这个过程称为重排版,重排版完成时,浏览器在一个重绘进程中重新绘制屏幕上受到影响的部分。
不是所有的DOM改变都会影响几何尺寸。例如,改变一个元素的背景颜色不会影响它的宽度和高度。在这种情况下,只需重绘不需要重排版,因为元素的布局没有改变。
重绘和重排版是负担很重的操作,可能导致网页应用的用户界面失去响应。

以下操作会发生重排版:
添加、删除可见的DOM元素、元素位置改变、元素尺寸改变(边距、填充、边框高度等属性改变)、内容改变、浏览器窗口尺寸改变
因为计算量与每次重排版有关,大多数浏览器通过队列化修改和批量显示优化重排版过程。然后获取布局信息的操作将导致刷新队列的动作。offsetTop、offsetLeft、offsetWidth、offsetHeight、scrollTop、clientTop。改变风格时最好不用使用这些属性。任何一个访问都将刷新渲染队列。

最小化重绘和重排版

//改变风格
var el = document.getElementById('mydiv'); 
el.style.borderLeft = '1px'; 
el.style.borderRight = '2px';
el.style.padding = '5px';

这里改变了三个风格属性,每次改变都影响到元素的几何属性。在这个例子中,它导致浏览器chong排版了三次。一个达到同样效果而效率更高的方法是:将所有改变合并在一起执行,只修改DOM一次。可以通过使用cssText属性实现:

 var el = document.getElementById('mydiv');
 el.style.cssText = 'border-left: 1px; border-right: 2px; padding: 5px;';

另一个一次性改变风格的方法是修改css的类名称,而不是改变内联风格代码。

批量修改DOM
有三种基本方法可以将DOM元素从文档中摘除:
1、隐藏元素,进行修改,然后再显示它
2、使用一个文档片段在已存在DOM元素之外创建一个子数,然后将它拷贝到文档中。
3、将原始元素拷贝到一个脱离文档的节点中,修改副本,然后覆盖原始元素。

高性能Javascirpt之数据访问

第二章:数据访问

在Javascript中有四种基本的数据位置:直接量、变量、数组项、对象成员,对一种数据存储位置都具有特定的读写操作负担。总的来说,对直接量和局部变量的访问速度要快于数组项和对象成员的访问速度。
作用域链和标识符解析
函数内部的[[Scope]]属性包含了一个函数被创建的作用域中对象的集合。此集合称为函数的作用域链,它决定哪些数据可由函数访问。此函数作用域链中的每个对象被称为一个可变对象。

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

当add()函数创建后,它的作用域链中填入了一个单独的可变对象,此全局对象代表了所有全局范围定义的变量。此全局变量包含了诸如窗口、浏览器和文档之类的访问接口。
函数作用域链将在运行时用到。如 var total = add(5,10);运行此add函数时,建立一个内部对象,称作”运行期上下文”。一个运行期上下文定义了一个函数运行时的环境。对函数的每次运行而言,每个运行时的上下文都是独一的,所以多次调用同一个函数就会导致多次创建运行期上下文。当函数执行完毕,运行期上下文就会被销毁。

一个运行期上下文有它自己的作用域链,用于标识符解析。当运行期上下文被创建时,它的作用域链被初始化,连同运行函数的[[Scope]]属性中所包含的对象。这些值按照它们出现在函数中的顺序,被复制到运行期上下文的作用域链中。这项工作一旦完成,一个被称作”激活对象”的新对象就为运行期上下文创建好了。
在函数运行过程中,每遇到一个变量,标识符识别过程要决定从哪里获得或者存储数据,此过程搜索运行期上下文的作用域链,查找同名的标识符。搜索工作从运行函数的激活目标之作用域链的前端开始。如果找到后,直接返回,若没有找到则进入作用域链的下一个对象。
在运行期上下文的作用域链中,一个标识符所处的位置越深,它的读写速度就越慢。所以,函数中局部变量的访问速度总是最快的,而全局变量的访问通常是最慢的。全局变量总是处于运行期上下文作用域链的最后一个位置。

function initUI(){
var bd = document.body,
links = document.getElementsByTagName_r("a"),
 i = 0,
len = links.length;
while(i < len){
update(links[i++]); }
    document.getElementById("go-btn").onclick = function(){ start();
};
bd.className = "active"; 
}  

此函数包含三个对document的引用,document是一个全局对象、搜素此变量,必须遍历整个作用域链,直到最后在全局变量对象中找到它。你可以通过这种方法减轻重复的全局变量访问对象性能的影响:首先将全局变量的引用存储在一个局部变量中,然后使用整个局部变量代替全局变量。

function initUI(){
var doc = document,
bd = doc.body,
links = doc.getElementsByTagName_r("a"),
i = 0,
len = links.length; while(i < len){
update(links[i++]); 
}
    doc.getElementById("go-btn").onclick = function(){ start();
};
    bd.className = "active"; 
}  

改变作用域链
一般来说,一个运行期上下文的作用域链不会被改变。但是,有两种表达式可以在运行期临时改变运行期的上下文作用域链。第一个是with表达式。
with表达式为所有对象属性创建一个默认操作变量。在其他语言中通常用来避免书写一些重复代码。当代码流执行到一个with表达式时,运行期上下文的作用域链被临时改变了。一个新的可变对象将创建,它包含指定对象的所有属性。此对象被插入到作用域链的前端,意味着现在函数的所有局部变量都被推入第二个作用域链对象中,所以访问代价更高了。
在Javascript中不只是with表达式人为地改变运行期上下文的作用域链,try-catch表达式的catch子句具有相同的效果。当try块发生错误时,程序流程自动转入catch块,并将异常对象推入作用域链前端的一个可变对象中。在catch块中,函数的所有局部变量现在被放在第二个作用域链对象中。例如:

try { 
    methodThatMightCauseAnError();
} catch (ex){
    alert(ex.message); //scope chain is augmented here
}

只要catch子句执行完毕,作用域链就会返回到原来的状态。在程序中可以通过精简代码的办法最小化catch子句对性能的影响。一个很好的模式是将错误交给一个专用函数来处理。例子如下:

try { 
    methodThatMightCauseAnError();
} catch (ex){
    handleError(ex);//delegate to handler method
}  

handleError()函数是catch子句中运行的唯一代码。此函数以适当的方法自由地处理错误,并接收由错误产生的异常对象。由于只有一条语句,没有局部变量访问,作用域链临时改变就不会影响代码的性能。

动态作用域
无论是with表达式还是try-catch表达式的catch子句,以及包含()的函数,都被认为是动态作用域。一个动态作用域只因代码运行而存在,因此无法通过静态分析(查看代码结构)来确定。

闭包、作用域和内存
闭包是Javascript最强大的一个方面,它允许函数访问局部变量之外的数据。

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

assignEvents函数为一个DOM元素指定了一个事件处理句柄。此事件句柄是一个闭包,当assignEvents()执行时创建,可以访问其范围内部的id变量。用这种方法封闭了对id变量的访问,必须创建一个特殊的作用域链。
在assignEvents()被执行时,一个激活对象被创建,并包含了一些应有的内容,其中包含id变量。它将成为运行期上下文作用域链上的第一个对象,全局对象是第二个。当闭包创建时,[[Scope]]属性与这些对象一起被初始化。
由于闭包的[[Scope]]属性包含与运行期上下文作用域链相同的对象引用,会产生副作用。通常,一个函数的激活对象与运行期上下文一起销毁。当涉及闭包时,激活对象就无法销毁,因为引用仍存在与闭包的[[Scope]]属性中。这意味着脚本中的闭包和非闭包函数相比,需要更多的内存开销。
当闭包被执行时,一个运行期上下文被创建,它的作用域链与[[Scope]]中引用的两个相同的作用域链同时被初始化,然后一个新的激活对象为闭包自身被创建。

对象成员
对象可以有两种类型的成员:实例成员和原形成员。实例成员直接存于实例自身,而原形成员则从对象原形继承。
一般来说,如果在同一个函数中你要多次读取同一个对象属性,最好将它存入一个局部变量。以局部变量替代属性,避免多余的属性查找带来性能开销。

jQuery内核详解和实战

一、使用jQuery选择器应该注意的问题

1、多用ID选择器:
2、少直接使用Class选择器
3、多用父子关系,少用嵌套关系(使用parent>child替代parent child)
4、缓存jQuery对象
5、选择器分类:基本选择器、组合选择器、属性选择器、伪类选择器、用户界面选择器、结构性选择器、其他选择器
6、jQuery选择器返回的永远是一个数组对象,但是若没有找到指定的元素,则会返回一个空的数组对象。因此判断一个jQuery对象是否存在,不能使用如下语句if($(“tr”)){//code}
7、getElementsByTagName返回数组

二、类数组(Array-Like)

1、get(index)方法读取集合中的元素,它与直接通过[i]来读取元素的方法是完全相同的
2、eq(index)方法克隆集合中的元素,也就是说不修改数组元素

三、this

1、this绑定与函数声明无关,与函数的调用方式有关
2、函数调用的this指向全局对象,若使用”use strict”的话,this将被设置为undefined
3、典型使用硬绑定的方式var bar = function(){ return foo.apply(obj,arguments);};
4、var bar = new foo();//this->constructed object
5、var bar = foo.call(obj2);//this->obj2
6、var bar = obj1.foo();//this->obj1
7、var bar = foo();//this指向全局对象

四、Javascript之OOP

1、在Javascript面向对象编程中存在许多层:简单对象、对象原型链、构造函数、构造器的继承
2、删除操作只针对与当前对象

Javascript Array对象详解

一、数组的定义

1.1、一维数组

方法1:

    var myCars = new Array();        
    myCars[0] = "Benz";
    myCars[1] = "BMW";
    myCars[2] = "Saab";

方法2:

定义和初始化一起:

    var myCars = new Array("Benz","BMW","Saab");

    或者另一种写法:

    var myCars = ["Benz","BMW","Saab"]; //推荐         

1.2、二维数组

方法1、

    var arr = new Array(['a','b'],['c','d']);

    arr[0]返回第一个一维数组,arr[0][0]返回第一个一维数组的第一个元素'a'

方法2、

      var arr = new Array();
      for(var i=0;i<10;i++){
          arr[i] = new Array(i);
      }    

二、常用属性和方法详解

length
设置或返回数组中元素的数目

注意:设置length属性可改变数组的大小。如果设置的值比当前值小,数组将被截断。其尾部的元素即将丢失。如果设置的值比它当前值大,新的元素被添加到数组尾部,它们的值为undefined。所以length不一定代表数组元素的个数。

    var arr = new Array("John","Andy","Wendy");

    console.log(arr);// ["John","Andy","Wendy"]
    console.log(arr.length);// 3

    arr.length = 2;

    console.log(arr);// ["John","Andy"]
    console.log(arr.length);// 2

    arr.length = 5;

    console.log(arr);// ["John","Andy","undefined","undefined","undefined"]
    console.log(arr.length);// 5  

concat()
concat()方法用于连接两个或多个数组。该方法不会改变现有的数组,仅仅返回被连接数组的一个副本。

arrayObject.concat(array1,array2,…,arrayn);

注意:参数是必须的。可以是具体的值,也可以是数组对象,可以任意多个。结果返回一个新的数组

    var a = [1,2,3];

    var b = a.concat(4,5);

    var c = a.concat(4,5,[6,7],8,"9");  

    console.log(a);// [1,2,3]
    console.log(b);// [1,2,3,4,5]
    console.log(c);// [1,2,3,4,5,6,7,8,"9"]

join()
join()方法用于把数组的所有元素放入一个字符串,元素通过指定的分隔符进行分隔。

arrayObject.join(separator)

注意:参数代表分隔符,是可选的,默认是半角逗号。返回值是字符串不是数组。

    var arr = new Array("John","Andy","Wendy");

    console.log(arr);// ["John","Andy","Wendy"]
    console.log(arr.join());// "John,Andy,Wendy"
    console.log(arr.join(" & "));//"John & Andy & Wendy"  

现代的主流浏览器,都对+运算符做了很好的优化,所以建议使用+运算符代替join()方法,参考 array join vs string connect

pop()
pop()方法用于删除并返回数组的最后一个元素。

注意:pop()方法将改变数组(删除数组的最后一个元素,把数组长度减1),并返回删除的元素值。如果数组已经为空,则pop()不改变数组,并返回undefined。

    var arr = new Array("John","Andy","Wendy");

    console.log(arr);// ["John","Andy","Wendy"]
    console.log(arr.pop());// "Wendy"
    console.log(arr);// ["John","Andy"]
    console.log(arr.pop());// "Andy"
    console.log(arr);// ["John"]
    console.log(arr.pop());// "John"
    console.log(arr);// []
    console.log(arr.pop());// undefined
    console.log(arr);// []          

push()
push()方法可以向数组的末尾添加一个或多个元素,并返回新的长度,也就是添加元素后数组的长度。

arrayObject.push(element1,element2,…,elementn);

注意:push()方法至少需要一个参数。push()方法可把它的参数按顺序添加到arrayObject的尾部。它直接修改arrayObject,而不是创建一个新的数组。push()方法和pop()方法使用数组提供的后进先出栈的功能。push()方法中额参数不管是什么类型(数组、对象),参数将会被作为一个整体插入到arrayObject的尾部,不做拆分。

    var a = ["a","b"];
    var b = {name : "Sem"};
    var c = [1,2,3];

    console.log(a);// ["a","b"]
    console.log(a.push(b));// 3
    console.log(a);    // ["a","b",[object Object]{ name : "Sem"}]
    console.log(a.push(c));// 4
    console.log(a);// ["a", "b", [object Object] { name: "Tom"}, [1, 2, 3]]  

reverse()
reverse() 方法用于颠倒数组中元素的顺序。

注意:该方法会改变原来的数组,而不会创建新的数组。reverse()并非效率最高的数组倒序排列方法,如果你对效率要求更高,可以参考JS: Array.reverse() vs. for and while loops

    var a = ["a","b","c"];
    var b = ["你","我","他"];

    console.log(a.reverse());  //["c", "b", "a"]
    console.log(b.reverse());  //["他", "我", "你"]

shift()
shift() 方法用于把数组的第一个元素从其中删除,并返回第一个元素的值。

注意:如果数组是空的,那么 shift() 方法将不进行任何操作,返回 undefined 值。请注意,该方法不创建新数组,而是直接修改原有的数组。shift() 方法通常比 pop() 方法要慢很多。

     var arr = ["George", "John", "Thomas"];

    console.log(arr);          // ["George", "John", "Thomas"]
    console.log(arr.shift());  // "George"
    console.log(arr);          // ["John", "Thomas"]
    console.log(arr.shift());  // "John"
    console.log(arr);          // ["Thomas"]
    console.log(arr.shift());  // "Thomas"
    console.log(arr);          // []
    console.log(arr.shift());  // undefined  

slice()
slice() 方法可从已有的数组中返回选定的元素。

arrayObject.slice(start, end)

注意:
参数start是必需的,规定从何处开始选取,如果是负数,那么它规定从数组尾部开始算起的位置。也就是说,-1 指最后一个元素,-2 指倒数第二个元素,以此类推。

参数end是可选的,规定从何处结束选取,该参数是数组片断结束处的数组下标。如果没有指定该参数,那么切分的数组包含从 start 到数组结束的所有元素。如果这个参数是负数,那么它规定的是从数组尾部开始算起的元素。

该方法并不会修改数组,方法会返回一个新的数组,包含从 start 到 end (不包括该元素)的 arrayObject 中的元素。

slice()和splice()的区别:slice意思是切片,即把数组切出来一段;splice意思是绞接,就像我们平时接两根绳子一样,需要把原来的绳子切下来一段,然后再和新绳子接在一起。如果想删除数组中的一段元素,并向数组添加新元素,应该使用方法 Array.splice()。

在使用slice()时,如果参数start超过了数组的起点,则会从数组头部开始;如果参数end超过了数组的结尾,则会从数组的尾部结束;如果start和end中的范围不在数组中,或者end小于start,则会返回空数组;如果start和end不为数字,则会进行转换,转换失败的话,start默认为0,end默认为0。

    var arr = ["George", "John", "Thomas", "James", "Adrew", "Martin"];

    console.log(arr); // ["George", "John", "Thomas", "James", "Adrew", "Martin"]
    console.log(arr.slice(2,4));      // ["Thomas", "James"]
    console.log(arr.slice(-3,4));     // ["James"]
    console.log(arr.slice(-10,4));    // ["George", "John", "Thomas", "James"]
    console.log(arr.slice(-10,-4));   // ["George", "John"]
    console.log(arr.slice(4,3));      // []
    console.log(arr.slice(-20,-10));  // []
    console.log(arr.slice("2","4"));  // ["Thomas", "James"]
    console.log(arr.slice("a","4"));  // ["George", "John", "Thomas", "James"]
    console.log(arr.slice("a","b"));  // []
    console.log(arr.slice("2a","4a"));// []
    console.log(arr.slice("",""));    // []  

sort()
sort() 方法用于对数组的元素进行排序(从小到大)。

arrayObject.sort(sortby)

注意:
数组在原数组上进行排序,不生成副本。参数sortby是可选的,是自定义的函数,规定排序方法。

如果调用该方法时没有使用参数,将按字母顺序对数组中的元素进行排序,说得更精确点,是按照字符编码的顺序进行排序。要实现这一点,首先应把数组的元素都转换成字符串(如有必要),以便进行比较。

如果想按照其他标准进行排序,就需要提供比较函数,该函数要比较两个值,然后返回一个用于说明这两个值的相对顺序的数字。比较函数应该具有两个参数 a 和 b,其返回值如下:

  • 若 a 小于 b,在排序后的数组中 a 应该出现在 b 之前,则返回一个小于 0 的值。
  • 若 a 等于 b,则返回 0。
  • 若 a 大于 b,则返回一个大于 0 的值。

      var arr = ["10", "5", "40", "25", "1000", "1"];
    
      function sortNumber(a,b){
            return a - b;
      }
    
      console.log(arr);  // ["10", "5", "40", "25", "1000", "1"]
      console.log(arr.sort());  // ["1", "10", "1000", "25", "40", "5"]
      console.log(arr.sort(sortNumber));  // ["1", "5", "10", "25", "40", "1000"]  
    

splice()
splice() 方法用于插入、删除或替换数组的元素。

arrayObject.splice(index, howmany, element1, ….., elementX)

注意:

参数index是必需的。规定从何处添加/删除元素,该参数是开始(包含)插入和(或)删除的数组元素的下标,必须是数字。

参数howmany是必需的。规定应该删除多少元素。必须是数字,但可以是 “0″。如果未规定此参数,则删除从 index 开始到原数组结尾的所有元素。

参数element1…elementX是可选的。规定要添加到数组的新元素,从 index 所指的下标处开始插入。

splice() 方法可删除从 index 处开始的零个或多个元素,并且用参数列表中声明的一个或多个值来替换那些被删除的元素。如果从 arrayObject 中删除了元素,则返回的是含有被删除的元素的数组。splice()会直接对原数组进行修改。

如果参数index不为数字,则会自动转换。

    var arr = ["George", "John", "Thomas", "James", "Adrew", "Martin"];

        console.log(arr);  // ["George", "John", "Thomas", "James", "Adrew", "Martin"]
        console.log(arr.splice(2,1));  //["Thomas"]
        console.log(arr);  // ["George", "John", "James", "Adrew", "Martin"]
        console.log(arr.splice(2,2,"William"));  // ["James", "Adrew"]
        console.log(arr);  // ["George", "John", "William", "Martin"]
        console.log(arr.splice(2,1,"Tom","Jerry"));  // ["William"]
        console.log(arr);  // ["George", "John", "Tom", "Jerry", "Martin"]
        console.log(arr.splice(2));  // ["Tom", "Jerry", "Martin"]
        console.log(arr);  // ["George", "John"]
        console.log(arr.splice("2"));  // []
        console.log(arr);  // ["George", "John"]
        console.log(arr.splice("a"));  // ["George", "John"]
        console.log(arr);  // []  

如果index为负数,则会从数组尾部算起;howmany为负数,则不删除。

        var arr = ["George", "John", "Thomas", "James", "Adrew", "Martin"];

        console.log(arr);  // ["George", "John", "Thomas", "James", "Adrew", "Martin"]
        console.log(arr.splice(-2,1));  // ["Adrew"]
        console.log(arr);  // ["George", "John", "Thomas", "James", "Martin"]
        console.log(arr.splice(-2,-1));  // []
        console.log(arr);  // ["George", "John", "Thomas", "James", "Martin"]  

对于大型数组,splice()的效率会比较低,参考Splicing a single value

toString()
toString() 方法可把数组转换为字符串,并返回结果。

注意:Array.toString() 相当于 Array.join() ,返回值与没有参数的 join() 方法返回的字符串相同。

unshift()
unshift() 方法可向数组的开头添加一个或更多元素,并返回新的长度。

arrayObject.unshift(newelement1, newelement2, …., newelementX)

注意:

参数newelement1……X至少要有一个。unshift() 方法将把它的参数插入 arrayObject 的头部,并将已经存在的元素顺次地移到较高的下标处,以便留出空间。该方法的第一个参数将成为数组的新元素 0,如果还有第二个参数,它将成为新的元素 1,以此类推。

unshift() 方法不创建新的数组,而是直接修改原有的数组。在IE6与IE7中,unshift()会返回 underfined!

    var arr = ["George", "John", "Thomas"];

    console.log(arr);  // ["George", "John", "Thomas"]
    console.log(arr.unshift("William"));  // 4
    console.log(arr.unshift("Tom","Jerry"));  // 6
    console.log(arr);  //["Tom", "Jerry", "William", "George", "John", "Thomas"]  

三、总结

1、改变原数组的方法: pop() 、push()、reverse()、shift()、sort()、splice()、unshift()

2、不改变原数组的方法: concat()、join()、slice()、toString()

3、JavaScript里面,没有好的机制来区别Array和Object,一般可以通过下面的方法来识别数组:

      var isArray = function(value){
            return Object.prototype.toString.apply(value) === '[object Array]';
     }

柯里化与偏应用

一、函数参数的个数

函数定义时会写明它所接收的参数个数。”一元参数”接收一个参数,”多元函数”接收多个参数。有些函数能够接收不定数量的参数,我们称之为”可变参数函数”。

二、偏应用

作为铺垫,我们首先实现一个map函数,用来将某个函数应用到数组的每个元素上:

    var _map = [].map;

    function map(list,unaryFn){
        return _map.call(list,unaryFn);
    }

    function square(n){
        return n*n;
    }

    map([1,2,3],square);
    //=> [1,4,9] 

显然,map是二元函数,square是一元函数。当我们使用[1,2,3]和square作为参数调用map时,我们是将这两个参数应用到map函数,并获得结果。
由于map函数接收两个参数,我们也提供了两个参数,所以说这是一次完整的应用。那何谓偏应用(或部分应用)呢?其实就是提供少于指定数量的参数。如仅提供一个参数来调用map函数。
如果我们只提供一个函数来调用map会怎么样?我们无法得到所要的结果,只能得到一个新的一元函数,通过调用这个函数并传递缺失的参数后,才能获得结果。
假设现在我们只提供一个参数给map,这个参数是unaryFn。我们从后往前逐步实现,首先map函数创建一个包装函数*

    var mapWrapper(list,unaryFn){
        return map(list,unaryFn);
    }

然后,我们将这个二元函数分割成两个嵌套的一元函数:

    function mapWrapper(unaryFn){
        return function(list){
            return map(list,unaryFn);
        }
    }  

这样一来,我们就能每次传递一个参数来进行调用:

    mapWrapper(square)([1,2,3]);
    //=>[1,4,9]

和之前的map函数相较,新的函数mapWrapper是一元函数,它的返回值是另一个一元函数,需要再次调用它才能获得返回值。那么偏应用要从何体现?让我们从第二个一元函数着手:

    var squareAll = mapWrapper(square);
    //=>[function]

    squareAll([1,2,3]);
    //=>[1,4,9]  

我们首先将square这个参数部分应用到了map函数,并获得一个一元函数squareAll,它能实现我们需要的功能。如果每次想要使用偏应用都需要手动编写这样一个包装函数,显然我们希望能自动化实现它。这就是下一节的内容:柯里化。

三、柯里化

首先,我们可以编写一个函数来返回包装器。我们仍以二元函数为例:

    function wrapper(unaryFn){
        return function(list){
            return map(list,unaryFn);
        }
    }  

将函数map和参数名称替换掉

    function wrapper(secondArg){
        return function(firstArg){
            return binaryFn(firstArg,secondArg);
        }
    }  

最后,我们再包装一层:

    function rightmostCurry(binaryFn){
        return function(secondArg){
            return function(firstArg){
                return binaryFn(firstArg,secondArg);
            }
        }
    }  

这样一来,我们之前使用的模式就抽象出来了。这个函数的用法是:

    var rightmostCurriedMap = rightmostCurry(map);
    var squareAll = rightmostCurriedMap(square);

    squareAll([1,4,9]);
    //=>[1,4,9]  

将一个多元函数转换成一系列一元函数的嵌套调用,这种转换称之为柯里化。

四、柯里化和偏应用的区别

1、柯里化是将一个多元函数分解为一系列嵌套调用的一元函数。分解后,你可以部分应用一个或多个参数。柯里化过程不会向函数传递参数。

2、偏应用是为一个多元函数提供部分参数,从而在调用时可以省略这些参数。

变量对象

一、概要

在构建系统过程中,我们会在程序中定义一些变量和函数。然而对于解释器来说,它是如何获取这些数据的?当引用一个对象的时候,在解释器内部又发生什么?
许多ECMAScript程序都知道,变量和执行上下文是密切相关的:

    var a = 10;//全局上下文中的变量
    (function(){
        var b = 20;//函数上下文中的局部变量
    })();

    alert(a);// 10
    alert(b);// b is not defined  

不仅如此,许多程序员也都知道,ECMAScript标准中指出独立的作用域只有通过”函数代码”才能创建。与C/C++不同的是,ECMAScript中的for循环代码块是无法创建本地上下文的:

    for(var k in {a:1,b:2}){
        alert(k);
    }

    alert(k);//尽管循环已经结束,但是变量"k"仍然在作用域中

二、数据声明

既然变量和执行上下文有关,那么它就该知道数据存储在哪里以及如何获取。这种机制就称作变量对象:
变量对象是一个特殊的对象,它与执行上下文息息相关。变量对象主要存储变量、函数声明、函数入参等
举个例子,可以用ECMAScript的对象来表示变量对象: VO = {};
VO同时也是一个执行上下文的属性:

    activeExecutionContext = {
        VO : {
            //上下文中的数据(变量声明、函数声明、函数形参)
        }
    }  

声明新的变量和函数的过程其实就是在VO中创建新的变量以及函数名对应的属性和属性值的过程。
如下所示:

    var a = 10;

    function test(x){
        var b = 20;
    }

    test(30);  

上述代码对应的变量对象:

      //全局上下文中的变量对象
      VO(globalContext) = {
          a : 10,
          test : 
      }  

      //test函数上下文中的变量对象
      VO(test functionContext) = {
          x : 30,
          b : 20
      };

但是,在实现层,变量对象只是一个抽象的概念。在实际执行上下文中,VO可能完全不叫VO,并且初始的结构也可能完全不同。

三、全局上下文的变量对象

全局对象是一个在进入任何执行上下文前就创建出来的对象,此对象以单例形式存在;它的属性在程序任何地方都可以访问,其声明周期随着程序的结束而终止。
全局对象在创建的时候,诸如Math、String、Date、parseInt等等属性也会被初始化,同时,其中一些对象会指向全局对象本身。比如,DOM中,全局对象上的window属性就是指向了全局对象。

 global = {
     Math:<...>,
     String:<...>
     ...
     ...
     window: global
 };  

在引用全局对象的属性时,前缀通常可以省略,因为全局对象是不能通过名字直接访问的。然而,通过全局对象上的this值,以及通过如DOM中的window对象这样递归引用的方式都可以访问到全局对象。

    String(10);//等同于 global.String(10);

    //带前缀
    window.a = 10;//global.window.a = 10 = global.a = 10;
    this.b = 20;//global.b = 20;

    var a = new String('test');

    alert(a);//直接访问,通过VO(globalContext)访问

    alert(window['a']);//间接访问
    alert(a === this.a);//true  

四、函数上下文中的变量对象

在函数的执行上下文中,VO是不能直接访问的。它主要扮演被称作活跃对象(activation object)的角色。
VO(functionContext) === AO;
活跃对象会在进入函数上下文的时候创建出来,初始化的时候会创建一个argument属性,其值就是Argument对象:
Argument对象是活跃对象上的属性,它包含了如下属性:

  • callee—对当前函数的引用
  • length—实参的个数
  • argument对象的properties-index的值与当前(实际传递的)形参是共享的

      function foo(x,y,z){
          //定义的函数的参数个数
          alert(foo.length);//3
    
          //实际传递参数的个数
          alert(argument.length);//2
    
          //引用函数自身
          alert(argument.callee === foo);//true
    
          //参数共享
          alert(x === argument[0]);true
          alert(x);//10
    
          argument[0] = 20;
          alert(x);// 20
    
          x = 30;
          alert(argument[0]);//30
    
          //然而对于没有传递的参数z
          //相关的argument对象的properties-index是不共享的
    
          z = 40;
          alert(argument[2]);//undefined
    
          argument[2] = 50;
          alert(z);//40
      }  
    

处理上下文代码的几个阶段:
1、进入执行上下文
一旦进入执行上下文(在执行代码之前),VO就会被一些属性填充:

  • 函数的形参—-变量对象的属性,其属性名就是形参的名字,其值就是实参的值,对于没有传递的参数,其值为undefined
  • 函数声明—-变量对象的属性,其属性名和值都是函数对象创建出来的,如果变量对象已经包含了相同名字的属性,则进行替换
  • 变量声明—-变量对象的一个属性,其属性名为变量名,其值为undefined,如果变量名和已声明的函数名或者函数参数名相同,则不会影响已存在的属性。

       function test(a,b){
           var c = 10;
           function d(){}
           var e = function _e(){};
           (function x(){});
       }
    
       test(10);//函数调用
    

    当以10为参数进入test函数上下文的时候,对应的AO如下所示:

        A0(test) = {
            a : 10,
            b : undefined,
            c : undefined,
            e : undefined,
            d : <reference to FunctionDeclaration "d">
        }  
    

    上面的AO中并不包含函数x,这是因为这里的x并不是函数声明,而是函数表达式,函数表达式不会对VO造成影响。

2、执行代码
此时,AO/VO的属性已经填充好了。(尽管大部分的属性都还没有赋予真正的值,都只是初始化时候的undefined值)
继续以上面的例子为例,到了执行代码阶段,AO/VO就会修改成如下形式:

    AO['c'] = 10;
    AO['e'] = <reference to FunctionExpress>  

以下是更加典型的例子:

    alert(x);//function

    var x = 10;
    alert(x);// 10

    x = 20;

    function x(){}

    alert(x);// 20  

正如此前提到的,变量声明是在函数声明和函数形参之后,并且变量声明不会对已存在的同名的函数声明和函数形参发生冲突,因此,在进入上下文阶段,VO填充如下形式:

    VO = {};

    VO['x'] = <reference to FunctionDeclaration "x">

    //发现var x = 10;
    //如果函数"x"还未定义
    //但是在我们的例子中变量声明并不会影响同名的函数值

    VO['x'] = <值不受影响,仍是函数引用>

关于变量

大多数讲Javascript的文章甚至Javascript的书通常都会这么说:声明全局变量的方式有两种,一种是使用var关键字,另外一种是不用var关键字,而这样的描述是错误。要记住的是:使用var关键字是声明变量的唯一方式。
如下赋值语句:a = 10;仅仅只是在全局对象上创建了新的属性(而不是变量)。
不同点如下:

    alert(a);//undefined
    alert(b);//"b" is not defined

    b = 10;
    var a = 20;  

接下来要谈到VO和不同阶段对VO的修改(进入上下文阶段和代码执行阶段):
进入上下文:

      VO = {
          a : undefined
      };

我们看到,这个阶段并没有任何”b”,因为它不是变量,b在执行阶段出现。
将上述代码稍作改动:

    alert(a);//undefined

    b = 10;
    alert(b);//10,代码执行阶段建立

    var a = 20;
    alert(a);//20,代码执行阶段赋值   

关于变量还有非常重要的一点是:与简单属性不同的是,变量是不能删除的,这意味着想要通过delete操作符来删除一个变量是不可能的。

    a = 10;
    alert(window.a);// 10
    alert(delete a);//true

    alert(window.a);//undefined

    var b = 20;
    alert(window.b);//20

    alert(delete b);//false

    alert(window.b);//仍然是20

但是,这里有一个例外,就是在eval执行上下文中,是可以删除变量的。

    eval('var a = 10');
    alert(window.a); // 10

    alert(delete a); // true

    alert(window.a); // undefined

Javascript函数

一、函数类型

ECMAScript中包含三类函数,每一类都有各自的特性
1、函数声明:

  • 拥有函数名
  • 代码位置:全局上下文或者另外一个函数体中
  • 创建时间:在进入上下文时创建
  • 会影响变量对象
  • 声明方式如下:

    function add(a,b){
       return a + b;    
     }
    

    这类函数的主要特性是:只有它们可以影响变量对象(存储在上下文VO中)。
    2、函数表达式:

  • 代码位置位于表达式的位置
  • 名字可有可无
  • 不会影响变量对象
  • 在代码执行阶段创建
    这类函数的主要特性是:代码总是处于表达式的位置。最简单的示例如下:

      var foo = function(a,b){
            return a + b;
      }   
    

    3、函数声明 VS 函数表达式
    当函数表达式有名字的时候,它很难和FD区分。不过如果仔细看这两者定义的话,要区分它们还是很容易的:
    函数表达式总是处于表达式的位置。

      //在括号(分组操作符)只可能是表达式  
      (function foo(){});  
    
      //数组初始化中,同样也只能是表达式  
      [function bar(){}];  
    
      //逗号操作符也只能是表达式  
      1,function baz(){}  
    

    定义中还提到函数表达式是在执行代码阶段创建,并且不是存储在变量对象上。

      //不论是在定义前还是定义后,函数表达式都访问不到  
      //(因为它在代码执行的阶段创建)  
    
      alert(foo);//"foo" is not defined   
    
      (function foo(){});  
    
      //后面也没有用,因为它根本不存在VO中  
    
      alert(foo);//"foo" is not defined    
    

    4、立即执行函数

        方式一:  
        (function (){})();  
        方式二:  
        (function (){}());  
    

    二、函数创建的算法

    如下所示使用伪代码表示函数创建的算法(不包含联合对象的步骤)。有助于理解ECMAScript中的函数对象,此算法对所有函数类型都是一样的。

        F = new NativeObject();  
    
        //属性[[Class]] is "Function"  
        F.[[Class]] = "Function"  
    
        //函数对象的原型  
        F.[[Prototype]] = Function.prototype  
    
        //对函数自身引用  
        //[[Call]]在函数调用F()激活  
        //同时创建一个新的执行上下文  
        F.[[Call]] = <reference to function>  
    
        //内置的构造器  
        //[[Construct]]会在使用"new"关键字的时候激活  
        //事实上,它会为新对象申请内存  
        //然后调用F.[[Call]]来初始化创建的对象,将this值设置为新创建的对象  
        F.[[Construct]] = internalContructor  
    
        //当前上下文(创建函数F的上下文)的作用域链  
        F.[[Scope]] = activeContext.Scope  
        //如果是通过new Function()方式来创建,则  
        F.[[Scope]] = globalContext.Scope  
    
        //形参个数  
        F.length = countParameters  
    
        //通过F创建出来的对象的原型  
        __objectPrototype = new Object();  
        __objectPrototype.constructor = F   
    
        F.prototype = __objectPrototype  
    
        return F  
    

    要注意的是,F.[[Prototype]]是函数(构造器)的原型,而F.prototype是通过该函数创建出来的对象的原型。在有些文章中会将F.prototype叫做”构造器的原型”,这是错误的。

javascript中类型检测

一、Javascript语言定义的5种原始类型及6种基本数据类型

1、javascript中定义的5种原始类型分别为:
null、undefined、number、string、boolean
2、javascript中定义的6种基本数据类型分别为:
null、undefined、number、string、boolean、object

二、检测原始值

如果你希望一个值是字符串、数字、布尔值或undefined,最佳选择是使用typeof运算符。typeof运算符会返回一个表示值的类型的字符串。

  • 对于字符串,typeof返回”string”
  • 对于数字,typeof返回”number”
  • 对于布尔值,typeof返回”boolean”
  • 对于undefined,typeof返回”undefined”

typeof的基本语法是:
typeof variable
你也可以这样使用typeof:
typeof(variable)
尽管这是合法的javascript语法,但这种方法让typeof看起来像一个函数而非运算符。鉴于此,我们推荐无括号的写法。
使用typeof来检测以下4种原始值类型是非常安全的做法。

//检测字符串
if(typeof name === "string"){
    anotherName = name.substring(3);
}

//检测数字
if(typeof count === "number"){
    updateCount(count);
}

//检测布尔值
if(typeof found === "boolean" && found){
    console.info("Found!");
}

//检测undefined
if(typeof MyApp === "undefined"){
    MyApp = {
        //相关代码
    };
}  

typeof运算符的独特之处在于,将其用于一个未声明的变量也不会报错。未定义的变量和值为undefined的变量通过typeof都将返回”undefined”。运行typeof null则返回”object”,这是一种低效的判断null的方法。如果需要检测null,则直接使用恒等运算(===)或非恒等运算(!==)。

三、检测引用值

引用值也称作对象(object)。在Javascript中除了原始值之外的值都是引用。有以下几种内置的引用类型:Object、Array、Date、Error等。使用typeof运算符在判断这些引用类型时则会显得力不从心,因为所有对象都会返回”object”。typeof另外一种不推荐的用法是当检测null的类型时,typeof运算符用于null时将会返回”object”。
检测某个引用值的类型的最好方法是使用instanceof运算符。
instanceof的基本语法是:
value instanceof constructor

//检测日期
if(value instanceof Date){
    console.log(value.getFullYear());
}

//检测正则表达式
if(value instanceof RegExp){
    if(value.test(anotherValue)){
        console.log("Matches");
    }
}  

//检测Error
if(value instanceof Error){
    throw value;
}  

instanceof的一个特性是它不仅检测构造这个对象的构造器,还检测原型链。原型链包含了很多信息,包括定义对象所采用的继承模式。比如,默认情况下,每个对象都继承自Object,因此每个对象的value instanceof Object都会返回true。比如:

var now = new Date();
console.log(now instanceof Object);//true
console.log(now instanceof Date);//true

因此使用instanceof来判断对象是否属于某个特定类型的做法并非最佳选择。
instanceof运算符也可以检测自定义类型,比如:

function Person(name){
    this.name = name;
} 
var me = new Person("sem");
console.log(me instanceof Object);//true
console.log(me instanceof Person);//true  

在Javascript中检测自定义类型时,最好的做法就是使用instanceof运算符,这也是唯一的办法。假设一个浏览器帧(frame A)里的一个对象被传入到另一个帧(frame B)中。两个帧都定义了构造函数Person。如果来自帧A的对象是帧A的Person的实例。则如下规则成立。

//true
frameAPersonInstance instanceof frameAPerson

//false
frameAPersonInstance instanceof frameBPerson  

因为每个帧(frame)都拥有Person的一份拷贝,它被认为是该帧(frame)中的Person的拷贝的实例。

四、检测函数

从技术上讲,Javascript中的函数是引用类型,同样存在Function构造函数,每个函数都是实例,比如:

function myFunc(){}
//不好的写法
console.log(myFunc instanceof Function);//true

然而,这个方法不能跨帧使用,每个帧都有各自的Function构造函数。好在typeof运算符可以用于函数,返回”function”。

function myFunc(){}
//好的写法
console.log(typeof myFunc === "function");//true  

检测函数最好的方法是使用typeof,因为它可以跨帧使用。用typeof来检测函数有一个限制。在IE8和更早版本的IE浏览器中,使用typeof来检测DOM节点(比如document.getElementById())中的函数都返回”object”而不是”function”。
之所以出现这种怪异的现象是因为浏览器对DOM的实现有差异。简而言之,这些早期版本的IE并没有将DOM实现为内置的Javascript方法,导致内置的typeof运算符将这些函数标识为对象。开发者往往通过in运算符来检测DOM的方法,比如:

//检测DOM方法
if("querySelectorAll" in document){
    images = document.querySelectorAll("img");
}

在其他所有的情形中,typeof运算符是检测javascript函数的最佳选择。

五、检测数组

关于如何在Javascript中检测数组类型已经有很多研究了,最终Kangax给出了一种优雅的解决方案。

function isArray(value){
    return Object.prototype.toString.call(value) === "[object Array]";
}

Kangax发现调用某个值的内置toString()方法在所有浏览器中都会返回标准的字符串结果。对于数组来说,返回的字符串为”[object Array]”,也不用考虑数组实例是在哪个帧中被构造出来。这种方法在识别内置对象时往往十分有用,但对于自定义对象请不要用这种方法。

六、检测属性

另外一种用到null(以及undefined)的场景是当检测一个属性是否在对象中存在时,比如:

//不好的写法:检测假值
if(object[propertyName]){
    //一些代码
}

//不好的写法:和null相比较
if(object[propertyName] != null){
    //一些代码
}

//不好的写法:和undefined比较
if(object[propertyName] != undefined){
    //一些代码
}

以上这段代码里的每个判断,实际上是通过给定的名字来检查属性的值,而非判断给定的名字所指的属性是否存在,因为当属性值为假值结果会出错,比如0、””、false、null、undefined。比如如果属性记录了一个数字,则这个值可以是零。这样的话,上段代码第一个判断就会导致错误。以此类推,如果属性值为null或者undefined时,三个判断都会导致错误。
判断属性是否存在最好的方法是使用in运算符。in运算符仅仅会简单地判断属性是否存在,而不会去读取属性的值,这样就避免了前文提到的有歧义的语句。如果实例对象的属性存在、或者继承自对象原型,in运算符都会返回true。比如:

var object = {
    count:0,
    related : null
};

//好的写法
if("count" in object){
    //这里的代码会执行
}

//不好的写法:检测假值
if(object["count"]){
    //这里的代码不会执行
}

//好的写法
if("related" in object){
    //这里代码会执行
}

//不好的写法:检测是否为null
if(object["related"] != null){
    //这里代码不会执行
}

如果你只想检查实例对象的某个属性是否存在,则使用hasOwnProperty()方法。所有继承自Object的Javascript对象都有这个方法,如果实例中存在这个属性则返回true(如果这个属性只存在原型里,则返回false)。需要注意的是,在IE8以及更早版本的IE中,DOM对象并非继承自Object,因此也不会包含这个方法。也就是说,你在调用DOM对象的hasOwnProperty()方法之前应当先检测其是否存在。

//对于所有非DOM对象来说,这是好的写法
if(Object.hasOwnProperty("related")){
    //执行这里的代码
}  

//如果你不确定为DOM对象,则这样来写
if("hasOwnProperty" in object && object.hasOwnProperty("related")){
    //执行这里的代码
}  

不管你什么时候需要检测属性的存在性,请使用in运算符或者hasOwnProperty(),这样做可以避免很多Bug。

Fork me on GitHub