被引用到的函数

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
// Shortcut function for checking if an object has a given property directly
// on itself (in other words, not on a prototype).
function has(obj, key) {
return obj != null && hasOwnProperty.call(obj, key);
}
// Is a given variable an object?
function isObject(obj) {
var type = typeof obj;
return type === 'function' || type === 'object' && !!obj;
}
// Retrieve the names of an object's own properties.
function keys(obj) {
if(!isObject(obj)) return [];
if(Object.keys) return Object.keys(obj);
var keys = [];
for (var key in obj) if(has(obj, key)) keys.push(key);
return keys;
}
// Returns the first index on an array-like that passes a predicate test
function findIndex(array, fn){
var length = array != null ? array.length : 0;
for (var i = 0; i < length; i++) {
if (fn(array[i], i, array)) return i;
}
return -1;
}
// Returns the first key on an object that passes a predicate test
function findKey(obj, fn){
var keys = keys(obj), key;
for (var i = 0, length = keys.length; i < length; i++) {
key = keys[i];
if (fn(obj[key], key, obj)) return key;
}
}

闭包

整个函数在一个闭包中,避免污染全局变量。通过传入this(其实就是window对象)来改变函数的作用域。

1
(function(){ ... }.call(this))

格式

1
2
3
4
5
var
nativeIsArray = Array.isArray,
nativeKeys = Object.keys,
nativeBind = FuncProto.bind,
nativeCreate = Object.create;

这种定义的方式省略了多余的var,格式也美观

数据判断

判断是否为dom,dom的nodeType属性值为1。这里用!!强转为boolean值

1
2
3
function isElement(obj) {
return !!(obj && obj.nodeType === 1);
};

判断是否为数组。所以为了兼容之前的版本,在原生判断函数不存在的情况下,后面重写了一个判断函数。用call函数来改变作用域可以避免当obj没有toString函数报错的情况。

1
2
3
isArray = Array.isArray || function(obj) {
return toString.call(obj) === '[object Array]';
};

判断是否为对象。先用typeof判断数据类型。函数也属于对象,但是由于typeof null也是object,所以用!!obj来区分这种情况。

1
2
3
4
isObject = function(obj) {
var type = typeof obj;
return type === 'function' || type === 'object' && !!obj;
};

判断是否为arguments,很简单,arguments有个特有属性callee。

1
2
3
isArguments = function(obj) {
return has(obj, 'callee');
};

NaN这个值有两个特点:1.它是一个数;2.不等于它自己。
‘+’放在变量前面一般作用是把后面的变量变成一个数,在这里已经判断为一个数仍加上’+’,是为了把var num = new Number()这种没有值的数字也归为NaN。

1
2
3
4
5
6
7
isNumber = function(obj) {
return Object.prototype.toString.call(obj) === '[object ' + isNumber + ']';
};
isNaN = function(obj) {
return isNumber(obj) && obj !== +obj;
};

var b = new Boolean()。b也是布尔值。

1
2
3
isBoolean = function(obj) {
return obj === true || obj === false || Object.prototype.toString.call(obj) === '[object Boolean]';
};

用void 0来表示undefined,非常有意思的小技巧。

1
2
3
isUndefined = function(obj) {
return obj === void 0;
};

Return the first value which passes a truth test.

1
2
3
4
5
6
7
8
9
function find(obj, fn) {
var key;
if (obj.length === +obj.length) {
key = findIndex(obj, fn);
} else {
key = findKey(obj, fn);
}
if (key !== void 0 && key !== -1) return obj[key];
}

noop

1
_.noop = function(){};

noop仅仅是一个空函数。因为函数不写返回值,可以是返回undefined。

1
2
3
4
5
6
7
8
var a ;
undefined = 2;
if(a === undefined){
alert("2222");
}
if(a===_.noop()){
alert("2222");
}

es3下,undefined是可以修改的,所以ie第一个alert是不执行的。这样通过函数执行可以拿到undefined的.

random

1
2
3
4
5
6
7
_.random = function(min,max){
if(max == null){
max = min;
min = 0;
}
return min + Math.floor(Math.random()*(max - min + 1));
};

uniqueId

生成dom的id

1
2
3
4
5
var idCounter = 0;
_.uniqueId = function(prefix){
var id = ++idCounter + '';//转为字符串格式
return prefix ? prefix + id :id;
};

事件处理程序

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
var EventUtil = {
//跨浏览器的事件处理程序
addHandler: function(element, type, handler) {
if (element.addEventListener) {
element.addEventListener(type, handler, false);
} else if (element.attachEvent) {
element.attachEvent("on" + type, handler);
} else {
element["on" + type] = handler;
}
},
removeHandler: function(element, type, handler) {
if (element.removeEventListener) {
element.removeEventListener(type, handler, false);
} else if (element.detachEvent) {
element.detachEvent("on" + type, handler);
} else {
element["on" + type] = null;
}
},
//跨浏览器的事件对象
getEvent: function(event) {
return event ? event : window.event;
},
getTarget: function(event) {
return event.target || event.srcElement;
},
preventDefault: function(event) {
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
},
stopPropagation: function(event) {
if (event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBubble = true;
}
},
//跨浏览器取得相关元素
getRelatedTarget: function(event) {
if (event.relatedTarget) {
return event.relatedTarget;
} else if (event.toElement) {
return event.toElement;
} else if (event.fromElement) {
return event.fromElement;
} else {
return null;
}
},
//鼠标按钮
getButton: function(event) {
if (document.implementation.hasFeature("MouseEvents", "2.0")) {
return event.button;
} else {
switch(event.button) {
case 0:
case 1:
case 3:
case 5:
case 7:
return 0;
case 2:
case 6:
return 2;
case 4:
return 1;
}
}
},
//鼠标滚轮事件
getWheelDelta: function(event) {
if (event.wheelDelta) {
return (client.engine.opera && client.engine.opera < 9.5 ? -event.wheelDelta : event.wheelDelta);
} else {
return -event.detail * 40;
}
},
//字符编码
getCharCode: function(event) {
if (typeof event.charCode == "number") {
return event.charCode; //只有在发生keypress事件才包含值
} else {
return event.keyCode;
}
},
//操作剪贴板
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);
}
},
};
//跨浏览器的事件处理程序示例:
var btn = document.getElementById("myBtn");
var handler = function(){
alert("Clicked");
};
EventUtil.addHandler(btn, "click", handler);
//这里省略其它代码
EventUtil.removeHandler(btn, "click", handler);
//跨浏览器取得相关元素示例:
var div = document.getElementById("myDiv");
EventUtil.addHandler(div, "mouseout", function(event){
event = Event.getEvent(event);
var target = EventUtil.getTarget(event);
var relatedTarget = EventUtil.getRelatedTarget(event);
alert("Moused out fo " + target.tagName + " to " + relatedTarget.tagName);
});

鼠标与滚轮事件

客户区坐标位置

可以使用类似下列代码取得鼠标事件的客户端坐标信息:

1
2
3
4
5
var div = document.getElementById("myDiv");
EventUtil.addHandler(div, "click", function(event){
event = EventUtil.getEvent(event);
alert("Client coordinates: " + event.clientX + "," + event.clientY);
});

修改键

当某个鼠标事件发生时,通过检测这几个属性就可以确定用户是否同时按下了其中的键。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var div = document.getElementById("myDiv");
EventUtil.addHandler(div, "click", function(event){
event = EventUtil.getEvent(event);
var keys = new Array();
if (event.shiftKey) {
keys.push("shift");
}
if (event.ctrlKey) {
keys.push("ctrlKey");
}
if (event.altKey) {
keys.push("altKey");
}
if (event.metaKey) {
keys.push("metaKey");
}
alert("Keys: " + keys.join(","));
});

键盘与文本事件

textInput 事件

这个用于替代 keypress 的textInput 事件的行为稍有不同。区别之一就是任何可以获得焦点的元素都可以触发 keypress 事件,但只有可编辑区域才能触发 textInput 事件。区别之二是 textInput 事件只会在用户按下能够输入实际字符的键时才会触发,而 keypress 事件则在按下那些能够影响文本显示的键时也会触发(例如退格键)。

由于 textInput 主要考虑的是字符,因此它的 event 对象中还包含一个 data 属性,这个属性的值就是用户输入的字符(而非字符编码)。话句话说,用户在没有按上档键的情况下按下了S键,data 的值就是”s”,而如果在按住上档键时按下该键,data 的值就是”s”。

1
2
3
4
5
var textbox = document.getElementById("myText");
EventUtil.addHandler(textbox, "textInput", function(event) {
event = EventUtil.getEvent(event);
alert(event.data);
})

HTML5 事件

contextmenu 事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
EventUtil.addHandler(window, "load", function(event) {
var div = document.getElementById('myDiv');
EventUtil.addHandler(div, "contextmenu", function(event){
event = EventUtil.getEvent(event);
EventUtil.preventDefault(event);
var menu = document.getElementById('myMenu');
menu.style.left = event.clientX + 'px';
menu.style.top = event.clientY = 'px';
menu,style.visibility = "visible";
});
EventUtil.addHandler(document, 'click', function(){
document.getElementById('myMenu').style.visibility = 'hidden';
});
})

beforeunload(页面卸载前) 事件

1
2
3
4
5
6
EventUtil.addHandler(window, "beforeunload", function(event){
event = EventUtil.getEvent(event);
var message = "I'm really going to miss you if you go.";
event.returnValue = message;
return message;
});

DOMContentLoaded(IE 9+) 事件

DOMContentLoaded 事件在形成完整的 DOM 树之后就会触发,不理会图像、JavaScript 文件、CSS 文件或其他资源是否已经下载完毕。

1
2
3
EventUtil.addHandler(document, "DOMContentLoaded", function(event){
alert('Content loaded');
});

readystatechange 事件

readyState 属性: uninitialized|loading|loaded|interactive|complete

内存和性能

事件委托

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
//一下面的 HTML 代码为例
<ul id="myLinks">
<li id="goSomewhere">Go somewhere</li>
<li id="doSomething">Do something</li>
<li id="sayHi">Say hi</li>
</ul>
var list = document.getElementById('myLinks');
EventUtil.addHandler(list, "click", function(event){
event = EventUtil.getEvent(event);
var target = EventUtil.getTarget(event);
switch(target.id) {
case "doSomething":
document.title = "I changed the document's title";
break;
case "goSomewhere":
location.href = 'http://www.google.com';
break;
case "sayHi":
alert("hi");
break;
}
});

最适合采用事件委托技术的事件包括 click、mousedown、mouseup、keydown、keyup、和keypress。虽然 moseover 和 mouseout 事件也冒泡,但要适当处理它们并不容易,而且经常需要计算元素的位置。(因为当鼠标从一个元素移到其子节点时,或者当鼠标移出该元素时,都会触发 mouseout事件。)

理解作用域

RHS LHS

它们分别代表左侧和右侧。
什么东西的左侧和右侧?是一个赋值操作的左侧和右侧。
换句话说,当变量出现在赋值操作的左侧时进行LHS查询,出现在右侧时进行RHS查询。
讲得更准确一点,RHS查询与简单地查找某个变量的值别无二致,而LHS查询则是试图找到变量
的容器本身,从而可以对其赋值。从这个角度说,RHS并不是真正意义上的“赋值操作的右侧”,更准
确地说是“非左侧”。
你可以将RHS理解成retrieve his source value(取到它的源值),这意味着“得到某某的值”。

考虑以下代码:

1
console.log( a );

其中对a的引用是一个RHS引用,因为这里a并没有赋予任何值。相应地,需要查找并取得a的值,这
样才能将值传递给console.log(..)。
相比之下,例如:

1
a = 2;

这里对a的引用则是LHS引用,因为实际上我们并不关心当前的值是什么,只是想要为= 2这个赋
值操作找到一个目标。

LHS和RHS的含义是“赋值操作的左侧或右侧”并不一定意味着就是“=赋值操作符的左侧
或右侧”。赋值操作还有其他几种形式,因此在概念上最好将其理解为“赋值操作的目标是谁
(LHS)”以及“谁是赋值操作的源头(RHS)”。

考虑下面的程序,其中既有LHS也有RHS引用:

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

最后一行foo(..)函数的调用需要对foo进行RHS引用,意味着“去找到foo的值,并把它给我”。并
且(..)意味着foo的值需要被执行,因此它最好真的是一个函数类型的值!
这里还有一个容易被忽略却非常重要的细节。
代码中隐式的a = 2操作可能很容易被你忽略掉。这个操作发生在2被当作参数传递给foo(..)函数
时,2会被分配给参数a。为了给参数a(隐式地)分配值,需要进行一次LHS查询。
这里还有对a进行的RHS引用,并且将得到的值传给了console.log(..)。console.log(..)本身也需
要一个引用才能执行,因此会对console对象进行RHS查询,并且检查得到的值中是否有一个叫
作log的方法。
最后,在概念上可以理解为在LHS和RHS之间通过对值2进行交互来将其传递进log(..)(通过变
量a的RHS查询)。假设在log(..)函数的原生实现中它可以接受参数,在将2赋值给其中第一个(也
许叫作arg1)参数之前,这个参数需要进行LHS引用查询。

你可能会倾向于将函数声明function foo(a) {…概念化为普通的变量声明和赋值,比
如var foo、foo = function(a) {…。如果这样理解的话,这个函数声明将需要进行LHS查询。
然而还有一个重要的细微差别,编译器可以在代码生成的同时处理声明和值的定义,比如在引
擎执行代码时,并不会有线程专门用来将一个函数值“分配给”foo。因此,将函数声明理解成前
面讨论的LHS查询和赋值的形式并不合适。

引擎和作用域的对话

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

让我们把上面这段代码的处理过程想象成一段对话,这段对话可能是下面这样的。

引擎:我说作用域,我需要为foo进行RHS引用。你见过它吗?
作用域:别说,我还真见过,编译器那小子刚刚声明了它。它是一个函数,给你。
引擎:哥们太够意思了!好吧,我来执行一下foo。
引擎:作用域,还有个事儿。我需要为a进行LHS引用,这个你见过吗?
作用域:这个也见过,编译器最近把它声名为foo的一个形式参数了,拿去吧。
引擎:大恩不言谢,你总是这么棒。现在我要把2赋值给a。
引擎:哥们,不好意思又来打扰你。我要为console进行RHS引用,你见过它吗?
作用域:咱俩谁跟谁啊,再说我就是干这个的。这个我也有,console是个内置对象。给你。
引擎:么么哒。我得看看这里面是不是有log(..)。太好了,找到了,是一个函数。
引擎:哥们,能帮我再找一下对a的RHS引用吗?虽然我记得它,但想再确认一次。
作用域:放心吧,这个变量没有变动过,拿走,不谢。
引擎:真棒。我来把a的值,也就是2,传递进log(..)。
……

作用域嵌套

作用域是根据名称查找变量的一套规则。实际情况中,通常需要同时顾及几个作用域。
当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用域中无法
找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作
用域(也就是全局作用域)为止。

考虑以下代码:

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

对b进行的RHS引用无法在函数foo内部完成,但可以在上一级作用域(在这个例子中就是全局作
用域)中完成。
因此,回顾一下引擎和作用域之间的对话,会进一步听到:

引擎:foo的作用域兄弟,你见过b吗?我需要对它进行RHS引用。
作用域:听都没听过,走开。
引擎:foo的上级作用域兄弟,咦?有眼不识泰山,原来你是全局作用域大哥,太好了。你见过b
吗?我需要对它进行RHS引用。
作用域:当然了,给你吧。

遍历嵌套作用域链的规则很简单:引擎从当前的执行作用域开始查找变量,如果找不到,就向上一
级继续查找。当抵达最外层的全局作用域时,无论找到还是没找到,查找过程都会停止。

异常

为什么区分LHS和RHS是一件重要的事情?
因为在变量还没有声明(在任何作用域中都无法找到该变量)的情况下,这两种查询的行为是不一
样的。

考虑如下代码:

1
2
3
4
5
function foo(a) {
console.log( a + b );
b = a;
}
foo( 2 );

第一次对b进行RHS查询时是无法找到该变量的。也就是说,这是一个“未声明”的变量,因为在任何
相关的作用域中都无法找到它。
如果RHS查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出ReferenceError异常。值
得注意的是,ReferenceError是非常重要的异常类型。
相较之下,当引擎执行LHS查询时,如果在顶层(全局作用域)中也无法找到目标变量,全局作用域
中就会创建一个具有该名称的变量,并将其返还给引擎,前提是程序运行在非“严格模式”下。
“不,这个变量之前并不存在,但是我很热心地帮你创建了一个。”
ES5中引入了“严格模式”。同正常模式,或者说宽松/懒惰模式相比,严格模式在行为上有很多不
同。其中一个不同的行为是严格模式禁止自动或隐式地创建全局变量。因此,在严格模式中LHS查
询失败时,并不会创建并返回一个全局变量,引擎会抛出同RHS查询失败时类似的ReferenceError
异常。
接下来,如果RHS查询找到了一个变量,但是你尝试对这个变量的值进行不合理的操作,比如试图
对一个非函数类型的值进行函数调用,或着引用null或undefined类型的值中的属性,那么引擎会
抛出另外一种类型的异常,叫作TypeError。
ReferenceError同作用域判别失败相关,而TypeError则代表作用域判别成功了,但是对结果的操作
是非法或不合理的。

小结

作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对变量进行
赋值,那么就会使用LHS查询;如果目的是获取变量的值,就会使用RHS查询。
赋值操作符会导致LHS查询。=操作符或调用函数时传入参数的操作都会导致关联作用域的赋值
操作。
JavaScript引擎首先会在代码执行前对其进行编译,在这个过程中,像var a = 2这样的声明会被分
解成两个独立的步骤:

  1. 首先,var a在其作用域中声明新变量。这会在最开始的阶段,也就是代码执行前进行。
  2. 接下来,a = 2会查询(LHS查询)变量a并对其进行赋值。
    LHS和RHS查询都会在当前执行作用域中开始,如果有需要(也就是说它们没有找到所需的标识
    符),就会向上级作用域继续查找目标标识符,这样每次上升一级作用域(一层楼),最后抵达全局
    作用域(顶层),无论找到或没找到都将停止。
    不成功的RHS引用会导致抛出ReferenceError异常。不成功的LHS引用会导致自动隐式地创建一个
    全局变量(非严格模式下),该变量使用LHS引用的目标作为标识符,或者抛出ReferenceError异常
    (严格模式下)。

window 对象

窗口位置

款浏览器取得窗口左边和上边的位置。

1
2
var leftPos = (typeof window.screenLeft == "number") ? window.screenLeft : window.screenY;
var topPos = (typeof window.screenTop == "number") ? window.screenTop : window.screenX;

移动到新位置

1
2
3
4
5
//将窗口移动到屏幕左上角
window.moveTo(0,0);
//将窗口向下移动到100像素
window.moveBy(0,100);

窗口大小

取得页面视口的大小

1
2
3
4
5
6
7
8
9
10
11
12
var pageWidth = window.innerWidth,
pageHeight = window.innerHeight;
if (typeof pageWidth != "number") {
if (document.compatMode == "CSS1Compat") {
pageWidth = document.documentElement.clientWidth;
pageHeight = document.documentElement.pageHeight;
} else {
pageWidth = document.body.clientWidth;
pageHeight = document.body.pageHeight;
}
}

调整浏览器窗口的大小

1
2
3
4
5
//调整到 100 x 100
window.resizeTo(100, 100);
//调整到200 x 150
window.resizeTo(100, 50);

导航和打开窗口

弹出窗口

设置: height|left|location|menubar|resizable|scrollbars|status|toolbar|top|width|

1
window.open("http://www.google.com", "wroxWindow", "height=400,width=400,top=10,left=10,resizable=yes");

调用 close() 方法还可以关闭新打开的窗口。

1
2
wroxWin.close();
alert(wroxWin.closed); //true
弹出窗口屏蔽程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var blocked = false;
try {
var wroxWin = window.open("http:www.google.com"", "_blank");
if (wroxWin == null) {
blocked = true;
}
} catch (ex) {
blocked = true;
}
if (blocked) {
alert("The popup was blocked!");
}

间歇调用和超时调用

1
2
3
4
5
6
7
//不建议传递字符串
setTimeout ("alert('hello world')", 1000);
//推荐的调用方式
setTimeout(function(){
alert('hello world');
}, 1000);

虽然这两种调用方式都没有问题,但由于传递字符串可能导致性能损失,因此不建议以字符串作为第一个参数。
第二个参数是一个表示等待多长时间的毫秒数,但经过该时间后指定的代码不一定会执行。JavaScript 是一个单线程的解释器,因此一定时间内只能执行一段代码。为了要控制要执行的代码,就有一个JavaScript任务队列。这些任务会按照将它们添加到队列的顺序执行。setTimout()的第二个参数告诉 JavaScript 再过多长时间把当前任务到队列中。如果队列是空的,那么添加的代码回立即执行;如果队列不是空的,那么它就要等前面的代码执行完了以后再执行。

超时调用的代码都是在全局作用域总执行的,因此函数中 this 的值在非严格模式下指向 window 对象,在严格模式下是 undefined。

间歇调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var num 0;
var max = 10;
var intervalId = null;
function incrementNumber() {
num++;
//如果执行次数达到max设定的值,则取消后续尚未执行的调用
if (num == max) {
clearInterval(intervalId);
alert("Done");
}
},
intervalId = setInterval(incrementNumber, 500);

这个模式也可以使用超时调用来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var num 0;
var max = 10;
var intervalId = null;
function incrementNumber() {
num++;
//如果执行次数未达到max设定的值,则设置另一次超时调用
if (num < max) {
setTimout(incrementNumber, 500);
} else {
alert("Done");
}
}
setTimeout(incrementNumber, 500);

可见,在使用超时调用时,没有必要跟踪超时调用 ID,因为每次执行代码后,如果不在设置另一次超时调用,调用就会自行停止。一般认为,使用超时调用来模拟间歇调用的是一种最佳模式。在开发环境下,很少使用真正的间歇调用,原因是后一个间歇调用有可能在前一个间歇调用结束之前启用。而像前面实例中那样使用超时调用,则完全可以避免这一点。所以最好不要使用间歇调用。

系统对话框

确认对话框的典型用法如下。

1
2
3
4
5
if (confirm("Are you sure?")) {
alert("I'm so glad you're sure!");
} else {
alert("I'm sorry to hear you're not sure.");
}

调用 promet()。

1
2
3
4
var result = prompt("What is your name?", "");
if (result !== null) {
alert("Welcome, " + result);
}

其它

1
2
3
4
5
//显示"打印"对话框
window.print();
//显示"查找"对话框
window.find();

location 对象

属性名: hash|host|hostname|href|pathname|port|protocol|search

查询字符串参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function getQueryStringArgs(){
//取得查询字符串并去掉开头的问号
var qs = (location.search.length > 0 ? location.search.substring(1) : ""),
args = {},
items = qs.length ? qs.split("&") : [],
item = null,
name = null,
value = null,
//在 for 循环中使用
i = 0,
len = items.length;
//逐个将每一项添加到 args 对象中
for (i=0; i < len; i++) {
item = items[i].split("=");
name = decodeURIComponent(item[0]);
value = decodeURIComponent(item[1]);
if (name.length) {
args[name] = value;
}
}
return args;
}

位置操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//下列三行代码的效果完全一样
location.assign("http://www.google.com");
window.location("http://www.google.com");
location.href("http://www.google.com");
//设置新值来改变URL
//假设初始URL为 http://www.google.com/WileyCDA/
//将URL修改为"http://www.google.com/WileyCDA/#section1"
location.hash = "#section1";
//将URL修改为"http://www.google.com/WileyCDA/?q=javascript"
location.search = "?q=javascript"
//将URL修改为"http://www.yahoo.com/WileyCDA/"
location.hostname = "www.yahoo.com"
//将URL修改为"http://www.yahoo.com/mydir"
location.pathname = "mydir";
//将URL修改为"http://www.yahoo.com:8080/WileyCDA"
location.port = 8080;

没修改 location 的属性(hash除外),页面都会以新URL重新加载

当通过上述任何一种方式修改 URL 之后,浏览器的历史纪录中就会生成一条新纪录,因此用户通过单击”后退”按钮都会导航到前一个页面。要禁用这种行为,可以使用 replace() 方法。

1
location.replace("http://www.google.com");

实质问题

下面用一些代码来解释这个定义。

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

这段代码看起来和嵌套作用域中的示例代码很相似。基于词法作用域的查找规则,函数bar()可以
访问外部作用域中的变量a(这个例子中的是一个RHS引用查询)。
这是闭包吗?
技术上来讲,也许是。但根据前面的定义,确切地说并不是。我认为最准确地用来解释bar()对a的
引用的方法是词法作用域的查找规则,而这些规则只是闭包的一部分。(但却是非常重要的一部
分!)

从纯学术的角度说,在上面的代码片段中,函数bar()具有一个涵盖foo()作用域的闭包(事实上,涵
盖了它能访问的所有作用域,比如全局作用域)。也可以认为bar()被封闭在了foo()的作用域中。为
什么呢?原因简单明了,因为bar()嵌套在foo()内部。
但是通过这种方式定义的闭包并不能直接进行观察,也无法明白在这个代码片段中闭包是如何工
作的。我们可以很容易地理解词法作用域,而闭包则隐藏在代码之后的神秘阴影里,并不那么容易
理解。
下面我们来看一段代码,清晰地展示了闭包:

1
2
3
4
5
6
7
8
9
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2 ———— 朋友,这就是闭包的效果。

函数bar()的词法作用域能够访问foo()的内部作用域。然后我们将bar()函数本身当作一个值类型
进行传递。在这个例子中,我们将bar所引用的函数对象本身当作返回值。
在foo()执行后,其返回值(也就是内部的bar()函数)赋值给变量baz并调用baz(),实际上只是通过
不同的标识符引用调用了内部的函数bar()。
bar()显然可以被正常执行。但是在这个例子中,它在自己定义的词法作用域以外的地方执行。
在foo()执行后,通常会期待foo()的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器用
来释放不再使用的内存空间。由于看上去foo()的内容不会再被使用,所以很自然地会考虑对其进
行回收。
而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此没有被回
收。谁在使用这个内部作用域?原来是bar()本身在使用。
拜bar()所声明的位置所赐,它拥有涵盖foo()内部作用域的闭包,使得该作用域能够一直存活,以
供bar()在之后任何时间进行引用。
bar()依然持有对该作用域的引用,而这个引用就叫作闭包。
因此,在几微秒之后变量baz被实际调用(调用内部函数bar),不出意料它可以访问定义时的词法
作用域,因此它也可以如预期般访问变量a。
这个函数在定义时的词法作用域以外的地方被调用。闭包使得函数可以继续访问定义时的词法作
用域。
当然,无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包。

1
2
3
4
5
6
7
8
9
10
function foo() {
var a = 2;
function baz() {
console.log( a ); // 2
}
bar( baz );
}
function bar(fn) {
fn(); // 妈妈快看呀,这就是闭包!
}

把内部函数baz传递给bar,当调用这个内部函数时(现在叫作fn),它涵盖的foo()内部作用域的闭
包就可以观察到了,因为它能够访问a。
传递函数当然也可以是间接的。

1
2
3
4
5
6
7
8
9
10
11
12
13
var fn;
function foo() {
var a = 2;
function baz() {
console.log( a );
}
fn = baz; // 将baz分配给全局变量
}
function bar() {
fn(); // 妈妈快看呀,这就是闭包!
}
foo();
bar(); // 2

无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引
用,无论在何处执行这个函数都会使用闭包。

本质上无论何时何地,如果将函数(访问它们各自的词法作用域)当作第一级的值类
型并到处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听器、Ajax请求、跨窗口通
信、Web Workers或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使
用闭包!

通常认为IIFE是典型的闭包例子,但根据先前对闭包的定义,我并
不是很同意这个观点。

1
2
3
4
var a = 2;
(function IIFE() {
console.log( a );
})();

虽然这段代码可以正常工作,但严格来讲它并不是闭包。为什么?因为函数(示例代码中的IIFE)并
不是在它本身的词法作用域以外执行的。它在定义时所在的作用域中执行(而外部作用域,也就是
全局作用域也持有a)。a是通过普通的词法作用域查找而非闭包被发现的。
尽管技术上来讲,闭包是发生在定义时的,但并不非常明显,就好像六祖慧能所说:”既非风动,亦
非幡动,仁者心动耳。”

尽管IIFE本身并不是观察闭包的恰当例子,但它的确创建了闭包,并且也是最常用来创建可以被
封闭起来的闭包的工具。因此IIFE的确同闭包息息相关,即使本身并不会真的使用闭包。

循环和闭包

要说明闭包,for循环是最常见的例子。

1
2
3
4
5
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}

正常情况下,我们对这段代码行为的预期是分别输出数字1~5,每秒一次,每次一个。
但实际上,这段代码在运行时会以每秒一次的频率输出五次6。
这是为什么?
首先解释6是从哪里来的。这个循环的终止条件是i不再<=5。条件首次成立时i的值是6。因此,输出
显示的是循环结束时i的最终值。
仔细想一下,这好像又是显而易见的,延迟函数的回调会在循环结束时才执行。事实上,当定时器
运行时即使每个迭代中执行的是setTimeout(.., 0),所有的回调函数依然是在循环结束后才会被
执行,因此会每次输出一个6出来。
这里引伸出一个更深入的问题,代码中到底有什么缺陷导致它的行为同语义所暗示的不一致呢?
缺陷是我们试图假设循环中的每个迭代在运行时都会给自己“捕获”一个i的副本。但是根据作用域
的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭
在一个共享的全局作用域中,因此实际上只有一个i。
下面回到正题。缺陷是什么?我们需要更多的闭包作用域,特别是在循环的过程中每个迭代都需要
一个闭包作用域。
IIFE会通过声明并立即执行一个函数来创建作用域。
我们来试一下:

1
2
3
4
5
6
7
for (var i=1; i<=5; i++) {
(function() {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
})();
}

。这样不行。但是为什么呢?我们现在显然拥有更多的词法作用域了。的确每个延迟
函数都会将IIFE在每次迭代中创建的作用域封闭起来。
如果作用域是空的,那么仅仅将它们进行封闭是不够的。仔细看一下,我们的IIFE只是一个什么都
没有的空作用域。它需要包含一点实质内容才能为我们所用。
它需要有自己的变量,用来在每个迭代中储存i的值:

1
2
3
4
5
6
7
8
for (var i=1; i<=5; i++) {
(function() {
var j = i;
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})();
}

行了!它能正常工作了!。
可以对这段代码进行一些改进:

1
2
3
4
5
6
7
for (var i=1; i<=5; i++) {
(function(j) {
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})( i );
}

let声明,可以用来劫持块作用域,并且
在这个块作用域中声明一个变量。
本质上这是将一个块转换成一个可以被关闭的作用域。因此,下面这些看起来很酷的代码就可以
正常运行了:

1
2
3
4
5
6
for (var i=1; i<=5; i++) {
let j = i; // 是的,闭包的块作用域!
setTimeout( function timer() {
console.log( j );
}, j*1000 );
}

但是,这还不是全部!for循环头部的let声明还会有一个特殊的行
为。这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使
用上一个迭代结束时的值来初始化这个变量。

1
2
3
4
5
for (let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}

模块

还有其他的代码模式利用闭包的强大威力,但从表面上看,它们似乎与回调无关。下面一起来研究
其中最强大的一个:模块。

1
2
3
4
5
6
7
8
9
10
function foo() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! " ) );
}
}

正如在这段代码中所看到的,这里并没有明显的闭包,只有两个私有数据变量something
和another,以及doSomething()和doAnother()两个内部函数,它们的词法作用域(而这就是闭包)也
就是foo()的内部作用域。
接下来考虑以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! " ) );
}
return {
doSomething: doSomething,
doAnother: doAnother
};
}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3

这个模式在JavaScript中被称为模块。最常见的实现模块模式的方法通常被称为模块暴露,这里展
示的是其变体。
我们仔细研究一下这些代码。
首先,CoolModule()只是一个函数,必须要通过调用它来创建一个模块实例。如果不执行外部函数,
内部作用域和闭包都无法被创建。
其次,CoolModule()返回一个用对象字面量语法{ key: value, … }来表示的对象。这个返回的对
象中含有对内部函数而不是内部数据变量的引用。我们保持内部数据变量是隐藏且私有的状态。
可以将这个对象类型的返回值看作本质上是模块的公共API。
这个对象类型的返回值最终被赋值给外部的变量foo,然后就可以通过它来访问API中的属性方
法,比如foo.doSomething()。

doSomething()和doAnother()函数具有涵盖模块实例内部作用域的闭包(通过调用CoolModule()实
现)。当通过返回一个含有属性引用的对象的方式来将函数传递到词法作用域外部时,我们已经创
造了可以观察和实践闭包的条件。
如果要更简单的描述,模块模式需要具备两个必要条件。

  1. 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
  2. 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以
    访问或者修改私有的状态。
    一个具有函数属性的对象本身并不是真正的模块。从方便观察的角度看,一个从函数调用所返回
    的,只有数据属性而没有闭包函数的对象并不是真正的模块。
    上一个示例代码中有一个叫作CoolModule()的独立的模块创建器,可以被调用任意多次,每次调用
    都会创建一个新的模块实例。当只需要一个实例时,可以对这个模式进行简单的改进来实现单例
    模式:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    var foo = (function CoolModule() {
    var something = "cool";
    var another = [1, 2, 3];
    function doSomething() {
    console.log( something );
    }
    function doAnother() {
    console.log( another.join( " ! " ) );
    }
    return {
    doSomething: doSomething,
    doAnother: doAnother
    };
    })();
    foo.doSomething(); // cool
    foo.doAnother(); // 1 ! 2 ! 3

我们将模块函数转换成了IIFE,立即调用这个函数并将返回值直接赋值给单例的模
块实例标识符foo。
模块也是普通的函数,因此可以接受参数:

1
2
3
4
5
6
7
8
9
10
11
12
function CoolModule(id) {
function identify() {
console.log( id );
}
return {
identify: identify
};
}
var foo1 = CoolModule( "foo 1" );
var foo2 = CoolModule( "foo 2" );
foo1.identify(); // "foo 1"
foo2.identify(); // "foo 2"

模块模式另一个简单但强大的变化用法是,命名将要作为公共API返回的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var foo = (function CoolModule(id) {
function change() {
// 修改公共API
publicAPI.identify = identify2;
}
function identify1() {
console.log( id );
}
function identify2() {
console.log( id.toUpperCase() );
}
var publicAPI = {
change: change,
identify: identify1
};
return publicAPI;
})( "foo module" );
foo.identify(); // foo module
foo.change();
foo.identify(); // FOO MODULE

通过在模块实例的内部保留对公共API对象的内部引用,可以从内部对模块实例进行修改,包括添
加或删除方法和属性,以及修改它们的值。

大多数模块依赖加载器/管理器本质上都是将这种模块定义封装进一个友好的API。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var MyModules = (function Manager() {
var modules = {};
function define(name, deps, impl) {
for (var i=0; i<deps.length; i++) {
deps[i] = modules[deps[i]];
}
modules[name] = impl.apply( impl, deps );
}
function get(name) {
return modules[name];
}
return {
define: define,
get: get
};
})();

这段代码的核心是modules[name] = impl.apply(impl, deps)。为了模块的定义引入了包装函数(可
以传入任何依赖),并且将返回值,也就是模块的API,储存在一个根据名字来管理的模块列表中。
下面展示了如何使用它来定义模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
MyModules.define( "bar", [], function() {
function hello(who) {
return "Let me introduce: " + who;
}
return {
hello: hello
};
} );
MyModules.define( "foo", ["bar"], function(bar) {
var hungry = "hippo";
function awesome() {
console.log( bar.hello( hungry ).toUpperCase() );
}
return {
awesome: awesome
};
} );
var bar = MyModules.get( "bar" );
var foo = MyModules.get( "foo" );
console.log(
bar.hello( "hippo" )
); // <i>Let me introduce: hippo</i>
foo.awesome(); // LET ME INTRODUCE: HIPPO

“foo”和”bar”模块都是通过一个返回公共API的函数来定义的。”foo”甚至接受”bar”的示例作为依
赖参数,并能相应地使用它。
要理解模块管理器没有任何特殊的“魔力”。它们符合前面列出的模块模式的两个特点:为函数定义
引入包装函数,并保证它的返回值和模块的API保持一致。
换句话说,模块就是模块,即使在它们外层加上一个友好的包装工具也不会发生任何变化。

节点层次

Node 类型

确定节点类型

1
2
3
if (someNode.nodeType == Node.ELEMENT_NODE) { //在IE中无效
alert("Node is an element");
}

跨浏览器兼容

1
2
3
if (someNode.nodeType == 1) { //适用于所有浏览器
alert("Node is an element");
}

可以将 NodeList 对象转为数组

1
2
//在 IE8 及之前版本中无效
var arrayOfNodes = Array.prototype.slice.call(someNode.childNodes,0);

跨浏览器兼容

1
2
3
4
5
6
7
8
9
10
11
12
13
function convertToArray(nodes) {
var array = null;
try {
array = Array.prototype.slice.call(someNode.childNodes,0); //针对非 IE 浏览器
} catch (ex) {
array = new Array();
for (var i=0, len = nodes.length; i < len; i++) {
array.push(nodes[i]);
}
}
return array;
}

Element 类型

1
2
3
4
5
6
7
if (elemeent.tagName == "div") { //不能这样比较,很容易出错!
//在此执行某些操作
}
if (elemeent.tagName.toLowerCase() == "div") { //这样最好(适用于任何文档)
//在此执行某些操作
}
取得特性

在通过 JavaScript 以编程方式操作 DOM 时,开发人员经常不使用 getAttribute(),而是只使用对象的属性。只用在取得自定义特性值的情况下,才会使用 getAttribute() 方法。

DocumentFragment 类型

假设想为这个 <\ul id=”myList”><\/ul> 元素添加3个列表项。如果逐个添加列表项,将会导致浏览器反复渲染新信息。为了避免这个问题,可以像下面这样使用一个文档片段来保存创建的列表项,然后再一次性将他们添加到文档中。

1
2
3
4
5
6
7
8
9
10
11
var fragment = document.createDocumentFragment();
var ul = document.gtElementById("myList");
var li = null;
for (var i=0; i < 3; i++) {
li = document.createElement("li");
li.appendChid(document.createTextNode("Item " + (i+1)));
fragment.appendChild(li);
}
ul.appendChild(fragment);

DOM 操作技术

动态脚本

兼容 IE 和早期版本 Safari

1
2
3
4
5
6
7
8
9
10
function loadScriptString(code) {
var script = document.createElement("script");
script.type = "text/javascript";
try {
script.appendChild(document.createTextNode(code));
} catch (ex) {
script.text = code;
}
document.body.appdendChild(script);
}

实际上,这样执行代码与在全局全局作用域中巴相同的字符串传递给eval()是一样的。

动态样式

通用的解决方案

1
2
3
4
5
6
7
8
9
10
11
function loadStyleString(css) {
var style = document.createElement("style");
style.type = "text/css";
try {
style.appendChild(document.createTextNode(css));
} catch(ex) {
style.styleSheet.cssText = css; //兼容IE
}
var head = document.getElementsByTagName("head")[0];
head.appendChild(style);
}

调用这个函数的实例如下:

1
loadStyleString("body{background-color:red}");

如果专门针对 IE 编写代码,务必小心使用 styleSheet.cssText 属性。在重用同一个<\style>元素并再次设置这个属性时,有可能导致浏览器崩溃。同样,将 cssText 属性设置为空字符串也可能导致浏览器崩溃。

内联命名函数

1
2
3
4
5
6
7
8
9
var ninja = function myNinja(){
//在内联函数中,验证两个名字是等价的
assert(ninja == myNinja,"Thi function is named two things at once!");
};
ninja(); //调用函数执行内部验证
//验证内联函数的名称在内联外部是不是可用的
assert(typeof myNinja == 'undefined', 'But myNinja isn't defined outside of the function);

尽管可以给内联函数进行命名,但这些名称只能在自身函数内部才是可见的。匿名函数的名称和变量名称有点像,它们的作用域仅限于它们的函数。

这就是为什么要将全局函数作为window的方法进行创建的原因。不使用window的属性,我们没有办法引用这些函数。

存储函数

有时候,我们可能需要存储一组相关但独立的函数,事件回调管理是最明显的例子。向这个函数添加函数时,我们面临的挑战是要确定哪些函数在集合众不存在,而应该添加,以及哪些函数已经存在并且不需要再添加了。
显而易见但天真的做法是,将所有的函数保存在一个数组里,然后遍历数组检查重复的函数。只不过这种方式很一般,我们可以利用函数属性的特性,给函数添加一个附加属性从而实现上述目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var store = {
//持续跟踪要赋值的nextId
nextId: 1,
//创建一个对象作为缓存,用于存储函数
cache: {},
//向缓存中添加函数,但只有缓存不存在的情况下才能添加成功
add: function(fn) {
if (!fn.id) {
fn.id = store.nextId++;
return !!(store.cache[fn.id] = fn);
}
}
};
function ninja(){}
assert(store.add(ninja),"Function was safely added.");
assert(!store.add(ninja),"But it was only added once.");

自记忆函数

缓存记忆是构建函数的过程,这种函数能够记忆先前计算的结果。通过避免已经执行过的不必要复杂的计算,这种方式可以显著提高性能。

缓存记忆昂贵的计算结果

作为一个基本的示例,让我们看一个计算素数的简单算法(当然不是特别有效)。这只是一个复杂计算的简单例子,但这种方法却很容易适用于其它昂贵的计算,例如字符串的MD5哈希计算,比这里的实例复杂得多。

记忆之前计算出的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function isPrime(value) {
!isPrime.answers && isPrime.answers = {}; //创建缓存
//检测缓存过的值
isPrime.answers[value] != null && return isPrime.answers[value];
var prime = value !=1; //1 can never be prime
for(var i=2; i < value; i++) {
if(vaule % i ==0) {
prime = false;
break;
}
}
return isPrime.answers[value] = prime; //保存计算出的值
}
//测试是否正常
assert(isPrime(5), "5 is prime!");
assert(isPrime.answers[5],"The answer was cached!");

伪造数组方法

有时,我们可能想创建一个包含一组数据的对象。如果只是集合,则只需要创建一个数组即可。但在某些情况下,除了集合本身,可能会有更多的状态需要保存,比如集合项有关的一些数据。
Array(构造器)上已经存在可以进行集合处理的方法。可以将这些功能嫁接到我们自己的对象上。

模拟类似数组的方法

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
<body>
<input id="first"/>
<input id="second"/>
<script>
var elems = {
//保存元素的个数。我们要假装成是数组,那就需要保存元素项的个数
length:0,
//实现将元素添加到集合的方法。Array的原型中的一个方法可以做到。
add: function(elem) {
Array.prototype.push.call(this, elem);
},
//实现一个gather()方法,根据id值查找元素,并将其添加到集合中
gather: function(id) {
this.add(document.getElementById(id));
}
}
elems.gather("first");
assert(elems.length == 1 && elems[0].nodeType,"Verify that we have an element in our stash");
elems.gather("second");
assert(elems.length == 2 && elems[1].nodeType,"Verify the other insertion");
</script>
</body>

函数重载

函数的length属性

所有的函数都有一个有趣的属性,它并不为人知,但却让我们可以了解函数的声明,那就是length属性。不要将该属性和arguments参数的length属性弄混淆了。该属性值等于该函数声明时所要传入的形参数量。
因此,如果声明一个接收单个参数的函数,那这个函数的length属性值应该是1.

1
2
3
4
function makeNinja(name) {}
function makeSamurai(name,rank){}
assert(makeNinja.length == 1,"Only expecting a single argument");
assert(makeSamurai.length == 2, "Two arguments expected");

因此,对于一个函数,在参数方面,我们可以确定两件事。

通过其length属性,可以知道声明了多少命名参数。
通过arguments.length,可以知道在调用时传入了多少参数。

利用参数个数进行重载

基于传入的参数,有很多种方法可以判断并进行函数重载。一种通用的方法是,根据传入参数的类型执行不同的操作。另一种方法是,可以通过某些特定参数是否存在来进行判断。还有一种方法是通过传入参数的个数进行判断。

重载函数的方法

1
2
3
4
5
6
7
8
9
10
function addMethod(object,name,fn) {
//保存原有的函数,因为调用的时候可能不匹配传入的参数个数
var old = object[name];
object[name] = function(){
//如果该匿名函数的形参个数和实参个数匹配,就调用该函数
if (fn.length == arguments.length) return fn.apply(this, arguments)
//如果传入的参数不匹配,则调用原有的参数
else if (typeof old == 'function') return old.apply(this.arguments);
}
}

测试addMethod()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//创建一个基础对象,实现加载一些测试数据
var ninjas = {
values: ['Dean Edwards', 'Sam Stephenson', 'Alex Russell'];
};
//在基础对象上绑定一个无参数方法
addMethod(ninjas, 'find', function(){
return this.values;
});
//在基础对象上绑定一个单参数的方法
addMethod(ninjas, 'find',function(name){
var ret = [];
for (var i = 0; i < this.values.length; i++)
if (this.values[i].indexOf(name) == 0) ret.push(this.values[i]);
return ret;
});
//在基础对象上绑定一个两个参数的方法
addMethod(ninjas, 'find', function(first, last) {
var ret = [];
for (var i = 0; i < this.values.length; i++)
if (this.values[i] == (first + ' ' + last)) ret.push(this.values[i]);
return ret;
});

重载只适用于不同数量的参数,但并不区分类型,参数名称或其它东西。
这样的重载方法会有一些函数调用的开销,要考虑在高性能时用的情况。

函数判断

1
2
3
function isFunction(fn) {
return Object.prototype.toString.call(fn) == '[object Function]'
}

为什么不直接调用fn.toString()获取结果,原因有两个。

不同的对象可能有自己的toString()方法实现
JavaScript 中的大多数类型都已经有一个预定义的toString方法覆盖了Object.prototype提供的toString()方法

通过直接访问Object.prototype的方法,可以确保我们得到的不是覆盖版本的toString(),而且最终得到的准确信息正是我们所需要的。

递归

arguments.callee 是一个指向正在执行的函数的指针,因此可以用它来实现对函数的递归调用。

1
2
3
4
5
6
7
function factorial(num){
if(num <= 1){
return 1;
}else{
return num * arguments.callee(num-1);
}
}

通过使用 argument.callee 代替函数名,可以确保无论怎样调用函数都不会出问题。因此,在编写递归函数时,使用 arguments.callee 总比使用函数名更保险。
但在严格模式下,不能通过脚本访问 arguments.callee, 访问这个属性会导致错误。不过,可以使用函数表达式来达成相同的结果。

1
2
3
4
5
6
7
var factorial = (function f(num)) {
if (num <= 1) {
return 1;
} else{
return num * f(num-1);
}
}

即便把函数赋值给了另一个变量,函数的名字 f 仍然有效,所以递归调用照样能正确完成。这种方式在严格模式和非严格模式下都行得通。

闭包

闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createComparisonFunction(propertyName) {
return function(object1, object2) {
var value1 = object1[propertyName];
var value2 = object2[propertyName];
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
}
}

内部函数的代码访问了外部函数中的变量 propertyName。 即使这个内部函数被返回了,而且是在其它地方被调用了,但它仍然可以访问变量propertyName。之所以还能访问这个变量,是因为内部函数的作用域中包含 createComparisonFunction() 的作用域。要彻底搞清楚其中的细节,必须从理解函数被调用的时候都会发生什么入手。
当某个函数被调用时,会创建一个执行环境及相应的作用域链。然后,使用 arguments 和其他命名参数的值来初始化函数的活动对象。但在作用域链中,外部函数的活动对象始终位于第二位,外部函数的外部函数的活动对象始终位于第三位,……直至作为作用域终点的全局执行环境。
在函数执行过程中,为读取和写入变量的值,就需要在作用域中查找变量。

1
2
3
4
5
6
7
8
9
10
11
function compare(value1, value2) {
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
}
var result = compare(5, 10);

以上代码先定义了 compare() 函数, 然后又在全局作用域中调用了它。当调用 compare() 时,会创建一个包含 arguments、value1、value2 的活动对象。全局执行环境(包含 result 和 compare) 在 compare() 执行环境的作用域链中则处于第二位。

后台的每个执行环境都有一个表示变量的对象---变量对象。全局环境的变量对象始终存在,而像 compare() 函数这样的局部环境的变量对象,则只在函数的执行的过程中存在。
无论什么时候再函数中访问一个变量时,就会从作用域链中搜索具有响应名字的变量。一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。但是,闭包的情况又有所不同。
在另一个
在另一个函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到它的作用域链中。因此,在 createComparisonFunction() 函数内部定义的匿名函数的作用域链中,实际上将会包含外部函数 createComparisonFunction() 的活动对象。

1
2
var compare = createComparisonFunction("name");
var result = compare({name: "Nicholas"}, {name: "Greg"});

在匿名函数从 createComparisonFunction() 中被返回后,它的作用域链被初始化为包含 createComparisonFunction() 函数的活动对象和全局变量对象。这样,匿名函数就可以访问在 createComparisonFunction() 中定义的所有变量。更为重要的是,createComparisonFunction() 函数在执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。换句话说,当 createComparisonFunction() 函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中;直到匿名函数被销毁返回后,createComparisonFunction() 的活动对象才会被销毁。

1
2
3
4
5
6
7
8
//创建函数
var compareNames = createComparisonFunction("name");
//调用函数
var result = compareNames({ name: "Nicholas"}, { name, "Greg"});
//解除对匿名函数的引用(以便释放内存)
compareNames = null;

首先,创建的比较函数被保存在变量 compareNames 中。而通过将 compareNames 设置为等于null解除该函数的引用,就等于通知垃圾回收例程将其清除。随着匿名函数的作用域链被销毁,其它作用域(除了全局作用域)也都可以安全地销毁了。

闭包与变量

作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能取得包含函数中任何变量的最后一个值。因为闭包所保存的是整个变量对象,而不是某个特殊的变量。

1
2
3
4
5
6
7
8
9
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++) {
result[i] = function(){
return i;
};
}
return result;
}

此时每个函数都引用着保存变量 i 的同一个变量对象,所以在每个函数内部 i 的值都是10.但是,可以通过创建另一个匿名函数强制让闭包的行为符合预期。

1
2
3
4
5
6
7
8
9
10
11
12
function createFunctions(){
var result = new Array();
for(var i=0; i < 10; i++) {
result[i] = function(num){
return function() {
return num;
};
}(i);
}
return result;
}

关于this

this 对象是在运行时基于函数的执行环境绑定的:在全局函数中,this 等于 window,而当函数被作为某个对象的方法调用时,this 等于那个对象。不过,匿名函数的执行环境具有全局性,因此其 this 对象通常指向window。

1
2
3
4
5
6
7
8
9
10
11
12
13
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
}
}
}
alert(object.getNameFunc()()); //"The Window" (在非严格模式下)

每个函数在被调用时都会自动取得这两个特殊变量:this 和 arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。

this 和 arguments 也存在同样的问题。如果想访问作用域中的 arguments 对象,必须将该对象的引用保存到另一个闭包能够访问的变量中。

内存泄漏

如果闭包的作用域链中保存着一个HTML元素,那么久意味着该元素无法被销毁。

1
2
3
4
5
6
function assignHandler(){
var element = document.getElementById("someElement");
element.onclick = function(){
alert(element.id);
}
}

这个闭包创建了一个循环引用。由于匿名函数保存了一个对 assignHandler() 的活动对象的引用,因此就会导致无法减少 element 的引用数。只要匿名函数存在,element 的引用数至少也是 1,因此它所占用的内存就永远不会被回收。不过,这个问题可以通过稍微改一下代码来解决。

1
2
3
4
5
6
7
8
9
10
function assignHandler(){
var element = document.getElementById("someElement");
var id = element.id;
element.onclick = function(){
alert(id);
}
element = null;
}

通过把 element.id 的一个副本保存在一个变量中,并且在闭包中引用该变量消除循环引用。但仅仅做到这一步,还是不能解决内存泄漏的问题。因为闭包会引用闭包包含函数的整个活动对象,而其中包含着 element。即使闭包不直接引用 element,包含函数的活动对象中也仍然会保存引用。因此,有必要把 element 变量设为 null。这样就能够解除对 DOM 对象的引用,顺利地减少其引用数,确保正常回收其占用的内存。

模仿块级作用域

无论在什么地方,只要临时需要一些变量,就可以使用私有作用域。

1
2
3
4
5
6
7
8
function outputNumbers(count) {
(function () {
for (var i=0; i < count; i++) {
alert(i);
}
})();
alert(i); //导致一个错误!
}

私有变量

有权访问私有变量和私有函数的公有方法称为特权方法。有两种在对象上创建特权方法的方式。第一种是在构造函数中定义特权方法,基本模式如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function MyObject() {
//私有变量和私有函数
var privateVariable = 10;
function privateFunction(){
return false;
}
//特权方法
this.publicMethod = function (){
privateVariable++;
return privateFunction();
}
}

利用私有和特权成员,可以隐藏那些不应该直接被修改的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Person(name) {
this.getName = function(){
return name;
}
this.setName = function(value){
name = value;
}
}
var person = new Person("Nicholas");
alert(person.getName()); //"Nicholas"
person.setName("Greg");
alert(person.getName()); //"Greg"

在函数中定义特权方法也有缺点,那就是必须使用构造函数模式来达到这个目的。构造函数的缺点是针对每个实例都会创建同样一组新方法,而使用静态私有变量来实现特权方法就可以避免这个问题。

静态私有变量

通过在私有作用域中定义私有变量或函数,同样也可以创建特权方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(function(){
//私有变量和私有函数
var privateVariable = 10;
function privateFunction(){
return false;
}
//构造函数
MyObject = function(){
};
//公有/特权方法
MyObject.prototype.publicMethod = function(){
privateVariable++;
return privateFunction();
}
})();

这个模式在定义构造函数时并没有使用函数声明,而是使用了函数表达式。函数声明只能创建局部函数,但那并不是我们想要的。出于同样的原因,我们也没有在声明Object时使用 var 关键字。初始化未经声明的变量,总是会创建一个全局变量。因此,MyObject 就成了一个全局变量,能够在私有作用域外被访问到。但在严格模式下给未经生命的变量赋值会导致错误。
这个模式与在构造函数中定义特权方法的主要区别,就在于私有变量和函数是由实例共享的。由于特权方法是在原型上定义的,因此所有实例都使用同一个函数。而这个特权方法,作为一个闭包,总是保存着对包含作用域的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(function(){
var name = "";
Person = function(value){
name = value;
}
Person.prototype.getName = function(){
return name;
}
Person.prototype.setName = function(value){
name = value;
}
})();
var person1 = new Person("Nicholas");
alert(person1.getName()); //"Nicholas"
person1.setName("Greg");
alert(person1.getName()); //"Greg"
var person2 = new Person("Michael");
alert(person1.getName()); //"Michael"
alert(person2.getName()); //"Michael"

在这种模式下,变量name就变成了一个静态的、由所有实例共享的属性。也就是说,在一个实例上调用 setName() 会影响所有实例。而调用 seName() 或新建一个 Person 实例都会赋予 name 属性一个新值。
以这种方式创建静态私有变量会因为使用原型而增进代码复用,但每个实例都没有自己的私有变量。到底是使用实例变量,还是静态私有变量,最终还是要视具体需求而定。

多查找作用域链中的一个层次,就会在一定程度上影响查找速度。而这正是使用闭包和私有变量的一个明显的不足之处。

模块模式

前面的模式是用于为自定义类型创建私有变量和特权方法的。而模块模式则是为单例创建私有变量和特权方法。所谓单例,值得就是只有一个实例的对象。按照惯例,Javascript是以对象字面量的方式来创建单例对象的。

1
2
3
4
5
6
var singleton = {
name : value,
method : function(){
//这里是方法的代码
}
}

模块模式通过为单例添加私有变量和特权方法能够使其得到增强,其语法形式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var singleton = function(){
//私有变量和私有函数
var privateVariable = 10;
function privateFunction(){
return false;
}
//特权/公有方法和属性
return {
publicProperty: true,
publicMethod : function(){
privateVariable++;
return privateFunction();
}
}
}()

从本质上来讲,这个对象字面量定义的是单例的公共接口。这种模式在需要对单例进行某些初始化,同时又需要维护其私有变量时是非常有用的,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var application = function(){
//私有变量和函数
var components = new Array();
//初始化
components.push(new BaseComponent());
//公共
return {
getComponentCount : function(){
return components.length;
},
registerComponent : function(component){
if (typeof component == "object") {
components.push(component);
}
}
}
}()

如果必须创建一个对象并以某些数据对其初始化,同时还要公开一些能够访问这些私有数据的方法,那么就可以使用模块模式。以这种模式创建的每个单例都是 Object 的实例,因为最终要通过一个对象字面量来表示它。

块作用域

with

with关键字不仅是一个难于理解的结构,同时也是块作用域的一个例子
(块作用域的一种形式),用with从对象中创建出的作用域仅在with声明中而非外部作用域中有
效。

try/catch

非常少有人会注意到JavaScript的ES3规范中规定try/catch的catch分句会创建一个块作用域,其
中声明的变量仅在catch内部有效。

尽管这个行为已经被标准化,并且被大部分的标准JavaScript环境(除了老版本的IE浏览
器)所支持,但是当同一个作用域中的两个或多个catch分句用同样的标识符名称声明错误变
量时,很多静态检查工具还是会发出警告。实际上这并不是重复定义,因为所有变量都被安全
地限制在块作用域内部,但是静态检查工具还是会很烦人地发出警告。
为了避免这个不必要的警告,很多开发者会将catch的参数命名为err1、err2等。也有开发者干
脆关闭了静态检查工具对重复变量名的检查。

let

let关键字可以将变量绑定到所在的任意作用域中(通常是{ ..}内部)。换句话说,let为其声明的变量隐式地了所在的块作用域。

1
2
3
4
5
6
7
var foo = true;
if (foo) {
let bar = foo * 2;
bar = something( bar );
console.log( bar );
}
console.log( bar ); // ReferenceError

用let将变量附加在一个已经存在的块作用域上的行为是隐式的。在开发和修改代码的过程中,如
果没有密切关注哪些块作用域中有绑定的变量,并且习惯性地移动这些块或者将其包含在其他的
块中,就会导致代码变得混乱。
为块作用域显式地创建块可以部分解决这个问题,使变量的附属关系变得更加清晰。通常来讲,显
式的代码优于隐式或一些精巧但不清晰的代码。显式的块作用域风格非常容易书写,并且和其他
语言中块作用域的工作原理一致:

1
2
3
4
5
6
7
8
9
var foo = true;
<b>if</b> (foo) {
{ // <-- 显式的快
let bar = foo * 2;
bar = something( bar );
console.log( bar );
}
}
console.log( bar ); // ReferenceError

只要声明是有效的,在声明中的任意位置都可以使用{ .. }括号来为let创建一个用于绑定的块。在
这个例子中,我们在if声明内部显式地创建了一个块,如果需要对其进行重构,整个块都可以被方
便地移动而不会对外部if声明的位置和语义产生任何影响。

使用let进行的声明不会在块作用域中进行提升。声明的代码被运行之前,声明并不”存在”。

1
2
3
4
{
console.log( bar ); // ReferenceError!
let bar = 2;
}