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、删除操作只针对与当前对象

软件架构

一、软件架构

1.1运行时抽象
一个软件架构是一个软件系统在其操作的某个阶段的运行时元素的抽象。一个系统有可能由很多层抽象和很多个操作阶段组成,每个抽象和操作阶段都有自己的软件架构。
软件架构的核心是抽象原则:通过封装来隐藏系统的一些细节,从而更好地识别和支持系统的属性。一个复杂的系统包含有多层的抽象,每一层抽象都有自己的架构。架构代表了在某层次上系统行为的抽象,架构的元素被描述为提供给同层的其他元素的抽象接口。
除了架构层次,软件系统通常拥有多个操作阶段,例如启动、初始化、正常处理、重新初始化和停止。每个操作阶段都有自己的架构。例如,配置文件在启动阶段会被当做架构的一个数据元素来处理,但是在正常处理阶段则不会当做一个架构元素,因为在这个阶段这些信息已经分布到系统的各处。
组件是运行时执行某种功能的软件单元。这样的例子有程序、对象、进程、过滤器。软件架构是软件系统在运行时的抽象,而软件结构则是静态源代码的属性。
一个软件架构由一些架构元素(组件、连接器和数据)的配置来定义,这些元素之间的关系受到约束,以获得想要得到的一组架构属性。处理元素(组件)是执行数据转换的元素,数据元素是包含被使用和被转换的信息元素,连接元素(连接器)是将架构的不同部分结合在一起的粘合剂。
1.2组件
一个组件是软件指令和内部状态的一个抽象单元,通过其接口提供对于数据的转换。组件是软件指令和内部状态的一个抽象单元,通过其接口提供对于数据的转换。转换的例子包括从二级存储将数据加载到内存、执行一些运算、转换为另外一种格式、使用其他数据来封装等等。
1.3连接器
一个连接器是对于组件之间的通讯、协调或者合作进行仲裁的一种抽象机制。连接器的例子包括共享的表述、远程过程调用、消息传递协议和数据流。也许理解连接器的最佳方式是将它们与组件加以对比。连接器通过将数据元素从它的一个接口转移到另一个接口而不改变数据,来支持组件之间的通讯。在其内部,一个连接器可以包含一个由组件组成的子系统,为了转移的目的对数据进行某种转换、执行转移、然后做相反的转换并交付与原始数据相同的结果。
1.4数据
一个数据是组件通过一个连接器接收或发送的信息元素。数据的例子包括字节序列、消息、编码过的参数、以及序列化过的对象,但是不包括那些永久驻留或隐藏在组件中的信息。
1.5配置
一个配置是系统在运行期间组件、连接器和数据之间的架构关系的结构。 组件-计算的所在地;连接器-定义组件之间的交互;配置-相互交互的组件和连接器的集合。

大话socket

一、网络中进程之间如何通信?

本地进程间通信(IPC)有多种方式,但主要分为以下4类:

  • 消息传递(管道、FIFO、消息队列)
  • 同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量)
  • 共享内存
  • 远程过程调用(RPC)

在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的。其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。

二、什么是Socket

我们已经知道网络中的进程是通过socket来通信的,那什么是socket呢?我们经常把socket翻译为套接字,socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口,供应用层调用已实现进程在网络中通信。
socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用”打开open –> 读写write/read –> 关闭close“模式来操作。我们可以这样理解,Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)。
image

三、Socket通信流程

Socket是”打开—读/写—关闭”模式的实现,以使用TCP协议通讯的socket为例,其交互流程如下:
image

四、TCP三次握手建立一个可靠的连接

image

第一次握手:客户端尝试连接服务器,向服务器发送syn包(同步序列编号Synchronize Sequence Numbers),syn=j,客户端进入SYN_SEND状态等待服务器确认

第二次握手:服务器接收客户端syn包并确认(ack=j+1),同时向客户端发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手

乱弹同步与异步,阻塞与非阻塞

同步VS异步

同步(synchronous)和异步(asynchronous)其实是针对消息的发送和接受的次序而言的(在通信中就是消息的发送和接收,在IO中就是数据的读和写)
同步:就是消息的发送和接收是有序的,即接收和发送第二个包一定在第一个包之后第三个包之前,而不是乱序的
异步:就是消息的发送和接收是可以乱序的,第一个包没发完可以直接发第二个包

阻塞VS非阻塞

阻塞(block)和非阻塞(non-block)其实描述的是进程或线程进行等待时的一种方式
阻塞:是等待时进程或线程需要挂起
非阻塞:则是等待时线程或进程不需要被挂起,不影响线程的执行,这时线程或进程可以继续处理其它事物,不因为这个等待而受到影响(当然它仍然在等待这个消息,只不过可能会在线程或进程执行周期的某一个地方去查看消息的通知,而不是立即在原地等待)

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、偏应用是为一个多元函数提供部分参数,从而在调用时可以省略这些参数。

Fork me on GitHub