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

变量对象

一、概要

在构建系统过程中,我们会在程序中定义一些变量和函数。然而对于解释器来说,它是如何获取这些数据的?当引用一个对象的时候,在解释器内部又发生什么?
许多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。

Javascript函数调用的四种方式

一、Javascript函数的特点

Javascript中函数有两个主要特点使其显得比较特殊。第一个特点在于函数是第一类对象,第二个特点在于它们可以提供作用域。函数就是对象,其表现如下:

  • 函数可以在运行时动态创建,还可以在程序执行过程中创建
  • 函数可以分配给变量,可以将它们的引用复制到其他变量,可以被扩展,此外除了少数特殊情况外,函数还可以被删除
  • 可以作为参数传递给其他函数,并且还可以由其他函数返回
  • 函数可以有自己的属性和方法

二、Javascript中函数定义方式

方式一:
var add = function(a,b){ //函数表达式
    return a + b;
};
方式二:
function add(a,b){ //函数声明
    return a + b;
}
方式三:
var add = new Function('a,b','return a + b');//不推荐使用  

Javascript是一种解释性的语言,函数声明会在Javascript代码加载后、执行前被解释,而函数表达式只有在执行到这一行代码时才会被解释。函数声明中不需要分号结尾,但在函数表达式中需要分号,并且应该总是使用分号。

函数表达式VS函数声明:名称和变量声明提升

alert(add(10,10));//正常调用
function add(a,b){
    return a + b;
}
以上代码可以正常运行。因为在代码开始执行之前,解析器就已经读取函数声明并将其添加到执行环境中。  
若下面例子所示,把上面的函数声明改为使用函数表达式方式,就会在执行期间导致错误:  
alert(add(10,10));  
var add = function(a,b){
    return a + b ;
};  

三、Javascript中四种调用方式

1、方法调用模式
当一个函数被保存为对象的一个属性时,我们称它为一个方法。当一个方法被调用时,this被绑定到该对象。如果调用表达式包含一个提取属性的动作(即包含一个.或[subscript]下标表达式),那么它就当做一个方法来调用。

var myObject = {
    value : 0,
    increment:function(inc){
        this.value += typeof inc === 'number' ? inc : 1;
        console.log(inc);
    }
 }  

方法可以使用this访问自己所属的对象,所以它能从对象中取值或对对象进行修改。this到对象的绑定发生在调用的时候。这个延时绑定使得函数可以对this高度复用。
2、函数调用模式
当一个函数并非一个对象的属性时,那么它就是被当做一个函数来调用:
var sum = add(3,4);
以此模式调用函数时,this被绑定到全局对象。这是语言设计上的一个错误,倘若语言设计正确,那么当内部函数被调用时,this应该仍然绑定到外部函数的this变量。这个设计错误的后果就是方法不能利用内部函数来帮助它工作,因为内部函数的this被绑定了错误的值,所以不能共享方法对对象的访问权。如果该方法定义一个变量并给它赋值为this,那么内部函数就可以通过那个变量访问到this。按照约定我们把那个变量命名为that:

myObject.double = function (){//给myObject增加一个double方法  
    var that = this;
    var helper = function (){
        that.value = add(that.value,that.value);
        console.log(this);
    };
    helper();//以函数的形式调用helper
};
myObject.double();//以方法的形式调用double  

3、构造器调用模式
当以new操作符调用构造函数时,函数内部将会发生以下情况:

  • 创建一个空对象并且this变量引用了该对象,同时还继承了该函数的原型
  • 属性和方法被加入到this引用的对象中
  • 新创建的对象由this所引用,并且最后隐式地返回this(如果没有显式地返回其他对象)
    以上情况看起来就像是在后台发生了如下事情:

    var Person = function(name){
        //使用对象字面量创建一个新对象
        //var this = {};          
        //向this添加属性和方法
        this.name = name;
        this.say = function(){
            return "I am " + this.name;
        };
        //return this;          
    }      
    

    4、Apply调用模式
    因为Javascript是一门函数式的面向对象编程语言,所以函数可以拥有方法。apply方法让我们构建一个参数数组传递给调用函数,它允许我们选择this的值。apply方法接收两个参数,第一个要绑定给this的值,第二个就是参数数组。
    对于apply和call两者作用上是相同的,但两者在参数上有区别的。对于第一个参数意义都一样,但对第二个参数:apply传入的是一个参数数组,也就是将多个参数组合成一个数组传入,而call则作为call的参数传入(从第二个参数开始)。
    如func.call(func1,var1,var2,var3)对应的apply写法为:func.apply(func1,[var1,var2,var3]);

Fork me on GitHub