继承是OO语言中的一个最为人津津乐道的概念。许多OO语言都支持两种继承方式:接口继承和实现继承接口继承只实现方法签名,而实现继承则继承实际的方法。由于函数没有签名,在ECMAScript中无法实现接口继承。ECMAScript只支持实现继承,而且其实现继承主要依靠原型链来实现的。

原型链

别忘记默认的原型

所有的函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype。这也正是搜游自定义类型都会继承toString()、valueOf()、hasOwnProperty()、isPrototypeOf()、propertyIsEnumerable()、toLocalString()、toString()方法的根本原因。

谨慎地定义方法

子类型有时候需要覆盖超类型中的某个方法,或者需要添加超类型中不存在的某个方法。但不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后。

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
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
};
function SubType(){
this.subproperty = false;
}
//继承了 SuperType
SubType.prototype = new SuperType();
//添加新方法
SubType.prototype.getSubValue = function(){
return this.subproperty;
};
//重写超类型中的方法
SubType.prototype.getSuperValue = function(){
return false;
};
var instance = new SubType();
alert(instance.getSuperValue()); //false

当通过 SubType 的实例调用 getSuperValue() 时,调用的就是这个重新定义的方法;但通过 SuperType 的实例调用 getSuperValue()时,还会继续调用原来的那个方法。要额外注意的是,必须在用 SuperType 的实例替换原型之后,再定义这两个方法。
在通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这样就会重写原型链。

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
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
};
function SubType(){
this.subproperty = false;
}
//继承了 SuperType
SubType.prototype = new SuperType();
//使用字面量添加新方法,会导致上一行代码无效
SubType.prototype = {
geSubValue : function (){
return this.subproperty;
},
someOtherMethod : function (){
return false;
}
};
var instance = new SubType();
alert(instance.getSuperValue()); //error!

以上代码展示了刚刚把 SuperType 的实例赋值给原型,紧接着又将原型替换成一个对象字面量而导致的问题。由于现在的原型包含的是一个Object 的实例,而非 SuperType 的实例,因此我们设想中的原型已经被切断了---SubType 和 SuperType 之间已经没有关系了。

原型链的问题

最主要的问题来自包含引用类型值的原型。在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function SuperType(){
this.colors = ["red", "blue", "green"];
}
function SubType(){
}
//继承了SuperType
SubType.prototype = new SuperType();
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green,black"

原型链的第二个问题是:没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。有鉴于此,再加上由于原型中包含引用类型值所带来的问题,实践中会单独使用原型链。

借用构造函数

在解决原型中包含引用类型值所带来问题的过程中,开发人员开始使用一种叫借用构造函数的技术(有时候也叫做伪造对象或经典继承)。这种技术的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数。别忘了,函数只不过是在特定环境中执行代码的对象,因此通过使用apply()和call()方法也可以在(将来)新创建的对象上执行构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function SuperType(){
this.colors = ["red", "blue", "green"];
}
function SubType(){
//继承SuperType
SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.color); //"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.color); //"red,blue,green"

“借调”了超类型的构造函数,实际上是在(未来将要)新创建的SubType实例的环境下调用了 SuperType 构造函数。这样一来,就会在新 SubType 对象上执行 SuperType() 函数中定义的所有对象初始化代码。结果,SubType 的每个实例就都会有自己的 colors 属性的副本了

传递参数

相对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function SuperType(name){
this.name = name;
}
function SubType(){
//继承了SuperType,同时还传递了参数
SuperType.call(this,"Nicholas");
//实例属性
this.age = 29;
}
var instance = new SubType();
alert(instance.name); //"Nicholas";
alert(instance.age); //29

为了确保 SuperType 构造函数不会重写子类型的属性,可以在调用超类型构造函数后,再添加应该在子类型中定义的属性。

借用构造函数的问题

方法都在构造函数中定义,因此函数复用就无从谈起了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型只能使用构造函数模式。考虑到这些问题,借用构造函数的技术也是很少单独使用的。

组合继承

组合继承,有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。

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 SuperType(name){
this.name = name;
this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
}
function SubType(name,age){
//继承属性
SuperType.call(this,name);
this.age = age;
}
//继承方法
SubType.prototype = new SuperType();
SubType.Prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
alert(this.age);
}
var instance1 = new SubType("Nicholas", 29);
instance1.color.push("black");
alert(instance1.color); //"red,blue,green,black"
instance1.sayName(); //"Nicholas"
instance1.sayAge(); //29
var instance2 = new SubType("Greg", 27);
alert(instance2.color); //"red,blue,green"
instance2.sayName(); //"Greg"
instance2.sayAge(); //27

组合继承避免了原型链和构造函数的缺陷,融合了它们的优点,成为JavaScript中最常用的继承模式。而且,instanceof 和 isPrototypeOf() 也能够用于识别组合继承创建的对象。

原型式继承

ECMAScript5 通过新增 Object.create() 方法规范化了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"

Object.create() 方法的第二个参数与Object.defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。

1
2
3
4
5
6
7
8
9
10
11
12
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = Object.create(person, {
name: {
value: "Greg"
}
});
alert(anotherPerson.name); //"Greg"

在没有必要兴师动众地创建构造函数,而只想让一个对象与另一个对象保持类型的情况下,原型式继承是完全可以胜任的。不过,包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样。

寄生式继承

寄生式继承是与原型式继承紧密相关的一种思路,它与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function createAnother(original) {
var clone = object(original); //通过调用函数创建一个新对象
clone.sayHi = function(){ //以某种方式来增强这个对象
alert("hi")
};
return clone; //返回这个对象
}
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anothorPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"

新对象不仅具有 person 的所有属性和方法,而且还有自己sayHi()方法
在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。前面示范继承模式时使用的 object() 函数不是必需的;任何能够返回新对象的函数都适用于此模式。

使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;这一点与构造函数模式类似。

寄生组合式继承

组合继承是JavaScript最常用的继承模式;不过,它也有自己的不足。组合继承最大的问题是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型的时候,另一次是在子类型构造函数内部。没错,子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
}
function SubType(name,age){
SuperTpye.call(this, name); //第二次调用 SuperType()
this.age = age;
}
SubType.prototype = new SuperType(); //第一次调用 SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
alert(this.age);
};

第一次调用 SuperType 构造函数时,SubType.prototype 会得到两个属性:name 和 colors; 它们都是 SuperType 的实例属性,只不过现在位于 SubType 的原型中。当调用 SubType 构造函数时,又会调用一次 SuperType 构造函数,这一次又在新对象上创建了实例属性 name 和 colors。于是,这两个属性就屏蔽了原型中的两个同名属性。
好在已经找到了解决这个问题的方法---寄生组合式继承。
所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承超类型的原型,然后再将结果指定给子类型的原型。寄生组合式继承的基本模式如下。

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
function inheritPrototype(subType, superType){
var prototype = Object(superType.prototype); //创建对象
prototype.constructor = subType; //增强对象
subTye.prototype = prototype; //指定对象
}
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
};
function SubType(name, age){
SuperType.call(this.name);
this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function(){
alert(this.age);
};

这个例子的高效率体现在它值调用了一次 SuperType 构造函数,并且因此避免了在 SubType.prototype 上面创建不必要的,多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf()。 开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

表单字段

避免多次提交表单

1
2
3
4
5
6
7
8
9
10
EventUtil.addHandler(form, "submit", function(event){
event = EventUtil.getEvent(event);
var target = EventUtil.getTarget(event);
//取得提交按钮
var btn = target.elements["submit-btn"];
//禁用它
btn.disabled = true;
});

HTML5 为表单字段新增了一个 autofocus 属性。在支持这个属性的浏览器中,只要设置这个属性,不用javascript 就能自动把焦点移动到相应字段。

1
<input type="text" autofocus>

为了保证前面的代码在设置 autofocus 的浏览器中正常运行,必须先检查是否设置了改属性,如果设置了,就不用再调用 foucus() 了。

1
2
3
4
5
6
EventUtil.addHandler(window, "load", function(event){
var element = document.forms[0].elements[0];
if (element.autofocus != true) {
element.focus();
}
});

选择文本

跨浏览器取得选择文本

1
2
3
4
5
6
7
function getSelectedText(textbox) {
if (typeof textbox.selectionStart == "number") {
return textbox.value.substring(textbox.selectionStart,textbox.selectionEnd);
} else if (document.selection) { //IE 8-=
return document.selection.createRange().text;
}
}

跨浏览器选择部分文本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function selectText(textbox, startIndex, stopIndex) {
if (textbox.setSelectionRange) {
textbox.setSelectionRange(startIndex, stopIndex);
} else if (textbox.createTextRange) { //IE 8-=
var range = textbox.createTextRange();
range.collapse(true);
range.moveStart("character", startIndex);
range.moveEnd("character", stopIndex - startIndex);
range.select();
}
textbox.focus();
}
textbox.value = "Hello world!"
//选择所有文本
selectText(textbox, 0, textbox.value.length); //"Hello world!"
//选择前3个字符
selectText(text, 0, 3); //"Hel"
//选择第 4 到第 6 个字符
selectText(textbox, 4, 7); //"o w"

选择部分文本的技术在实现高级文本输入框时很有用,例如提供自动完成建议的文本框就可以使用这种技术。

过滤输入

屏蔽字符

下列代码只允许用户输入数值

1
2
3
4
5
6
7
8
9
EventUtil.addHandler(textbox, "keypress", function(event){
event = EventUtil.getEvent(event);
var target = EventUtil.getTarget(event);
var charCode = EventUtil.getCharCode(event);
if(!/\d/.test(String.fromCharCode(charCode)) && charCode > 9 && !event.ctrlKey) { //Safari 3-
EventUtil.preventDefault(event);
}
});

操作剪贴板

1
2
3
4
5
6
7
8
9
10
11
12
getClipboardText: function(event) {
var clipboardData = (event.clipboardData || window.clipboardData);
return clipboardData.getData("text");
},
setClipboardText: function(event){
if (event.clipboardData) {
return event.clipboardData.setData("text/plain", value);
} else if (window.clipboardData) {
return window.clipboardData.setData("text", value);
}
},

在需要确保黏贴到文本框中的文本中包含某些字符,或者符合某种格式要求时,能够访问剪贴板是非常有用的。例如,如果一个文本框只接受数值,那么久必须检测黏贴过来的值。

1
2
3
4
5
6
7
8
EventUtil.addHandler(textbox, "paste", function(event){
event = EventUtil.getEvent(event);
var text = EventUtil.getClipboardText(event);
if(!/^\d*$/.test(text)) {
EventUtil.preventDefault(event);
}
});

自动切换焦点

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
(function(){
function tabForward(event) {
event = EventUtil.getEvent(event);
var target = EventUtil.getTarget(event);
if (target.value.length == target.maxLength) {
var form = target.form;
for(var i=0, len=form.elements.length; i < len; i++) {
if (form.elements[i] == target) {
if (form.elements[i+1]) {
form.elements[i+1].focus();
}
return;
}
}
}
}
})();
var textbox1 = document.getElementById("txtTel1");
var textbox2 = document.getElementById("txtTel2");
var textbox3 = document.getElementById("txtTel3");
EventUtil.addHandler(textbox1, "keyup", tabForward);
EventUtil.addHandler(textbox2, "keyup", tabForward);
EventUtil.addHandler(textbox3, "keyup", tabForward);

不过这些代码只适用于前面给出的标记,而且没有考虑隐藏字段。

作用域共有两种主要的工作模型。第一种是最为普遍的,被大多数编程语言所采用的词法作用域。另外一种叫作动态作用域,仍有一些编程语言在使用(比如
Bash脚本、Perl中的一些模式等)。

词法阶段

大部分标准语言编译器的第一个工作阶段叫作词法化(也叫单词化)。词
法化的过程会对源代码中的字符进行检查,如果是有状态的解析过程,还会赋予单词语义。
这个概念是理解词法作用域及其名称来历的基础。
简单地说,词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将
变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情
况下是这样的)。

考虑以下代码:

1
2
3
4
5
6
7
8
function foo(a) {
var b = a * 2;
function bar(c) {
console.log( a, b, c );
}
bar( b * 3 );
}
foo( 2 ); // 2, 4, 12

在这个例子中有三个逐级嵌套的作用域。为了帮助理解,可以将它们想象成几个逐级包含的气泡。

气泡1包含着整个全局作用域,其中只有一个标识符:foo。
气泡2包含着foo所创建的作用域,其中有三个标识符:a、bar和b。
气泡3包含着bar所创建的作用域,其中只有一个标识符:c。
作用域气泡由其对应的作用域块代码写在哪里决定,它们是逐级包含的。

作用域查找会在找到第一个匹配的标识符时停止。在多层的嵌套作用域中可以定义同名的标识
符,这叫作“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)。抛开遮蔽效应,作用域查找始终从
运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见第一个匹配的标识符为止。

全局变量会自动成为全局对象(比如浏览器中的window对象)的属性,因此可以不直接通
过全局对象的词法名称,而是间接地通过对全局对象属性的引用来对其进行访问。

1
window.a

通过这种技术可以访问那些被同名变量所遮蔽的全局变量。但非全局的变量如果被遮蔽了,无
论如何都无法被访问到。

无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置
决定。

词法作用域查找只会查找一级标识符,比如a、b和c。如果代码中引用了foo.bar.baz,词法作用域查
找只会试图查找foo标识符,找到这个变量后,对象属性访问规则会分别接管对bar和baz属性的访
问。

欺骗词法

如果词法作用域完全由写代码期间函数所声明的位置来定义,怎样才能在运行时来“修改”(也可以
说欺骗)词法作用域呢?
JavaScript中有两种机制来实现这个目的。社区普遍认为在代码中使用这两种机制并不是什么好
方法。但是关于它们的争论通常会忽略掉最重要的点:欺骗词法作用域会导致性能下降。

eval

JavaScript中的eval(..)函数可以接受一个字符串为参数,并将其中的内容视为好像在书写时就存
在于程序中这个位置的代码。换句话说,可以在你写的代码中用程序生成代码并运行,就好像代码
是写在那个位置的一样。

考虑以下代码:

1
2
3
4
5
6
function foo(str, a) {
eval( str ); // 欺骗!
console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1, 3

eval(..)调用中的”var b = 3;”这段代码会被当作本来就在那里一样来处理。由于那段代码声明了
一个新的变量b,因此它对已经存在的foo(..)的词法作用域进行了修改。事实上,和前面提到的原
理一样,这段代码实际上在foo(..)内部创建了一个变量b,并遮蔽了外部(全局)作用域中的同名
变量。

在严格模式的程序中,eval(..)在运行时有其自己的词法作用域,意味着其中的声明无法
修改所在的作用域。

1
2
3
4
5
6
function foo(str) {
"use strict";
eval( str );
console.log( a ); // ReferenceError: a is not defined
}
foo( "var a = 2");

JavaScript中还有其他一些功能效果和eval(..)很相似。setTimeout(..)和setInterval(..)的第一
个参数可以是字符串,字符串的内容可以被解释为一段动态生成的函数代码。这些功能已经过时
且并不被提倡。不要使用它们!
new Function(..)函数的行为也很类似,最后一个参数可以接受代码字符串,并将其转化为动态生
成的函数(前面的参数是这个新生成的函数的形参)。这种构建函数的语法比eval(..)略微安全一
些,但也要尽量避免使用。
在程序中动态生成代码的使用场景非常罕见,因为它所带来的好处无法抵消性能上的损失。

with

JavaScript中另一个难以掌握(并且现在也不推荐使用)的用来欺骗词法作用域的功能是with关键
字。可以有很多方法来解释with,在这里我选择从这个角度来解释它:它如何同被它所影响的词法
作用域进行交互。
with通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。
比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var obj = {
a: 1,
b: 2,
c: 3
};
// 单调乏味的重复"obj"
obj.a = 2;
obj.b = 3;
obj.c = 4;
// 简单的快捷方式
with (obj) {
a = 3;
b = 4;
c = 5;
}

但实际上这不仅仅是为了方便地访问对象属性。考虑如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function foo(obj) {
with (obj) {
a = 2;
}
}
var o1 = {
a: 3
};
var o2 = {
b: 3
};
foo( o1 );
console.log( o1.a ); // 2
foo( o2 );
console.log( o2.a ); // undefined
console.log( a ); // 2——不好,a被泄漏到全局作用域上了!

这个例子中创建了o1和o2两个对象。其中一个具有a属性,另外一个没有。foo(..)函数接受一个obj
参数,该参数是一个对象引用,并对这个对象引用执行了with(obj) {..}。在with块内部,我们写的
代码看起来只是对变量a进行简单的词法引用,实际上就是一个LHS引用(查看第1章),并将2赋值
给它。
当我们将o1传递进去,a = 2赋值操作找到了o1.a并将2赋值给它,这在后面的console.log(o1.a)中
可以体现。而当o2传递进去,o2并没有a属性,因此不会创建这个属性,o2.a保持undefined。
但是可以注意到一个奇怪的副作用,实际上a = 2赋值操作创建了一个全局的变量a。这是怎么回
事?
with可以将一个没有或有多个属性的对象处理为一个完全隔离的词法作用域,因此这个对象的属
性也会被处理为定义在这个作用域中的词法标识符。

尽管with块可以将一个对象处理为词法作用域,但是这个块内部正常的var声明并不会被
限制在这个块的作用域中,而是被添加到with所处的函数作用域中。

eval(..)函数如果接受了含有一个或多个声明的代码,就会修改其所处的词法作用域,而with声明
实际上是根据你传递给它的对象凭空创建了一个全新的词法作用域。
可以这样理解,当我们传递o1给with时,with所声明的作用域是o1,而这个作用域中含有一个
同o1.a属性相符的标识符。但当我们将o2作为作用域时,其中并没有a标识符,因此进行了正常的
LHS标识符查找。
o2的作用域、foo(..)的作用域和全局作用域中都没有找到标识符a,因此当a = 2执行时,自动创建
了一个全局变量(因为是非严格模式)。

另外一个不推荐使用eval(..)和with的原因是会被严格模式所影响(限制)。with被完全
禁止,而在保留核心功能的前提下,间接或非安全地使用eval(..)也被禁止了。

性能

如果代码中大量使用eval(..)或with,那么运行起来一定会变得非常慢。无论引擎多聪明,试图将
这些悲观情况的副作用限制在最小范围内,也无法避免如果没有这些优化,代码会运行得更慢这
个事实。

1
[].forEach.call($$("*"),function(a){a.style.outline="1px solid #" + (~~(Math.random()*(1<<24))).toString(16)})

[].forEach.call == Array.prototype.forEach.call
$$(“*“) == document.querySelectorAll(“*“)

parseInt(“ffffff”, 16) == 16777215

1 // 1 == 2^0
100 //4 == 2^2
10000 //16 = 2^4
1000000000000000000000000 // 16777216 == 2^24

1<<24 == 16777216

Math.random()*(1<<24) == 0 ~ 16777216

var a = 12.34, //~~a = 12
b = 1231.8754, //~~b = -1231
c = 3213.000001 //~~c = 3213

相当于

1
2
3
Array.prototype.forEach.call(document.querySelectorAll("*"),function(a){
a.style.outline="1px solid #" + parseInt(Math.random()*16777216).toString(16)
})

媒体查询语法

纵向媒体查询

1
<link rel="stylesheet" meida="screen and (orientation:portrait)" href="portrait-screen.css" />

在媒体查询的开头追加not则会颠倒查询的逻辑

1
<link rel="stylesheet" meida="not screen and (orientation:portrait)" href="portrait-screen.css" />

限制视口宽度

1
<link rel="stylesheet" meida="not screen and (orientation:portrait) and (min-width:800px)" href="portrait-screen.css" />

CSS样式表中使用媒体查询

1
2
3
@media screen and (max-device-width: 400px) {
h1 { color: green}
}

使用CSS的@import指令在当前样式表中按条件引入其他样式表

1
@import url("phone.css") screen and (max-width:360px);

使用CSS的@import方式会增加HTTP请求(这会影响加载速度),要谨慎使用。

媒体查询特性

视口宽度: width
屏幕宽度: device-width


类型

简单基本类型(string、boolean、number、null和undefined)本身并不是对象。null有时会被当
作一种对象类型,但是这其实只是语言本身的一个bug,即对null执行typeof null时会返回字符
串”object”。1实际上,null本身是基本类型。

原理是这样的,不同的对象在底层都表示为二进制,在JavaScript中二进制前三位都为0的话会被
判断为object类型,null的二进制表示是全0,自然前三位也是0,所以执行typeof时会返
回”object”

样式

DOM 样式属性和方法

取得 CSS 属性名和值

1
2
3
4
5
6
var prop, value, i, len;
for (i=0, len=myDiv.style.length; i < len; i++) {
prop = myDiv.style[i]; //或者 myDiv.style.item(i)
value = myDiv.style.getPropertyValue(prop);
alert(prop + ":" + value);
}

操作样式表

跨浏览器取得样式表对象

1
2
3
4
5
6
7
function getStyleSheet(element) {
return element.sheet || element.styleSheet;
}
//取得第一个<link/>元素引入的样式表
var link = document.getElementsByTagName("link")[0];
var sheet = getStyleSheet(link);
CSS 规则

跨浏览器向样式表中添加规则

1
2
3
4
5
6
7
8
9
function insertRule(sheet, selectorText, cssText, position) {
if (sheet.insertRule) {
sheet.insertRule(selectorText + "{" + cssText + "}", position);
} else if (sheet.addRule) { //兼容 IE
sheet.addRule(selectorText, cssText, position);
}
}
insertRule(document.styleSheets[0], "body", "background-color:silver", 0);
删除规则

跨浏览器删除规则

1
2
3
4
5
6
7
8
9
function deleteRule(sheet, index) {
if (sheet.deleteRule) {
sheet.deleteRule(index);
} else if (sheet.removeRule) {
sheet.removeRule(index); //兼容 IE
}
}
deleteRule(document.styleSheet[0], 0);

与添加规则相似,删除规则也不是实际 Web 开发中常见的做法。 考虑到删除规则可能会影响 CSS 层叠的效果,因此要慎重使用

元素大小

客户区大小

确定浏览器视口大小

1
2
3
4
5
6
7
8
9
10
11
12
13
function getViewport() {
if (document.compatMode == "BackCompat") {
return {
width: document.body.clientWidth, // IE
height: document.body.clientHeight
};
} else {
return {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
};
}
}

元素遍历

跨浏览器遍历某元素的所有子元素

1
2
3
4
5
6
7
8
9
var i,
len,
child = element.firstChild;
while(child != element.lastChild) {
if (child.nodeType == 1) {
processChild(child);
}
child = child.nextSibling;
}

而使用 Element Traversal(IE 9+) 新增的元素,代码会更简洁

1
2
3
4
5
6
7
var i,
len,
child = element.firstElementChild;
while(child != element.lastElementChild) {
processChild(child);
child = child.nextElementSbling;
}

删除类名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<div class="bd user disabled"></div>
//删除"user"类
//首先,取得类名字符串并拆分成数组
var classNames = div.className.split(/\s/);
//找到要删除的类名
var pos = -1,
i,
len;
for (i=0, len=classNames.length; i < len; i++) {
if (classNames[i] == "user") {
pos = i;
break;
}
}
//删除类名
classNames.splice(pos,1);
//把剩下的类名拼成字符串并重新设置
div.className = classNames.join(" ");

HTML5

跨浏览器兼容 innerText

1
2
3
4
5
6
7
function setInnerText(element, text) {
if (typeof element.textContent == "string") {
element.textContent = text; //兼容 Firefox
} else {
element.innerText = text;
}
}

总体架构

自调用匿名函数

为什么要为自调用匿名函数设置参数window,并传入window 对象?

通过传入window 对象,可以使window 对象变为局部变量(即把函数参数作为局部
变量使用),这样当在jQuery 代码块中访问window 对象时,不需要将作用域链回退到顶
层作用域,从而可以更快地访问window 对象,这是原因之一;另外,将window 对象作
为参数传入,可以在压缩代码时进行优化,在压缩文件jquery-1.7.1.min.js 中可以看到下
面的代码:

1
2
(function(a,b){ ... })(window);
// 参数 window 被压缩为 a,参数 undefined 被压缩为 b

注意到自调用匿名函数最后的分号(;)了吗?

通常在JavaScript 中,如果语句分别放置在不同的行中,则分号(;)是可选的,但是对
于自调用匿名函数来说,在之前或之后省略分号都可能会引起语法错误。例如,执行下面的
两个例子,就会抛出异常。
例1 在下面的代码中,如果自调用匿名函数的前一行末尾没有加分号,则自调用匿名
函数的第一对括号会被当作是函数调用。

1
2
3
var n = 1
( function(){} )()
// TypeError: number is not a function

例2 在下面的代码中,如果未在第一个自调用匿名函数的末尾加分号,则下一行自调
用匿名函数的第一对括号会被当作是函数调用。

1
2
3
( function(){} )()
( function(){} )()
// TypeError: undefined is not a function

所以,在使用自调用匿名函数时,最好不要省略自调用匿名函数之前和之后的分号。

构造jQuery对象

构造函数jQuery()

jQuery( selector [, context] )

如果传入一个字符串参数,jQuery 会检查这个字符串是选择器表达式还是HTML 代
码。如果是选择器表达式,则遍历文档,查找与之匹配的DOM 元素,并创建一个包含了
这些DOM 元素引用的jQuery 对象;如果没有元素与之匹配,则创建一个空jQuery 对象,
其中不包含任何元素,其属性length 等于0。字符串参数是HTML 代码的情况会在下一小
节介绍。
默认情况下,对匹配元素的查找将从根元素document 对象开始,即查找范围是整个文
档树,不过也可以传入第二个参数context 来限定查找范围(本书中把参数context 称为“选
择器的上下文”,或简称“上下文”)。例如,在一个事件监听函数中,可以像下面这样限制
查找范围:

1
2
3
$('div.foo').click(function() {
$('span', this).addClass('bar'); // 限定查找范围
});

在这个例子中,对选择器表达式“ span”的查找被限制在了this 的范围内,即只有被点
击元素内的span 元素才会被添加类样式“bar ”。
如果选择器表达式selector 是简单的“ #id”,且没有指定上下文context,则调用浏览器
原生方法document.getElementById() 查找属性id 等于指定值的元素;如果是比“ #id”复杂
的选择器表达式或指定了上下文,则通过jQuery 方法.find() 查找,因此$(‘span’, this) 等价
于$(this).find(‘span’)。
至于方法.find(),会调用CSS 选择器引擎Sizzle 实现

 jQuery( html [, ownerDocument] )、jQuery( html, props )

如果传入的字符串参数看起来像一段HTML 代码(例如,字符串中含有),
jQuery 则尝试用这段HTML 代码创建新的DOM 元素,并创建一个包含了这些DOM 元素引
用的jQuery 对象。例如,下面的代码将把HTML 代码转换成DOM 元素并插入body 节点的
末尾:

1
$('<p id="test">My <em>new</em> text</p>').appendTo('body');

如果HTML 代码是一个单独标签,例如,$(‘‘) 或$(‘‘),jQuery 会使
用浏览器原生方法document.createElement() 创建DOM 元素。如果是比单独标签更复杂的
HTML 片段,例如上面例子中的$(‘

Mynewtext

‘),则利用
浏览器的innerHTML 机制创建DOM 元素,这个过程由方法jQuery.buildFragment() 和方法
jQuery.clean() 实现。

第二个参数ownerDocument 用于指定创建新DOM 元素的文档对象,如果不传入,则默
认为当前文档对象。
如果HTML 代码是一个单独标签,那么第二个参数还可以是props,props 是一个包含了
属性、事件的普通对象;在调用document.createElement() 创建DOM 元素后,参数props 会被传给jQuery 方法.attr(),然后由.attr() 负责把参数props 中的属性、事件设置到新创建的
DOM 元素上。
参数props 的属性可以是任意的事件类型(如“ click”),此时属性值应该是事件监听
函数,它将被绑定到新创建的DOM 元素上;参数props 可以含有以下特殊属性:val、css、
html、text、data、width、height、offset,相应的jQuery 方法:.val()、.css()、.html()、.text()、.
data()、.width()、.height()、.offset() 将被执行,并且属性值会作为参数传入;其他类型的属性
则会被设置到新创建的DOM 元素上,某些特殊属性还会做跨浏览器兼容(如type、value、
tabindex 等);可以通过属性名class 设置类样式,但要用引号把class 包裹起来,因为class
是JavaScript 保留字。例如,在下面的例子中,创建一个div 元素,并设置类样式为“ test ”、
设置文本内容为“ Click me!”、绑定一个click 事件,然后插入body 节点的末尾,当点击该
div 元素时,还会切换类样式test:

1
2
3
4
5
6
7
$("<div/>", {
"class": "test",
text: "Click me!",
click: function(){
$(this).toggleClass("test");
}
}).appendTo("body");

jQuery( element )、jQuery( elementArray )

如果传入一个DOM 元素或DOM元素数组,则把DOM 元素封装到jQuery 对象中并返回。
这个功能常见于事件监听函数,即把关键字this 引用的DOM 元素封装为jQuery 对象,
然后在该jQuery 对象上调用jQuery 方法。例如,在下面的例子中,先调用$(this) 把被点击
的div 元素封装为jQuery 对象,然后调用方法slideUp() 以滑动动画隐藏该div 元素:

1
2
3
$('div.foo').click(function() {
$(this).slideUp();
});

### jQuery( object )
如果传入一个普通JavaScript 对象,则把该对象封装到jQuery 对象中并返回。
这个功能可以方便地在普通JavaScript 对象上实现自定义事件的绑定和触发,例如,执
行下面的代码会在对象foo 上绑定一个自定义事件custom,然后手动触发这个事件,执行绑
定的custom 事件监听函数,如下所示:

1
2
3
4
5
6
7
8
9
10
// 定义一个普通 JavaScript 对象
var foo = {foo:'bar', hello:'world'};
// 封装成 jQuery 对象
var $foo = $(foo);
// 绑定一个事件
$foo.on('custom', function (){
console.log('custom event was called');
});
// 触发这个事件
$foo.trigger('custom'); // 在控制台打印"custom event was called"

### jQuery( callback )
如果传入一个函数,则在document 上绑定一个ready 事件监听函数,当DOM 结构加载
完成时执行。ready 事件的触发要早于load 事件。ready 事件并不是浏览器原生事件,而是
DOMContentLoaded 事件、onreadystatechange 事件和函数doScrollCheck() 的统称。

### jQuery( jQuery object )
如果传入一个jQuery 对象,则创建该jQuery 对象的一个副本并返回,副本与传入的
jQuery 对象引用完全相同的DOM 元素。

### jQuery()
如果不传入任何参数,则返回一个空的jQuery 对象,属性length 为0。注意,在jQuery
1.4 之前,会返回一个含有document 对象的jQuery 对象。
这个功能可以用来复用jQuery 对象,例如,创建一个空的jQuery 对象,然后在需要时
先手动修改其中的元素,再调用jQuery 方法,从而避免重复创建jQuery 对象。

总体结构

构造jQuery 对象模块的总体源码结构如代码清单2-1 所示。

代码清单2-1 构造 jQuery 对象模块的总体源码结构



16 (function( window, undefined ) {
// 构造 jQuery 对象
22  var jQuery = (function() {
25  var jQuery = function( selector, context ) {
27  return new jQuery.fn.init( selector, context, root jQuery );
28  },
// 一堆局部变量声明
97  jQuery.fn = jQuery.prototype = {
98  constructor: jQuery,
99  init: function( selector, context, rootjQuery ) { … },
// 一堆原型属性和方法
319  };
322  jQuery.fn.init.prototype = jQuery.fn;
324  jQuery.extend = jQuery.fn.extend = function() { … };
388  jQuery.extend({
// 一堆静态属性和方法
892  });
955  return jQuery;
957 })();
// 省略其他模块的代码
9246  window.jQuery = window.$ = jQuery;
9266 })( window );

下面简要梳理下这段源码。
第16 ~ 9266 行是最外层的自调用匿名函数,第1 章中介绍过,当jQuery 初始化时,这
个自调用匿名函数包含的所有JavaScript 代码将被执行。
第22 行定义了一个变量jQuery,第22 ~ 957 行的自调用匿名函数返回jQuery 构造函
数并赋值给变量jQuery,最后在第9246 行把这个jQuery 变量暴露给全局作用域window,并
定义了别名$。
在第22 ~ 957 行的自调用匿名函数内,第25 行又定义了一个变量jQuery,它的值是
jQuery 构造函数,在第955 行返回并赋值给第22 行的变量jQuery。因此,这两个jQuery 变
量是等价的,都指向jQuery 构造函数,为了方便描述,在后面中统一称为构造函数jQuery()。
第97 ~ 319 行覆盖了构造函数jQuery() 的原型对象。第98 行覆盖了原型对象的属性
constructor,使它指向jQuery 构造函数;第99 行定义了原型方法jQuery.fn.init(),它负责
解析参数selector 和context 的类型并执行相应的查找;在第27 行可以看到,当我们调用
jQuery 构造函数时,实际返回的是jQuery.fn.init() 的实例;此外,还定义了一堆其他的原型
属性和方法,例如,selector、length、size()、toArray() 等。
第322 行用jQuery 构造函数的原型对象jQuery.fn 覆盖了jQuery.fn.init() 的原型对象。
第324 行定义了jQuery.extend() 和jQuery.fn.extend(),用于合并两个或多个对象的属性
到第一个对象;第388 ~ 892 行执行jQuery.extend() 在jQuery 构造函数上定义了一堆静态属
性和方法,例如,noConflict()、isReady、readyWait、holdReady() 等。
看上去代码清单2-1 所述的总体源码结构有些复杂,下面把疑问和难点一一罗列,逐个分析。
1)为什么要在构造函数jQuery() 内部用运算符new 创建并返回另一个构造函数的实例?
通常我们创建一个对象或实例的方式是在运算符new 后紧跟一个构造函数,例如,
newDate() 会返回一个Date 对象;但是,如果构造函数有返回值,运算符new 所创建的对象
会被丢弃,返回值将作为new 表达式的值。
jQuery 利用了这一特性,通过在构造函数jQuery() 内部用运算符new 创建并返回另一个
构造函数的实例,省去了构造函数jQuery() 前面的运算符new,即我们创建jQuery 对象时,
可以省略运算符new 直接写jQuery()。
为了拼写更方便,在第9246 行还为构造函数jQuery() 定义了别名$,因此,创建jQuery
对象的常见写法是$()。
2)为什么在第97 行执行jQuery.fn = jQuery.prototype,设置jQuery.fn 指向构造函数
jQuery() 的原型对象jQuery.prototype ?
jQuery.fn 是jQuery.prototype 的简写,可以少写7 个字符,以方便拼写。
3)既然调用构造函数jQuery() 返回的jQuery 对象实际上是构造函数jQuery.fn.init() 的
实例,为什么能在构造函数jQuery.fn.init() 的实例上调用构造函数jQuery() 的原型方法和属
性?例如,$(‘#id’).length 和$(‘#id’).size()。
在第322 行执行jQuery.fn.init.prototype = jQuery.fn 时,用构造函数jQuery() 的原型对象
覆盖了构造函数jQuery.fn.init() 的原型对象,从而使构造函数jQuery.fn.init() 的实例也可以访
问构造函数jQuery() 的原型方法和属性。
4)为什么要把第25 ~ 955 行的代码包裹在一个自调用匿名函数中,然后把第25 行定
义的构造函数jQuery() 作为返回值赋值给第22 行的jQuery 变量?去掉这个自调用匿名函
数,直接在第25 行定义构造函数jQuery() 不也可以吗?去掉了不是更容易阅读和理解吗?
去掉第25 ~ 955 行的自调用匿名函数当然可以,但会潜在地增加构造jQuery 对象模块
与其他模块的耦合度。在第25 ~ 97 行之间还定义了很多其他的局部变量,这些局部变量只
在构造jQuery 对象模块内部使用。通过把这些局部变量包裹在一个自调用匿名函数中,实现
了高内聚低耦合的设计思想。
5)为什么要覆盖构造函数jQuery() 的原型对象jQuery.prototype ?
在原型对象jQuery.prototype 上定义的属性和方法会被所有jQuery 对象继承,可以有
效减少每个jQuery 对象所需的内存。事实上,jQuery 对象只包含5 种非继承属性,其余都
继承自原型对象jQuery.prototype ;在构造函数jQuery.fn.init() 中设置了整型属性、length、
selector、context ;在原型方法.pushStack() 中设置了prevObject。因此,也不必因为jQuery
对象带有太多的属性和方法而担心会占用太多的内存。

jQuery.fn.init( selector, context, rootjQuery )

12 个分支

构造函数jQuery.fn.init() 负责解析参数selector 和context 的类型,并执行相应的逻辑,最
后返回jQuery.fn.init() 的实例。参数selector 和context 共有12 个有效分支,如表2-1 所示。

表2-1 参数selector 和context 的12 个分支

小结

默认绑定

首先要介绍的是最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用其他规
则时的默认规则。
思考一下下面的代码:

1
2
3
4
5
function foo() {
console.log( this.a );
}
var a = 2;
foo(); // 2

你应该注意到的第一件事是,声明在全局作用域中的变量(比如var a = 2)就是全局对象的一个同
名属性。它们本质上就是同一个东西,并不是通过复制得到的,就像一个硬币的两面一样。
接下来我们可以看到当调用foo()时,this.a被解析成了全局变量a。为什么?因为在本例中,函数
调用时应用了this的默认绑定,因此this指向全局对象。
那么我们怎么知道这里应用了默认绑定呢?可以通过分析调用位置来看看foo()是如何调用的。在
代码中,foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应
用其他规则。
如果使用严格模式(strict mode),那么全局对象将无法使用默认绑定,因此this会绑定
到undefined:

1
2
3
4
5
6
function foo() {
"use strict";
console.log( this.a );
}
var a = 2;
foo(); // TypeError: this is undefined

隐式绑定

另一条需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含,不
过这种说法可能会造成一些误导。
思考下面的代码:

1
2
3
4
5
6
7
8
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2

当函数引用有上下文
对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。因为调用foo()时this被绑
定到obj,因此this.a和obj.a是一样的。

对象属性引用链中只有最顶层或者说最后一层会影响调用位置。举例来说:

1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42

隐式丢失

一个最常见的this绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑
定,从而把this绑定到全局对象或者undefined上,取决于是否是严格模式。
思考下面的代码:

1
2
3
4
5
6
7
8
9
10
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函数别名!
var a = "oops, global"; // a是全局对象的属性
bar(); // "oops, global"

虽然bar是obj.foo的一个引用,但是实际上,它引用的是foo函数本身,因此此时的bar()其实是一
个不带任何修饰的函数调用,因此应用了默认绑定。

一种更微妙、更常见并且更出乎意料的情况发生在传入回调函数时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo() {
console.log( this.a );
}
function doFoo(fn) {
// fn其实引用的是foo
fn(); // <-- 调用位置!
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a是全局对象的属性
doFoo( obj.foo ); // "oops, global"
参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一个例子
一样。

如果把函数传入语言内置的函数而不是传入你自己声明的函数,会发生什么呢?结果是一样的,没
有区别:

1
2
3
4
5
6
7
8
9
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a是全局对象的属性
setTimeout( obj.foo, 100 ); // "oops, global"

JavaScript环境中内置的setTimeout()函数实现和下面的伪代码类似:

1
2
3
4
function setTimeout(fn,delay) {
// 等待delay毫秒
fn(); // <-- 调用位置!
}

就像我们看到的那样,回调函数丢失this绑定是非常常见的。除此之外,还有一种情况this的行为
会出乎我们意料:调用回调函数的函数可能会修改this。在一些流行的JavaScript库中事件处理器
常会把回调函数的this强制绑定到触发事件的DOM元素上。这在一些情况下可能很有用,但是有
时它可能会让你感到非常郁闷。遗憾的是,这些工具通常无法选择是否启用这个行为。
无论是哪种情况,this的改变都是意想不到的,实际上你无法控制回调函数的执行方式,因此就没
有办法控制会影响绑定的调用位置。

显式绑定

JavaScript中的“所有”函数都有一些有用的特性,可以用来解决这个问题。具体点说,可以使用函数的call(..)和apply(..)方法。JavaScript提供的绝大多数函数以及你自己创建的所有函数都可以使用call(..)
和apply(..)方法。

这两个方法是如何工作的呢?它们的第一个参数是一个对象,它们会把这个对象绑定到this,接着
在调用函数时指定这个this。因为你可以直接指定this的绑定对象,因此我们称之为显式绑定。
思考下面的代码:

1
2
3
4
5
6
7
function foo() {
console.log( this.a );
}
var obj = {
a:2
};
foo.call( obj ); // 2

通过foo.call(..),我们可以在调用foo时强制把它的this绑定到obj上。

如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作this的绑定对象,这个原
始值会被转换成它的对象形式(也就是new String(..)、new Boolean(..)或者new Number(..))。这通
常被称为“装箱”。

可惜,显式绑定仍然无法解决我们之前提出的丢失绑定问题。

硬绑定

但是显式绑定的一个变种可以解决这个问题。
思考下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo() {
console.log( this.a );
}
var obj = {
a:2
};
var bar = function() {
foo.call( obj );
};
bar(); // 2
setTimeout( bar, 100 ); // 2
// 硬绑定的bar不可能再修改它的this
bar.call( window ); // 2

硬绑定的典型应用场景就是创建一个包裹函数,传入所有的参数并返回接收到的所有值:

1
2
3
4
5
6
7
8
9
10
11
12
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
var obj = {
a:2
};
var bar = function() {
return foo.apply( obj, arguments );
};
var b = bar( 3 ); // 2 3
console.log( b ); // 5

另一种使用方法是创建一个bind可以重复使用的辅助函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
// 简单的辅助绑定函数
function bind(fn, obj) {
return function() {
return fn.apply( obj, arguments );
};
}
var obj = {
a:2
};
var bar = bind( foo, obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5

由于硬绑定是一种非常常用的模式,所以在ES5中提供了内置的方法Function.prototype.bind,它
的用法如下:

1
2
3
4
5
6
7
8
9
10
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
var obj = {
a:2
};
var bar = foo.bind( obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5

bind(..)会返回一个硬编码的新函数,它会把参数设置为this的上下文并调用原始函数。

API调用的”上下文”

第三方库的许多函数,以及JavaScript语言和宿主环境中许多新的内置函数,都提供了一个可选的
参数,通常被称为“上下文”(context),其作用和bind(..)一样,确保你的回调函数使用指定的this。
举例来说:

1
2
3
4
5
6
7
8
9
function foo(el) {
console.log( el, this.id );
}
var obj = {
id: "awesome"
};
// 调用foo(..)时把this绑定到obj
[1, 2, 3].forEach( foo, obj );
// 1 awesome 2 awesome 3 awesome

这些函数实际上就是通过call(..)或者apply(..)实现了显式绑定,这样你可以少些一些代码。

new绑定

在传统的面向类的语言中,“构造函数”是类中的一些特殊方法,使用new初始化类时会调用类中的
构造函数。通常的形式是这样的:

1
something = new MyClass(..);

JavaScript也有一个new操作符,使用方法看起来也和那些面向类的语言一样,绝大多数开发者都
认为JavaScript中new的机制也和那些语言一样。然而,JavaScript中new的机制实际上和面向类的
语言完全不同。

首先我们重新定义一下JavaScript中的“构造函数”。JavaScript,构造函数只是一些使用new操作符
时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一
种特殊的函数类型,它们只是被new操作符调用的普通函数而已。
举例来说,思考一下Number(..)作为构造函数时的行为,ES5.1中这样描述它:

15.7.2 Number构造函数
当Number在new表达式中被调用时,它是一个构造函数:它会初始化新创建的对象。

所以,包括内置对象函数(比如Number(..),详情请查看第3章)在内的所有函数都可以用new来调
用,这种函数调用被称为构造函数调用。这里有一个重要但是非常细微的区别:实际上并不存在所
谓的“构造函数”,只有对于函数的“构造调用”。
使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行[[原型]]连接。
  3. 这个新对象会绑定到函数调用的this。
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
    思考下面的代码:
    1
    2
    3
    4
    5
    function foo(a) {
    this.a = a;
    }
    var bar = new foo(2);
    console.log( bar.a ); // 2

使用new来调用foo(..)时,我们会构造一个新对象并把它绑定到foo(..)调用中的this上。new是最
后一种可以影响函数调用时this绑定行为的方法,我们称之为new绑定。

优先级

  1. 函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。

    1
    var bar = new foo()
  2. 函数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象。

    1
    var bar = foo.call(obj2)
  3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。

    1
    var bar = obj1.foo()
  4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。

    1
    var bar = foo()

绑定例外

####被忽略的this
如果你把null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被
忽略,实际应用的是默认绑定规则:

1
2
3
4
5
function foo() {
console.log( this.a );
}
var a = 2;
foo.call( null ); // 2

那么什么情况下你会传入null呢?
一种非常常见的做法是使用apply(..)来“展开”一个数组,并当作参数传入一个函数。类似
地,bind(..)可以对参数进行柯里化(预先设置一些参数),这种方法有时非常有用:

1
2
3
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}

// 把数组“展开”成参数

1
2
3
4
foo.apply( null, [2, 3] ); // a:2, b:3
// 使用 bind(..) 进行柯里化
var bar = foo.bind( null, 2 );
bar( 3 ); // a:2, b:3

这两种方法都需要传入一个参数当作this的绑定对象。如果函数并不关心this的话,你仍然需要
传入一个占位值,这时null可能是一个不错的选择,就像代码所示的那样。

然而,总是使用null来忽略this绑定可能产生一些副作用。如果某个函数确实使用了this(比如第
三方库中的一个函数),那默认绑定规则会把this绑定到全局对象(在浏览器中这个对象
是window),这将导致不可预计的后果(比如修改全局对象)。
显而易见,这种方式可能会导致许多难以分析和追踪的bug。

更安全的this

一种“更安全”的做法是传入一个特殊的对象,把this绑定到这个对象不会对你的程序产生任何副
作用。就像网络(以及军队)一样,我们可以创建一个“DMZ”(demilitarized zone,非军事区)对象
——它就是一个空的非委托的对象(委托在第5章和第6章介绍)。
如果我们在忽略this绑定时总是传入一个DMZ对象,那就什么都不用担心了,因为任何对于this
的使用都会被限制在这个空对象中,不会对全局对象产生任何影响。

JavaScript中创建一个空对象最简单的方法都是Object.create(null)Object.create(null)和{}很像,但是并不会创建Object.prototype这个委托,所以它
比{}”更空”:

1
2
3
4
5
6
7
8
9
10
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 我们的DMZ空对象
var ø = Object.create( null );
// 把数组展开成参数
foo.apply( ø, [2, 3] ); // a:2, b:3
// 使用bind(..)进行柯里化
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2, b:3

使用变量名ø不仅让函数变得更加“安全”,而且可以提高代码的可读性,因为ø表示“我希望this是
空”,这比null的含义更清楚。

间接引用

另一个需要注意的是,你有可能(有意或者无意地)创建一个函数的“间接引用”,在这种情况下,调
用这个函数会应用默认绑定规则。
间接引用最容易在赋值时发生:

1
2
3
4
5
6
7
8
function foo() {
`console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
(p.foo = o.foo)(); // 2

赋值表达式p.foo = o.foo的返回值是目标函数的引用,因此调用位置是foo()而不是p.foo()或
者o.foo()。根据我们之前说过的,这里会应用默认绑定。
注意:对于默认绑定来说,决定this绑定对象的并不是调用位置是否处于严格模式,而是函数体是
否处于严格模式。如果函数体处于严格模式,this会被绑定到undefined,否则this会被绑定到全局
对象。