面向对象编程

封装

对象生成的原始模式

1
2
3
4
var Cat = {
    name  : '',
    color : ''
}

然后生成实例对象,

1
2
3
4
5
6
7
8
9
var cat1 = {};
 
cat1.name = '大毛';
cat1.color = '黄色';
 
var cat2 = {};
 
cat2.name = '二毛';
cat2.color = '黑色';

很简单,但问题是,这样生成的“实例对象”其实和原型对象之间基本没有任何联系。而且生成多个实例的时候,没有简洁的方法,会非常麻烦。

原始模式的改进

1
2
3
4
5
6
function Cat(name,color){
    return {
        name  : name,
        color : color
    }
}

然后生成实例对象,但其实就等于在调用函数,返回一个对象。

1
2
var cat1 = Cat('大毛','黄色');
var cat2 = Cat('二毛','黑色');

这种问题是,如果 Cat 中不进行额外的处理,这里仍旧不能反映出来 cat1 和 cat2 内在的联系。

构造函数模式

1
2
3
4
function Cat(name,color){
    this.name = name;
    this.color = color;
}

现在就可以生成实例对象了。

1
2
var cat1 = new Cat('大毛','黄色');
var cat2 = new Cat('二毛','黑色');

这时 cat1 和 cat2 会自动含有一个 constructor 属性,指向他们的构造函数。

1
2
console.log(cat1 instanceof Cat); //true
console.log(cat2 instanceof Cat); //true

但此时这种方式仍旧存在其固有的问题。假设这里有一个方法。

1
2
3
4
5
6
function Cat(name,color){
    this.name = name;
    this.color = color;
    this.type = '猫科动物';
    this.eat = function(){console.log('吃老鼠');}
}

生成实例

1
2
3
var cat = Cat('大毛','黄色');
console.log(cat.type); // 猫科动物
cat.eat(); // 吃老鼠。

表面上没什么问题,用的时候也很好用,但是假如我们需要生成大量的 Cat 的实例,那么每个实例都会有 type 属性和 eat 方法,而且这两个都是一摸一样的内容,导致了内存的浪费,因为此时两个实例的相同属性和方法,并不是同样的内存地址。会存在多份。

那么,能不能将所有的相同的属性和方法只存在一份,然后每个实例同样的也可以使用呢?答案肯定是可以的。

prototype 模式

1
2
3
4
5
6
7
function Cat(name,color){
    this.name = name;
    this.color = color;
}
 
Cat.prototype.type = '猫科动物';
cat.prototype.eat = function(){console.log('吃老鼠');}

这时候,

1
2
var cat1 = new Cat('大毛','黄色');
var cat2 = new Cat('二毛','黑色');

此时,再多的实例对象,其 type 属性和 eat 方法都指向的是同一份,也即相同的内存地址。

1
console.log(cat1.eat === cat2.eat); //true

prototype 模式的验证方法

为了配合 prototype 模式, javascript 定义了一些辅助方法,帮助我们更方便的使用它。

1
isPrototypeOf()

这个方法用来判断,某个 prototype 对象和某个实例之间的关系。

1
console.log(Cat.prototype.isPrototypeOf(cat1)); //true

hasOwnProperty()
次方法用来判断某个属性到底是prototype 对象的属性还是类定义的(本地属性)。

1
2
console.log(cat1.hasOwnProperty('name')); true
console.log(cat1.hasOwnProperty('type')); false

in 运算符

此运算符用来判断某个属性是不是存在于某个实例中,即某个实例是否包含某个属性,不管是本地还是继承自 prototype

1
2
3
console.log("name" in cat1); // true
console.log('type' in cat1); // true
console.log('typo' in cat1); // false

in 运算符还可以用来遍历对象的属性。

1
2
3
for(var key in cat1){
    console.log('cat1[' + key ' ]=' + cat1[key]);
}

当然在遍历的时候可以使用 hasOwnProperty() 来限制输出的属性。

兼容多种模块规范的 javascript 模块化写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
;(function(name,definition){
 
    var hasDefine  = typeof define === 'function',  // 检测上下文环境是否为 AMD 或者 CMD        
        hasExports = typeof module !== 'undefined' && module.exports;  // 检查上下文是否为 node
 
    if(hasDefine){
        define(definition); // AMD 或者 CMD 环境
    }else if(hasExports){
        module.exports = definition(); // 定义为普通 node 模块
    }else{
        this[name] = definition(); // 将模块的执行结果挂载在 window 变量中,在浏览器中 this 指向 window 变量。
    }
 
})('hello',function(){ // 此处 hello 只是作为模块化的示例名称
    var hello = function(){};
    return hello;
})

不过此种写法要保证前端引用或者后端使用没有全局的 define 函数覆盖 AMD 或者 CMD 的 define 函数。

此代码来源于 深入浅出Node.js P45 2.7.4,此处为记录以及熟悉。

sort 作为数组的方法,被定义在 Array.prototype.sort,也即在 JavaScript 中所有数组都可以使用 sort 方法进行排序。虽然一般情况下,sort 甚至可以不用传递排序函数一样可以正常工作,但很多情况下,我们还是必须明确的定义排序函数,才能更好的按我们的需求来工作,这也是一般情况下建议的方式,因为不知道不同的 js 引擎在不传递排序函数的时候,会按照什么方案进行排序。
例如:

1
2
3
var colors = ['red', 'green', 'blue', 'yellow', 'white'];
console.log(colors.sort());    // ["blue", "green", "red", "white", "yellow"]
console.log(colors);           // ["blue", "green", "red", "white", "yellow"]

如果不传递排序函数,那么,对于字符串来说,其将按照字母顺序来进行排序,可是,如果这些字符串里面有大写小写字母混合呢?

1
2
var colors = ['red', 'green', 'blue', 'Yellow', 'White'];
console.log(colors.sort());    // ["White", "Yellow", "blue", "green", "red"]

怎么样,结果不一样了吧?这是因为此时按照 ASCII 编码的顺序进行排序了。

同时,另一个问题是,sort 函数作用于原数组,也即是排序的结果会覆盖排序前的数组。

然后,对于数字数组,不传递排序函数的话,会是什么结果呢?

1
2
var money = [12, 3, 7.4, 200];
console.log(money.sort());    // [12, 200, 3, 7.4]

咦?并没有得到预期的结果(除非你的预期不是按数字大小),那么为什么呢?

因为在 sort 排序的时候,如果不传递排序函数,那么会先将所有的元素转换为 字符串 ,然后才会一个字一个字(ASCII,对于 true 或者 false 这类型的值都是一样的)的排序,为什么都转换成字符串,因为 JavaScript 数组中可以是任意类型的数据,所以 JavaScript 无法推断到底转换为什么类型去排序。

那么我们看看不同的数据类型,排序函数的一些写法:

数组内都是数字,或者期望按数字为主的排序方式。

1
2
3
4
var money = [12, 3, 7.4, 200];
var compare = function(a, b) {return a - b;};
console.log(money.sort(compare)); // [3, 7.4, 12, 200]
//console.log(money.sort(function(a, b) {return a - b;})); // [3, 7.4, 12, 200]

这是最基础的排序函数,任何讲到 JavaScript 排序的地方,基本都会讲这个。首先取前两个元素赋值给 a b,返回 a-b 的值,如果返回负值则 a 在 b 前,等于 0 则无所谓,因为两个元素相等,返回正直,则 a 在 b 后。

对于字符串的元素来说,和数字的差不多,无非就是不同的排序函数,那如果是对象呢?

1
2
3
4
5
6
7
8
9
10
11
12
var people = [
    {
        name: 'Alice',
        id: 1234
    },
    {
        name: 'Bob',
        id: 567
    }
];
var compare = function(a, b) {return a.id - b.id;}
console.log(people.sort(compare)); // Bob is before Alice now

对于元素的排序函数传递的参数都一样,两个数组中的元素,然后此处主要比较两个对象元素的 id 的值,来进行排序的。

可是如果如下面这种情况呢?

1
2
3
var everything = [4, 'Red', '$200', 'white', 7.4, 12, true, 0.3, false];
var compare = function(a, b) {return a - b;};
console.log(everything.sort(compare));

当排序函数进行计算的时候,由于有减法运算的参与,所以这里都会先转成数字进行计算的,但是如果是字符串的话,此处就会转换为 NaN,因为在 ECMA 规范中没有明确定义这时候应该如何进行排序,所以各个平台,也就是各个浏览器(JavaScript环境)的不同,导致最终的结果会不一定相同。大家可以自己在不同的浏览器上测试下。

例子来源:http://zhangwenli.com/blog/2013/11/02/sorting-in-javascript/

一般情况下,我们都使用 JavaScript 的 console.log() 进行 debug 但是,对于 Object 来说,每次 console.log 之后,输出的都是不是十分有好的数据,例如:

1
2
3
4
5
6
7
var languages = [
    { name: "JavaScript", fileExtension: ".js" },
    { name: "TypeScript", fileExtension: ".ts" },
    { name: "CoffeeScript", fileExtension: ".coffee" }
];
 
console.log(languages);

输出的就如同下图所示(当然,会根据不同的浏览器以及不同的版本会有所差别):

logging_with_console_log.

然后呢,其实看起来还是那么回事,虽然麻烦点,但是还可以看的清楚,到底什么是什么东西。

再然后呢?如果我们使用这里介绍的 console.table 呢?

不过,聪明的人一眼就看出来其中的玄机了,或者,看到这里,直接就去浏览器的console里面,执行这句代码就完了。怎么样?很好用吧?

不过还是给个例子比较好,因为大部分都比较懒嘛。
还是上边的对象,然后使用 console.table(language) 输出看看。

logging_with_console_table

看到了吧,多么的一目了然!

然后点击各个表头试试看,还可以排序哦。

logging_with_console_table_sorted

好了,上面是数组,但是其实对于对象也是可以使用的哦。

1
2
3
4
5
6
var languages = {
    csharp: { name: "C#", paradigm: "object-oriented" },
    fsharp: { name: "F#", paradigm: "functional" }
};
 
console.table(languages);

然后看看结果,看到了吧?

logging_with_console_table_object

可是,如果就这么点,那么除了更好的阅读方式之外,就没什么意思。

那么,它还可以过滤,哈哈。

这么:

1
console.table(languages, ["name", "paradigm"]); //多个 key 组成的数组

或者这么:

1
console.table(languages, "name”); //单个 key

很多内容来自:http://blog.mariusschulz.com/2013/11/13/advanced-javascript-debugging-with-consoletable

前几天,在 Twitter 看到一句非常整洁的代码:

1
var bind = Function.prototype.call.bind(Function.prototype.bind);

第一次看的时候,我可以知道这段代码想干嘛,它让 x.y(z) 变成 y(x,z). 带着非常愉快的心情,我把这段代码给我的学生们看,但是,他们问我这段代码干嘛用呢。然后我就开始解释,但是我不知道从何说起,然后我就转身走开了。

应该明白的是,大部分比较好的代码,应该直接告诉我们它们是用来做什么的,带着这些在函数式 JavaScript 领域的经验,并且也在不断的阅读函数式 JavaScript 相关的书籍,在读的时候,我觉得是没有任何问题的,但是解释,将这些函数式程序解释给别人来说,还是没有太多经验。

但是,我还是想尝试下,通过我的方式,一些简单的示例,还有大量的注释,来解释一下这段代码的意思。

定义一个简单的对象 context

1
var context = {foo : “bar”};

定义一个函数,使用了一个在 this 上下文中,被命名为 foo 的变量,

1
2
3
function returnFoo(){
	return this.foo;
}

这个变量不存在于作用域中,因此,其返回 undefined。
(具体解释为,此时, this 指代全局的 window,而 window.foo 没有定义,所以返回 undefined)

1
returnFoo(); // undefined

但是,如果我们将此函数绑定到某个上下文。

1
var bound = returnFoo.bind(context);

现在变量 foo 存在于作用域中了.
(即,在 context 这个上下文中,是有定义 foo 变量的)

1
bound(); // bar

这就是 Function.prototype.bind 的作用,因为 returnFoo 是个函数,所以其继承了 function 的 prototype。

除了这种方式,还有另外其他方式来改变一个函数的上下文。例如 call 和 apply。

1
2
returnFoo.call(content); // bar
returnFoo.apply(content); // bar

还有就是将函数附加给某个对象,作为某个对象的一个方法。

1
2
content.returnFoo = returnFoo;
content.returnFoo();  // bar

让我们再来看看其他一些使用方法

Array.prototype 有一个特别好用的方法,叫做 slice。

在数组上调用此方法,它会返回一个从开始到结束的这个数组的一个拷贝,例如:

1
[1,2,3].slice(0,1);  // [1]

现在将,slice 赋值给一个本地变量。

1
var slice = Array.prototype.slice;

slice 现在未被绑定到任何对象,所以它和 Array.prototype.slice 比起来,对提供给它的上下文,或者 this,是不能正常的工作。

1
2
slice(0,1); //typeError
slice([1,2,3],0,1); //TypeError

但是,如果我们通过 call 或者 apply 来调用它,并且给它提供一个上下文。

1
2
slice.call([1,2,3],0,1);   // [1]
slice.apply([1,2,3],[0,1]);  //[1]

那么对于 bind 呢?我们来试试。

1
2
slice = Function.prototype.call.bind(Array.prototype.slice);
slice([1,2,3],0,1);  //[1]

很酷吧,那么我们对 bind 使用和 slice 一样的方式。

1
2
3
4
5
6
7
8
9
var bind = Function.prototype.call.bind(Function.prototype.bind);
 
var content = {foo : ”bar”};
function returnFoo(){
	return this.foo;
}
 
var amazing = bind(returnFoo, content);
amazing();   //bar

这样,就明白了 这段代码的意图。

这个bind 的解释是来源于 MDN 的,但是不明白为什么 Google 对 MDN 的东西这么低的评价,所以要翻页才能找到的。

翻译原文:Bind, Call and Apply in JavaScript

回调与作用域

在前面的例子中,执行回调的函数一般都如下所示:

1
    a(b);

而回调一般都如下所示:

1
    b(parameters);

这在一般情况下都可以正常的工作,但在某些时候,回调并不仅仅是单一的函数,而是某个对象的方法,并且如果该回调使用 this 来引用其所属的对象的话,就会导致错误发生。

假如有一个 add 的回调函数,它是 app 对象的一个方法:

1
2
3
4
5
	var app = {};  
	app.number = 1;  
	app.add = function (n) {  
		n = n + this.number;  
	};

makeArray 函数做了类似下面的事:

1
2
3
4
5
6
7
var makeArray = function (callback) {  
	// ...  
	if (typeof callback === "function") {  
		callback(i);  
	}  
	// ...  
};

调用

1
makeArray(app.add);

当调用的时候,它并不能按照预期的那样工作,因为 this.number 将会是 undefined。这里 this 将会指向全局对象,因为 makeArray 是一个全局函数。

如果 makeArray 是某个对象的方法,那么在回调函数中的 this 将会指向这个对象而不是期望的 app。(详情请了解下 javascript 中的 this)

解决这个问题的方法就是传递一个回调函数,此外再传递这个回调函数属于的对象作为一个参数:

1
makeArray(app.add, app);

紧跟着,我们需要去修改 makeArray 去绑定(bind)传递进来的对象:

1
2
3
4
5
6
7
var makeArray = function (callback, callback_obj) {  
	//...  
	if (typeof callback === "function") {  
		callback.call(callback_obj, i);  
	}  
	// ...  
};

对于传递一个对象和一个被用来回调的方法,另一个可选的方法就是将方法作为字符串传递,那么你就不会重复对象两次。换言之:

1
makeArray(app.add, app);

会变成:

1
makeArray("add", app);

那么 makeArray 将会变为如下面几行所示:

1
2
3
4
5
6
7
8
9
10
var makeArray = function (callback, callback_obj) {  
	if (typeof callback === "string") {  
		callback = callback_obj[callback];  
	}  
	//...  
	if (typeof callback === "function") {  
		callback.call(callback_obj, i);  
	}  
	// ...  
};

匿名的事件监听器

回调模式在日常中被经常使用,比如,当你附加一个事件监听器给页面上的某个元素时,你实际上提供了一个指向了回调函数的引用,并且在事件发生时被调用。

这里有个例子,怎么将 console.log() 作为一个回调函数监听文档的 click 事件:

1
	document.addEventListener("click", console.log, false);

绝大部分客户端浏览器都是事件驱动的(event-driven),当一个页面加载完成,会触发load事件,然后用户可以通过和页面交互触发各种各样的事件,比如:click, keypress, mouseover, mousemove等等,因为回调模式,JavaScript特别适合事件驱动编程,能让你的程序异步的工作,换言之,就是不受顺序限制。

在异步的事件驱动的JavaScript,可以提供一个回调函数用于在正确的时候被调用;有时候甚至可能提供比实际请求还要多的回调函数,因为某些事件可能不会发生,比如:如果用户不点击“购买”按钮,那么你用于验证表单格式的函数永远不会被调用。

定时

另一个使用回调模式的例子就是使用浏览器的window对象的setTimeout()和setInterval()方法,这些方法也可以接受和执行回调函数:

1
2
3
4
	var thePlotThickens = function () {  
		console.log('500ms later...');  
	};  
	setTimeout(thePlotThickens, 500);

再次注意一下,thePlotThickens 是如何被作为一个参数传递的,注意这里仍旧没有使用括号,因为你不想它立即执行,传递字符串 "thePlotThickens()" 取代函数的引用和eval()类似,是不好的模式。

当然可以提供匿名函数的方式进行调用。

1
	setTimeout(function(){thePlotThickens()}, 500);

回调

在 javascript 中,函数都是对象,这也就表明了他们也可以被作为参数,从而传递给其他函数。例如,将函数 a() 作为参数传递给 b() 时,那么,在某一时刻 b() 可能会执行或者说调用 a(); 在这种情况下,a() 就被称作毁掉函数(callback function),也可简称为回调(callback)。

代码示例:

1
2
3
4
5
6
7
8
9
10
    function b(callback) {
        // do something
        callback();
    }
 
    function a() {
        // do something
    }
 
    b(a);

这里需要注意的是,在 b(a) 中,作为参数的 a 函数,是不带括号的,括号表示要执行(此函数),而此处只需要将 a 函数传递给 b 函数,让 b 函数在适当的时候,加上括号调用它即可。

注:可以使用 火狐,或者谷歌的 javascript 控制台,如果调用 console.log(a) 的话,就会得到 a 的函数的定义代码,而前面说了括号表示要执行,所以加上括号之后,表示要执行 a 函数的代码,a 其实只是一个名字而已。

代码改写示例

首先不使用回调的方式来定义两个函数,然后使用回调的方式重构这两个函数。

使函数具有通用的,单一功能的行为,这是非常好的思想,可以对函数的功能一目了然,阅读和修改也更方便。下面两个函数都是用这种思想来定义。

makeArray 函数用来生成一个大数组,并且返回这个大数组。

1
2
3
4
5
6
7
8
9
    var makeArray = function () {
        var i     = 100000,
            array = [];
        while (i) {
            i --;
            array.push(i);
        } 
        return array;
    }

arrayPlus 函数的功能只对数组的元素进行处理。

1
2
3
4
5
6
7
    var arrayPlus = function (array) {
        var i   = 0,
            max = array.length;
        for(; i < max; i++) {
            console.log(array[i] + 1);
        }
    }

执行这两个函数

1
    arrayPlus(makeArray());  // 生成数组,并给所有的数字元素 +1 再返回此数组

函数达到了我们的目的,但是对于客户端 javascript 来说,性能在一定程度上也是至关重要的,但是上面的实现是比较低效率的,因为生成数组的 makeArray 有一遍大循环,而处理数组元素的 arrayPlus 函数,做了同样的一遍大循环。如果将 makeArray 中的 array.push(i); 改为 array.push(i+1); 即可,这样是高效的,但是这样生成数组和处理数组高度耦合,其并不再是一个通用的具有单一功能的函数,不符合我们的思想。

那么我们尝试使用回调的方式来重构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    var makeArray = function (callback) {
        var i     = 100000,
            array = [];
        while (i) {
            i --;
            if(typeof callback === 'function'){
                callback(i);
            }
            array.push(i);
        } 
        return array;
    }
 
    var arrayPlus = function (n) {
        console.log(++n);
    }

重构之后的代码,makeArray 唯一额外的任务是要检查是否有提供回调函数,如果有则执行,如果没有则不执行,并没有破坏原来 makeArray 函数的功能,makeArray 仍然可以像以前一样使用。而 arrayPlus 则变得更加简单。

调用,执行即可

1
    makeArray(arrayPlus);  // 注意和重构前的调用对比

对于回调函数来说,可以是已经定义好的函数,也可以是匿名函数,在调用主函数时创建回调函数,例如:

1
2
3
    makeArray(function(n){
       console.log(++n); 
    });

  在使用 jQuery 开发了很多网站之后,对原生的 JS 是越来与生疏了,但是对于任何一门语言来说,框架只会让事情事半功倍,但归根结底,还是语言原生的特性是根本。
  对于JS来说,基于浏览器这个宿主环境,其有很多尴尬的地方,例如兼容性不好,对于多种浏览器来实现兼容,导致代码很臃肿;原生的很多方法属性使用十分不便捷等。但对于一门语言要想深入,掌握其原生的特性还是十分必要的,毕竟相对于 jQuery 这种类库或者框架来说,要知其然,还要知其所以然。
  来自维基百科的关于AJAX 的解释是:Asynchronous JavaScript and XML(异步的JavaScript和XML技术,但其实AJAX的实现不一定和 XML 有关系,也即AJAX和数据格式无关),是一种创建交互式网页应用的网页开发技术,是多种技术的组合。目的就是让前台的数据交互变得更快捷,不用刷新页面就可以完成数据的更新。
  AJAX 的优点很明显,利于用户体验,在不刷新页面的情况下利用DOM交互可以更新页面内容,不会打断用户的操作,由于其获取和发送的数据量小的特点(获取数据格式可以使XML或者JSON或者其他,不是整个页面),减小服务器压力也是它的一个优点。
  而缺点,就是倍受人们关注的SEO问题(Google使用#!这种规定来支持AJAX的SEO,但是百度貌似还没有支持,所以做互联网的人,如果用百度的话,我劝你趁早转行,不要告诉我不稳定,不会那啥),前进后退问题(可以用其他方法解决HMTL5新增了相应的API),以及必须重视的安全问题等都是它现存的一些缺点。
  而AJAX 的应用范围,这其实这是一个太宽泛的问题,各人及各项目都有不同的用处,只要是能够提升用户体验的地方,都可以使用AJAX,但是对于什么是用户体验,又是一个写几本书都写不完的东西了。举例子来说,对于Gmail这类应用,其实可以看做SPA(单页面应用, Single Page Application)应用,所以整站都不需要刷新,而对于G+ 这种网站来说,虽然可以不整站刷新,但是整站刷新其实并没有什么不妥的地方,而不整站刷新,反而带来了页面假死的现象。据说知乎内部也有类似于G+的项目,但是我觉得这种应用,对于知乎来说,其实并不一定适合,在越来越快的网速这种情况下,其实还是整个页面刷新来的舒服,当然了,这个只是个人观点。而对于小面积更新数据来说,比如对用户名或者邮箱唯一性(输入框失去焦点后)等数据判断、选项卡(选项卡切换AJAX加载)、弹出的登录注册窗口(可以AJAX返回HTML结构)以及用户提交表单信息后的反馈信息等等,这种形式确实非常合适的。
  来了这么多开胃菜,其实很多人已经烦了,那么我们就开始来点干货,下面将通过一个新用户注册的过程来浅尝辄止的讲解下AJAX的基础知识。
  注册表单要求:用户名为邮箱,唯一性,密码必须大于六位,为了简单起见,这里不做过多合法性验证。
  AJAX出现之前,如果要注册新用户的话,用户必须填写完所有必填信息,然后单击提交按钮,将数据提交到服务器之后,才能验证数据的合法性,例如验证邮箱唯一性,提交表单之后,必须经过服务器判断邮箱的唯一性,然后刷新页面在显示此邮箱是否是唯一的,可用来注册的(其实还可以用隐藏iframe来实现)。而有了AJAX之后,当用户将邮箱输入完成,此输入框失去焦点的时候,通过AJAX查询服务器,然后给出邮箱是否是唯一的结果,虽然也经过了服务器的交互,但是页面没有刷新,并且交互的数据量非常小,发送的数据除了必要的头信息就是查询数据,而接受的数据除了头信息之外,甚至可以减少到只用数字0或者1来表示数据是否合法。
  来看HTML结构代码:

1
2
3
4
5
<form id="register" method="POST" action="register.php">
    <label>邮 箱:<input type="text" id="email" placeholder="请输入邮箱地址"></label>
    <label>密 码:<input type="password" id="password" placeholder="请输入密码"></label>
    <input type="submit" value="提交">
</form>

  除了表单页面之外,还需要有一个后端页面register.php来负责验证数据的合法性以及注册过程,这里是后端页面,就不给出具体代码了。
  接下来就交给AJAX来实现了,当邮箱输入框失去焦点时使用 GET 方式来和后台交互以验证此邮箱的唯一性。
  而当点击提交按钮时,使用 POST 方式来和后台交互,以验证是否能够注册成功。
  除了HTML和PHP之外,接下来就是AJAX发挥作用的时候了。
  提到AJAX,就不得不提XMLHttpRequest(XHR) 对象,它是W3C的一个标准,也是AJAX最核心也是最底层的对象,但是悲剧的是IE系列,直到IE7+ 才实现了W3C的标准XHR对象,但是我们还是要支持IE6的啊(万恶的IE6,这里不得不说,其实很多地方都误解了微软,在IE5中微软就实现了XHR对象,后来各个浏览器厂商才逐渐的实现此对象,只不过IE5/IE6是按微软自己的方式来实现的,使用的是ActiveX对象),所以不得不做兼容性处理。
  为了兼容IE6,那我们就写兼容性的代码吧。

1
2
3
4
5
6
7
8
9
function creatXHR(){
    if (window.XMLHttpRequest) {
        return new XMLHttpRequest();
    } else if(window.ActiveXObject) {
        return new ActiveXObject("Microsoft.XMLHTTP");
    } else {
  	alert(‘此浏览器不支持 XHR !’);
    }
}

  使用此代码来创建一个创建兼容XHR对象的函数,第二行先判断window是否有XMLHttpRequest这个属性,如果有则证明是支持W3C标准的XHR对象,否则就是IE6了(已经可以不用考虑其他浏览器了),使用微软的方式来创建XHR对象,所幸的是其他浏览器的实现和IE早期的实现在内部是兼容的,W3C也兼容(其实W3C的很多标准都是采纳于各个浏览器已经实现的事实标准)。
  我们先来想想数据验证的过程,获取数据,创建XHR对象,然后使用XHR对象的方法来与服务器交互。先不管具体的交互过程,不管是数据验证还是注册,都是这么三步,那么接下来就实现另外的函数。
  发送数据并验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function validateData(obj,method,url,data){
    var xhr      = creatXHR(),
        iscancel = setTimeout(function(){xhr.abort();console.log("超时取消!");},5000); // 设定超时取消
 
    xhr.onreadystatechange = function() {  //绑定事件
        if (xhr.readyState == 4 && xhr.status ==200){ //此时数据可用
 
            var res = (window.JSON)?(JSON.parse(xhr.responseText)):(eval("("+xhr.responseText+")"));//解析 json字符串
 
            obj.nextSibling.innerHTML = res.msg;
 
            if (res.code == 0 || res.code == 2) {
	        obj.nextSibling.style.color = 'red';
	    } else {
		obj.nextSibling.style.color = '#5B636A';
	    };
 
	    clearTimeout(iscancel);//如果响应成功,取消定时函数
 
        }
    }
 
    //检测 Method 方法,GET POST
    if (method.toLowerCase() === "get") {
        xhr.open("GET",modiUrl(url,data),true);
	xhr.send(null);
    } else if (method.toLowerCase() === "post"){
	xhr.open("POST",url,true);
	xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
	xhr.send(serialize(data));
    };		
}

  然后来详细阐述一下这个xhr对象的相关内容(获取输入数据就是很正常的获取数据的过程,这里不多说了)。
  对于vliData() 函数中的XHR而言(并不只是此函数中,所有的XHR对象都一样,后面相同,此处只是为了好说明),要使用XHR对象时,必须调用的第一个方法是open(),即:xhr.open();此方法需要接收三个参数:
  第一个参数是要发送的请求的类型(GET POST等),
  第二个参数即请求的 URL,GET请求可以将查询参数追加到URL的末尾,但是每个参数的名值对必须经过encodeURIComponent() 编码才可使用,不然容易发送错误(虽然一般情况下参数的名称都为拼音或者英文,但是还是应该养成每次编码的好习惯)。
  第三个参数即表示是否是异步发送请求,布尔值,true或者false。上例中已有使用,即:

1
xhr.open("GET","register.php?"+encodeURIComponent('email')+"="+ encodeURIComponent(email),true);

  这里使用了 get 方法,异步获取register.php 这个url的数据(url必须同域同端口同协议,否则报安全错误,可以绝对路径或者相对路径)。但是当调用open() 方法的时候并不会真正的发送请求,而是启动一个请求以备发送。而要发送请求,就必须调用send()方法才行。例如上面的代码所示,即:

1
xhr.send(null);

  这里的send()必须接受一个参数,即:要作为请求主体发送的数据。如果不需要通过请求主体发送数据,则必须传入 null ,因为不传入的话,有些浏览器会报错。
  好了,数据发送完毕了。
  数据发送完毕,等到服务器处理完毕并返回,浏览器收到响应的时候,相应的数据就会自动填充XHR对象的相应属性。XHR对象的相应属性主要包括以下几种。
  responseText : 作为响应主体被返回的文本。
  responseXML : 如果响应的内容类型是text/xml 或者 application/xml 的时候,这个属性保存这响应数据的XML DOC 文档。
  status : 响应的http状态。
  statusText : http状态的说明
  在接收到响应之后,第一步就是要检查 status 属性,以确定响应已经成功返回,一般来说,可以将http的状态码为200时作为成功的标志(其实还有304等),此时responseText 的内容已经就绪,如果内容类型正确的话,responseXML 也已经可以访问。
  但是我们这里使用的是异步请求,不像同步请求那样会将JS执行阻塞,当收到响应后才会执行后面的代码。因此要通过事件来确定XHR的状态,因为XHR对象的open() 或者 send() 方法被调用之后,并不清楚XHR 到达什么状态了,所以还要判断 XHR 的状态决定之后该如何处理。
  对于 XHR 状态的判断,可以检测 XHR 对象的 readyState 属性,该属性表示 XHR 对象请求/响应过程的当前活动阶段,可取值如下:
  0 : 未初始化,即尚未调用 open() 方法,
  1 : 启动,已经调用open() 方法,但尚未调用 send() 方法,
  2 : 发送,已经调用send() 方法,但尚未接收到响应,
  3 : 接收,已经接收到部分响应数据,
  4 : 完成,已经接收到全部响应数据,即可以在客户端使用数据了。
  同时,XHR 对象还有一个事件readystatechange ,而readyState 属性的值每变化一次,都会触发一次readystatechange 事件,因此可以通过检测此事件每次触发时readyState属性的值,一般情况下,我们只对readyState的值为 4 时的情况感兴趣,因为此时所有的数据已经就绪可用,不过为了确保浏览器的兼容性,必须在调用 open() 之前指定readystatechange事件处理程序。如上代码所示。

1
2
3
4
5
xhr.onreadystatechange = function() {
    if (xhr.readyState == 4 && xhr.status ==200){
      alert(xhr.responseText);
  }
}

  在onreadystatechange事件每次触发时,检测readyState的值,若为 4 ,且status 为200(http状态,前面已说明)时才可以使用此数据。
  另外,在接收到响应之前,还可以使用 abort() 来取消异步请求。例如当请求相应时间超长时,可以调用此方法,然后将其异步请求取消。如:

1
2
3
4
5
6
7
var iscancel = setTimeout(function(){xhr.abort();},5000);
xhr.onreadystatechange = function() {
    if (xhr.readyState == 4 && xhr.status ==200){
        alert(xhr.responseText);
  	clearTimeout(iscancel);
  }
}

  即可在超过5秒钟未收到响应,由于超时而取消本次请求。
  如果请求发送成功,并且也接收成功的话,responseXML或者responseText也就可用了,此时可以按返回不同的数据来进行处理。目前为了开发简便起见,一般返回的就是html结构或者json数据,关于数据的处理和使用,这里就不涉及了。
  对于 HTTP 来说,每个请求和响应都会包含相应的头部信息,有的有用,有的没用,同时 XHR 也提供了相应的方法来操作这两种头部的信息,因为其用处不是非常大,所以这里就不再展开了(不过有时候可以通过自定义的头部信息加上后端的配合来实现一些额外的功能),只是在使用 POST 方式请求的情况下,需要用到setRequestHeader() 方法。
  以上就是一个完整的利用原生的 JS 来写的GET 方式下的 AJAX请求,而POST 方式下的请求我们还没有接触到。那么我们下面就开始阐述一下POST方式下原生 JS 的AJAX请求的实现方法。
  前面已经说过,XHR对象调用的第一个方法是open(),open() 方法的第一个参数是请求类型,所以POST 方式的 open() 方法即为:

1
xhr.open("POST","register.php,true);

  而第二步就是将发送的数据通过 send() 方法发送出去(前面已经讲过)。但是服务器端对于提交的web表单必须经过处理,因为对于POST提交的非表单数据,其处理方式是不同的,也就是后端程序必须要读取前端发过来的原始表单数据,并从中解析出有用的部分,因此我们必须通过XHR模拟表单数据的提交,这也就用到了自定义头部信息。为了保证兼容性,使自定义头部发送成功,必须在open() 方法之后,在 send() 方法之前,设定自定义头部(如果不设定自定义头部的话后端是否可以获取提交的数据呢,答案是可以,但是就不能使用$_POST这种超级全局变量,而只能通过$HTTP_RAW_POST_DATA来获取了,此处基于PHP而言)。

1
2
3
xhr.open("POST","register.php,true);
xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
xhr.send(data);

  自定义头部的意思就是表单提交时的内容类型。
  然后就是要发送的数据了,因为最初XHR设计主要是为了处理 XML,所以这里可以传入XML DOM文档,传入的文档在经过序列化之后作为请求主体被提交到服务器,当然了,这里也可以传入任何想发送到服务器的字符串。我们这里使用提交字符串方式。
  由于 POST 数据的格式与查询时(GET)字符串格式相同,所以只需要将表单中的数据进行序列化,然后通过XHR发送到服务器即可。

1
2
3
4
5
6
7
8
xhr.onreadystatechange = function() {
    if (xhr.readyState == 4 && xhr.status ==200){
        alert(xhr.responseText);
    }
}
xhr.open("POST","register.php,true);
xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
xhr.send(data);

  然后就是对表单数据的序列化了。其实和GET数据格式相同,即email=a@b.com&site=yimity.com,但是要编码。这里就不多做讨论了。
  最终的代码为:

1
2
3
4
5
6
7
8
9
var data = "email=yimity%40gmail.com&password=123456";
xhr.onreadystatechange = function() {
    if (xhr.readyState == 4 && xhr.status ==200){
        alert(xhr.responseText);
    }
}
xhr.open("POST","register.php,true);
xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
xhr.send(data);

  
  通过一个例子,将AJAX最基本的 GET 和POST 讲清楚了,其实AJAX还可以用来上传文件,不过这都需要支持HTML5的浏览器才可实现(隐藏的iframe也可以实现,但不属于纯AJAX了),HTML5 中的 XMLHttpRequest 2 级实现了很多XMLHttpRequest 1级中的细节,并且实现了很多非常有用的API,例如文件上传, FormData 等,但是这些不属于基础的AJAX交互范畴,所以这里就不涉及了。
  
  打完收工,其实代码这东西,还是例子最重要,不过我的代码很丑。点此查看

比onload更快获取图片尺寸

lightbox类效果为了让图片居中显示而使用预加载,需要等待完全加载完毕才能显示,体验不佳(如filick相册的全屏效果)。javascript无法获取img文件头数据,真的是这样吗?本文通过一个巧妙的方法让javascript获取它。

这是大部分人使用预加载获取图片大小的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var imgLoad = function (url, callback) {
	var img = new Image();
 
	img.src = url;
	if (img.complete) {
		callback(img.width, img.height);
	} else {
		img.onload = function () {
			callback(img.width, img.height);
			img.onload = null;
		};
	};
 
};

可以看到上面必须等待图片加载完毕才能获取尺寸,其速度不敢恭维,我们需要改进。

web应用程序区别于桌面应用程序,响应速度才是最好的用户体验。如果想要速度与优雅兼得,那就必须提前获得图片尺寸,如何在图片没有加载完毕就能获取图片尺寸?

十多年的上网经验告诉我:浏览器在加载图片的时候你会看到图片会先占用一块地然后才慢慢加载完毕,并且不需要预设width与height属性,因为浏览器能够获取图片的头部数据。基于此,只需要使用javascript定时侦测图片的尺寸状态便可得知图片尺寸就绪的状态。

当然实际中会有一些兼容陷阱,如width与height检测各个浏览器的不一致,还有webkit new Image()建立的图片会受以处在加载进程中同url图片影响,经过反复测试后的最佳处理方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
// 更新:
// 05.27: 1、保证回调执行顺序:error > ready > load;2、回调函数this指向img本身
// 04-02: 1、增加图片完全加载后的回调 2、提高性能
 
/**
 * 图片头数据加载就绪事件 - 更快获取图片尺寸
 * @version	2011.05.27
 * @author	TangBin
 * @see		http://www.planeart.cn/?p=1121
 * @param	{String}	图片路径
 * @param	{Function}	尺寸就绪
 * @param	{Function}	加载完毕 (可选)
 * @param	{Function}	加载错误 (可选)
 * @example imgReady('http://www.google.com.hk/intl/zh-CN/images/logo_cn.png', function () {
		alert('size ready: width=' + this.width + '; height=' + this.height);
	});
 */
var imgReady = (function () {
	var list = [], intervalId = null,
 
	// 用来执行队列
	tick = function () {
		var i = 0;
		for (; i < list.length; i++) {
			list[i].end ? list.splice(i--, 1) : list[i]();
		};
		!list.length && stop();
	},
 
	// 停止所有定时器队列
	stop = function () {
		clearInterval(intervalId);
		intervalId = null;
	};
 
	return function (url, ready, load, error) {
		var onready, width, height, newWidth, newHeight,
			img = new Image();
 
		img.src = url;
 
		// 如果图片被缓存,则直接返回缓存数据
		if (img.complete) {
			ready.call(img);
			load && load.call(img);
			return;
		};
 
		width = img.width;
		height = img.height;
 
		// 加载错误后的事件
		img.onerror = function () {
			error && error.call(img);
			onready.end = true;
			img = img.onload = img.onerror = null;
		};
 
		// 图片尺寸就绪
		onready = function () {
			newWidth = img.width;
			newHeight = img.height;
			if (newWidth !== width || newHeight !== height ||
				// 如果图片已经在其他地方加载可使用面积检测
				newWidth * newHeight > 1024
			) {
				ready.call(img);
				onready.end = true;
			};
		};
		onready();
 
		// 完全加载完毕的事件
		img.onload = function () {
			// onload在定时器时间差范围内可能比onready快
			// 这里进行检查并保证onready优先执行
			!onready.end && onready();
 
			load && load.call(img);
 
			// IE gif动画会循环执行onload,置空onload即可
			img = img.onload = img.onerror = null;
		};
 
		// 加入队列中定期执行
		if (!onready.end) {
			list.push(onready);
			// 无论何时只允许出现一个定时器,减少浏览器性能损耗
			if (intervalId === null) intervalId = setInterval(tick, 40);
		};
	};
})();

调用例子:

1
2
3
imgReady('http://www.google.com.hk/intl/zh-CN/images/logo_cn.png', function () {
	alert('size ready: width=' + this.width + '; height=' + this.height);
});

是不是很简单?这样的方式获取摄影级别照片尺寸的速度往往是onload方式的几十多倍,而对于web普通(800×600内)浏览级别的图片能达到秒杀效果。看了这个再回忆一下你见过的web相册,是否绝大部分都可以重构一下呢?好了,请观赏令人愉悦的 DEMO :

http://www.planeart.cn/demo/imgReady/

(通过测试的浏览器:Chrome、Firefox、Safari、Opera、IE6、IE7、IE8)

转载自:planeArt

常用的 JavaScript 检测浏览器为 IE 是哪个版本的代码。包括是否是最人极端厌恶的 ie6 识别与检测。

1
2
3
4
5
6
7
8
9
10
11
12
13
var isIE=!!window.ActiveXObject;
var isIE6=isIE&&!window.XMLHttpRequest;
var isIE8=isIE&&!!document.documentMode;
var isIE7=isIE&&!isIE6&&!isIE8;
if (isIE){
    if (isIE6){
        alert("ie6");
    }else if (isIE8){
        alert("ie8");
    }else if (isIE7){
        alert("ie7");
    }
}

然后是一个稍微短的 js 判断是否 ie 的方法:
这个貌似是利用 IE 中 JScript所特有的条件编译(或曰条件注释)来区分 IE 和非 IE(这里的IE/非IE均是针对内核而言,以IE为内核的浏览器将视为IE)

1
var ie = 0/*@cc_on+1@*/;

一个最短的 js 判断 ie 或非 ie 的代码,大小仅仅 7bytes:

1
var ie = !+'\v1';

2010年1月,一位俄国人利用了IE与标准浏览器在处理数组的toString方法的差异,仅6bytes就完美完成了是否IE浏览器的检测:

1
var ie = !-[1,];

利用这些发现,我们可以使代码写的更短。如今检测是否为 IE6 其实可以写成:

1
var ie6=!-[1,]&&!window.XMLHttpRequest;

之前那大段累赘的分析 navigator,然后正则比较的方式,后面的 js 方法是不是效率更高了!

来源:
1. http://biancheng.dnbcw.info/javascript/101495.html
2. http://dean.edwards.name/weblog/2007/03/sniff/
3. http://feilong.org/shortest-ie6-judge-means