@@ -30,4 +30,362 @@ JavaScript的函数具有两个主要特性,正是这两个特性让它们与
30
30
31
31
函数的第二个重要特性是它能提供作用域支持。在JavaScript中没有块级作用域(译注:在JavaScript1.7中提供了块级作用域部分特性的支持,可以通过let来声明块级作用域内的“局部变量”),也就是说不能通过花括号来创建作用域,JavaScript中只有函数作用域(译注:这里作者的表述只针对函数而言,此外JavaScript还有全局作用域)。在函数内所有通过var声明的变量都是局部变量,在函数外部是不可见的。刚才所指花括号无法提供作用域支持的意思是说,如果在if条件句内、或在for或while循环体内用var定义了变量,这个变量并不是属于if语句或for(while)循环的局部变量,而是属于它所在的函数。如果不在任何函数内部,它会成为全局变量。在第二章里提到我们要减少对全局命名空间的污染,那么使用函数则是控制变量的作用域的不二之选。
32
32
33
- ### 术语释义
33
+ ### 术语释义
34
+
35
+ 首先我们先简单讨论下创建函数相关的术语,因为精确无歧义的术语约定和我们所讨论的各种模式一样重要。
36
+
37
+ 看下这个代码片段:
38
+
39
+ // named function expression
40
+ var add = function add(a, b) {
41
+ return a + b;
42
+ };
43
+
44
+ 这段代码描述了一个函数,这种描述称为“带有命名的函数表达式”。
45
+
46
+ 如果函数表达式将名字省略掉(比如下面的示例代码),这时它是“无名字的函数表达式”,通常我们称之为“匿名函数”,比如:
47
+
48
+ // function expression, a.k.a. anonymous function
49
+ var add = function (a, b) {
50
+ return a + b;
51
+ };
52
+
53
+ 因此“函数表达式”是一个更广义的概念,“带有命名的函数表达式”是函数表达式的一种特殊形式,仅仅当需要给函数定义一个可选的名字时使用。
54
+
55
+ 当省略第二个add,它就成了无名字的函数表达式,这不会对函数定义和调用语法造成任何影响。带名字和不带名字唯一的区别是函数对象的name属性是否是一个空字符串。name属性属于语言的扩展(未在ECMA标准中定义),但很多环境都实现了。如果不省略第二个add,那么属性add.name则是"add",name属性在用Firebug的调试过程中非常有用,还能让函数递归调用自身,其他情况可以省略它。
56
+
57
+ 最后来看一下“函数声明”,函数声明的语法和其他语言中的语法非常类似:
58
+
59
+ function foo() {
60
+ // function body goes here
61
+ }
62
+
63
+ 从语法角度讲,带有命名的函数表达式和函数声明非常像,特别是当不需要将函数表达式赋值给一个变量的时候(在本章后面所讲到的回调模式中有类似的例子)。多数情况下,函数声明和带命名的函数表达式在外观上没有多少不同,只是它们在函数执行时对上下文的影响有所区别,下一小节会讲到。
64
+
65
+ 两种语法的一个区别是末尾的分号。函数声明末尾不需要分号,而函数表达式末尾是需要分号的。推荐你始终不要丢掉函数表达式末尾的分号,即便JavaScript可以进行分号补全,也不要冒险这样做。
66
+
67
+ > 另外我们经常看到“函数直接量”。它用来表示函数表达式或带命名的函数表达式。由于这个术语是有歧义的,所以最好不要用它。
68
+
69
+
70
+ ### 声明 vs 表达式:命名与提前
71
+
72
+ 那么,到底应该用哪个呢?函数声明还是函数表达式?在不能使用函数声明语法的场景下,只能使用函数表达式了。下面这个例子中,我们给函数传入了另一个函数对象作为参数,以及给对象定义方法:
73
+
74
+ // this is a function expression,
75
+ // pased as an argument to the function `callMe`
76
+ callMe(function () {
77
+ // I am an unnamed function expression
78
+ // also known as an anonymous function
79
+ });
80
+
81
+ // this is a named function expression
82
+ callMe(function me() {
83
+ // I am a named function expression
84
+ // and my name is "me"
85
+ });
86
+
87
+ // another function expression
88
+ var myobject = {
89
+ say: function () {
90
+ // I am a function expression
91
+ }
92
+ };
93
+
94
+ 函数声明只能出现在“程序代码”中,也就是说在别的函数体内或在全局。这个定义不能赋值给变量或属性,同样不能作为函数调用的参数。下面这个例子是函数声明的合法用法,这里所有的函数foo(),bar()和local()都使用函数声明来定义:
95
+
96
+ // global scope
97
+ function foo() {}
98
+
99
+ function local() {
100
+ // local scope
101
+ function bar() {}
102
+ return bar;
103
+ }
104
+
105
+ ### 函数的name属性
106
+
107
+ 选择函数定义模式的另一个考虑是只读属性name的可用性。尽管标准规范中并未规定,但很多运行环境都实现了name属性,在函数声明和带有名字的函数表达式中是有name的属性定义的。在匿名函数表达式中,则不一定有定义,这个是和实现相关的,在IE中是无定义的,在Firefox和Safari中是有定义的,但是值为空字符串。
108
+
109
+ function foo() {} // declaration
110
+ var bar = function () {}; // expression
111
+ var baz = function baz() {}; // named expression
112
+
113
+ foo.name; // "foo"
114
+ bar.name; // ""
115
+ baz.name; // "baz"
116
+
117
+ 在Firebug或其他工具中调试程序时name属性非常有用,它可以用来显示当前正在执行的函数。同样可以通过name属性来递归的调用函数自身。如果你对这些场景不感兴趣,那么请尽可能的使用匿名函数表达式,这样会更简单、且冗余代码更少。
118
+
119
+ 和函数声明相比而言,函数表达式的语法更能说明函数是一种对象,而不是某种特别的语言写法。
120
+
121
+ > 我们可以将一个带名字的函数表达式赋值给变量,变量名和函数名不同,这在技术上是可行的。比如:` var foo = function bar(){}; ` 。然而,这种用法的行为在浏览器中的兼容性不佳(特别是IE中),因此并不推荐大家使用这种模式。
122
+
123
+ ### 函数提前
124
+
125
+ 通过前面的讲解,你可能以为函数声明和带名字的函数表达式是完全等价的。事实上不是这样,主要区别在于“声明提前”的行为。
126
+
127
+ > 术语“提前”并未在ECMAScript中定义,但是并没有其他更好的方法来描述这种行为了。
128
+
129
+ 我们知道,不管在函数内何处声明变量,变量都会自动提前至函数体的顶部。对于函数来说亦是如此,因为他们也是一种对象,赋值给了变量。需要注意的是,函数声明定义的函数不仅能让声明提前,还能让定义提前,看一下这段示例代码:
130
+
131
+ // antipattern
132
+ // for illustration only
133
+
134
+ // global functions
135
+ function foo() {
136
+ alert('global foo');
137
+ }
138
+ function bar() {
139
+ alert('global bar');
140
+ }
141
+
142
+ function hoistMe() {
143
+
144
+ console.log(typeof foo); // "function"
145
+ console.log(typeof bar); // "undefined"
146
+
147
+ foo(); // "local foo"
148
+ bar(); // TypeError: bar is not a function
149
+
150
+ // function declaration:
151
+ // variable 'foo' and its implementation both get hoisted
152
+
153
+ function foo() {
154
+ alert('local foo');
155
+ }
156
+
157
+ // function expression:
158
+ // only variable 'bar' gets hoisted
159
+ // not the implementation
160
+ var bar = function () {
161
+ alert('local bar');
162
+ };
163
+ }
164
+ hoistMe();
165
+
166
+ 在这段代码中,和普通的变量一样,hoistMe()函数中的foo和bar被“搬运”到了顶部,覆盖了全局的foo和bar。不同之处在于,局部的foo()定义提前至顶部并能正常工作,尽管定义它的位置并不靠前。bar()的定义并未提前,只是声明提前了。因此当程序执行到bar()定义的位置之前,它的值都是undefined,并不是函数(防止当前上下文查找到作用域链上的全局的bar(),也就“覆盖”了全局的bar())。
167
+
168
+ 到目前为止我们介绍了必要的背景知识和函数定义相关的术语,下面开始介绍一些JavaScript所提供的函数相关的好的模式,我们从回调模式开始。同样,再次强调JavaScript函数的两个特殊特性,掌握这两点至关重要:
169
+
170
+ - 函数是对象
171
+ - 函数提供局部变量作用域
172
+
173
+ ## 回调模式
174
+
175
+ 函数是对象,也就意味着函数可以当作参数传入另外一个函数中。当你给函数writeCode()传入一个函数参数introduceBugs(),在某个时刻writeCode()执行了(或调用了)introduceBugs()。在这种情况下,我们说introduceBugs()是一个“回调函数”,简称“回调”:
176
+
177
+ function writeCode(callback) {
178
+ // do something...
179
+ callback();
180
+ // ...
181
+ }
182
+
183
+ function introduceBugs() {
184
+ // ... make bugs
185
+ }
186
+
187
+ writeCode(introduceBugs);
188
+
189
+ 注意introduceBugs()是如何作为参数传入writeCode()的,当作参数的函数不带括号。括号的意思是执行函数,而这里我们希望传入一个引用,让writeCode()在合适的时机执行它(调用它)。
190
+
191
+ ### 一个回调的例子
192
+
193
+ 我们从一个例子开始,首先介绍无回调的情况,然后在作修改。假设你有一个通用的函数,用来完成某种复杂的逻辑并返回一大段数据。假设我们用findNodes()来命名这个通用函数,这个函数用来对DOM树进行遍历,并返回我所感兴趣的页面节点:
194
+
195
+ var findNodes = function () {
196
+ var i = 100000, // big, heavy loop
197
+ nodes = [], // stores the result
198
+ found; // the next node found
199
+ while (i) {
200
+ i -= 1;
201
+ // complex logic here...
202
+ nodes.push(found);
203
+ }
204
+ return nodes;
205
+ };
206
+
207
+ 保持这个函数的功能的通用性并一贯返回DOM节点组成的数组,并不会发生对节点的实际操作,这是一个不错的注意。可以将操作节点的逻辑放入另外一个函数中,比如放入一个hide()函数中,这个函数用来隐藏页面中的节点元素:
208
+
209
+ var hide = function (nodes) {
210
+ var i = 0, max = nodes.length;
211
+ for (; i < max; i += 1) {
212
+ nodes[i].style.display = "none";
213
+ }
214
+ };
215
+
216
+ // executing the functions
217
+ hide(findNodes());
218
+
219
+ 这个实现的效率并不高,因为它将findNodes()所返回的节点数组重新遍历了一遍。最好在findNodes()中选择元素的时候就直接应用hide()操作,这样就能避免第二次的遍历,从而提高效率。但如果将hide()的逻辑写死在findNodes()的函数体内,findNodes()就变得不再通用了(译注:如果我将hide()的逻辑替换成其他逻辑怎么办呢?),因为修改逻辑和遍历逻辑耦合在一起了。如果使用回调模式,则可以将隐藏节点的逻辑写入回调函数,将其传入findNodes()中适时执行:
220
+
221
+ // refactored findNodes() to accept a callback
222
+ var findNodes = function (callback) {
223
+ var i = 100000,
224
+ nodes = [],
225
+ found;
226
+
227
+ // check if callback is callable
228
+ if (typeof callback !== "function") {
229
+ callback = false;
230
+ }
231
+ while (i) {
232
+ i -= 1;
233
+
234
+ // complex logic here...
235
+
236
+ // now callback:
237
+ if (callback) {
238
+ callback(found);
239
+ }
240
+
241
+ nodes.push(found);
242
+ }
243
+ return nodes;
244
+ };
245
+
246
+ 这里的实现比较直接,findNodes()多作了一个额外工作,就是检查回调函数是否存在,如果存在的话就执行它。回调函数是可选的,因此修改后的findNodes()也是和之前一样使用,是可以兼容旧代码和旧API的。
247
+
248
+ 这时hide()的实现就非常简单了,因为它不用对元素列表做任何遍历了:
249
+
250
+ // a callback function
251
+ var hide = function (node) {
252
+ node.style.display = "none";
253
+ };
254
+
255
+ // find the nodes and hide them as you go
256
+ findNodes(hide);
257
+
258
+ 正如代码中所示,回调函数可以是事先定义好的,也可以是一个匿名函数,你也可以将其称作main函数,比如这段代码,我们利用同样的通用函数findNodes()来完成显示元素的操作:
259
+
260
+ // passing an anonymous callback
261
+ findNodes(function (node) {
262
+ node.style.display = "block";
263
+ });
264
+
265
+ ### 回调和作用域
266
+
267
+ 在上一个例子中,执行回调函数的写法是:
268
+
269
+ callback(parameters);
270
+
271
+ 尽管这种写法可以适用大多数的情况,而且足够简单,但还有一些场景,回调函数不是匿名函数或者全局函数,而是对象的方法。如果回调函数中使用this指向它所属的对象,则回调逻辑往往并不像我们希望的那样执行。
272
+
273
+ 假设回调函数是paint(),它是myapp的一个方法:
274
+
275
+ var myapp = {};
276
+ myapp.color = "green";
277
+ myapp.paint = function (node) {
278
+ node.style.color = this.color;
279
+ };
280
+
281
+ 函数findNodes()大致如下:
282
+
283
+ var findNodes = function (callback) {
284
+ // ...
285
+ if (typeof callback === "function") {
286
+ callback(found);
287
+ }
288
+ // ...
289
+ };
290
+
291
+ 当你调用findNodes(myapp.paint),运行结果和我们期望的不一致,因为this.color未定义。因为findNodes()是全局函数,this指向的是全局对象。如果findNodes()是dom对象的方法(类似dom.findNodes()),那么回调函数内的this则指向dom,而不是myapp。
292
+
293
+ 解决办法是,除了传入回调函数,还需将回调函数所属的对象当作参数传进去:
294
+
295
+ findNodes(myapp.paint, myapp);
296
+
297
+ 同样需要修改findNodes()的逻辑,增加对传入的对象的绑定:
298
+
299
+ var findNodes = function (callback, callback_obj) {
300
+ //...
301
+ if (typeof callback === "function") {
302
+ callback.call(callback_obj, found);
303
+ }
304
+ // ...
305
+ };
306
+
307
+ 在后续的章节会对call()和apply()有更详细的讲述。
308
+
309
+ 其实还有一种替代写法,就是将函数当作字符串传入findNodes(),这样就不必再写一次对象了,换句话说:
310
+
311
+ findNodes(myapp.paint, myapp);
312
+
313
+ 可以写成:
314
+
315
+ findNodes("paint", myapp);
316
+
317
+ 在findNodes()中的逻辑则需要修改为:
318
+
319
+ var findNodes = function (callback, callback_obj) {
320
+
321
+ if (typeof callback === "string") {
322
+ callback = callback_obj[callback];
323
+ }
324
+
325
+ //...
326
+ if (typeof callback === "function") {
327
+ callback.call(callback_obj, found);
328
+ }
329
+ // ...
330
+ };
331
+
332
+ ### 异步事件监听
333
+
334
+ JavaScript中的回调模式已经是我们的家常便饭了,比如,如果你给网页中的元素绑定事件,则需要提供回调函数的引用,以便事件发生时能调用到它。这里有一个简单的例子,我们将console.log()作为回调函数绑定了document的点击事件:
335
+
336
+ document.addEventListener("click", console.log, false);
337
+
338
+ 客户端浏览器中的大多数编程都是事件驱动的,当网页下载完成,则触发load事件,当用户和页面产生交互时也会触发多种事件,比如click、keypress、mouseover、mousemove等等。正是由于回调模式的灵活性,JavaScript天生适于事件驱动编程。回调模式能够让程序“异步”执行,换句话说,就是让程序不按顺序执行。
339
+
340
+ “不要打电话给我,我会打给你”,这是好莱坞很有名的一句话,很多电影都有这句台词。电影中的主角不可能同时应答很多个电话呼叫。在JavaScript的异步事件模型中也是同样的道理。电影中是留下电话号码,JavaScript中是提供一个回调函数,当时机成熟时就触发回调。有时甚至提供了很多回调,有些回调压根是没用的,但由于这个事件可能永远不会发生,因此这些回调的逻辑也不会执行。比如,假设你从此不再用“鼠标点击”,那么你之前绑定的鼠标点击的回调函数则永远也不会执行。
341
+
342
+ ### 超时
343
+
344
+ 另外一个最常用的回调模式是在调用超时函数时,超时函数是浏览器window对象的方法,共有两个:setTimeout()和setInterval()。这两个方法的参数都是回调函数。
345
+
346
+ var thePlotThickens = function () {
347
+ console.log('500ms later...');
348
+ };
349
+ setTimeout(thePlotThickens, 500);
350
+
351
+ 再次需要注意,函数thePlotThickens是作为变量传入setTimeout的,它不带括号,如果带括号的话则立即执行了,这里只是用到这个函数的引用,以便在setTimeout的逻辑中调用到它。也可以传入字符串“thePlotThickens()”,但这是一种反模式,和eval()一样不推荐使用。
352
+
353
+ ### 库中的回调
354
+
355
+ 回调模式非常简单,但又很强大。可以随手拈来灵活运用,因此这种模式在库的设计中也非常得宠。库的代码要尽可能的保持通用和重用,而回调模式则可帮助库的作者完成这个目标。你不必预料和实现你所想到的所有情形,因为这会让库变的膨胀而臃肿,而且大多数用户并不需要这些多余的特性支持。相反,你将精力放在核心功能的实现上,提供回调的入口作为“钩子”,可以让库的方法变得可扩展、可定制。
356
+
357
+ ## 返回函数
358
+
359
+ 函数是对象,因此当然可以作为返回值。也就是说,函数不一定非要返回一坨数据,函数可以返回另外一个定制好的函数,或者可以根据输入的不同按需创造另外一个函数。
360
+
361
+ 这里有一个简单的例子:一个函数完成了某种功能,可能是一次性初始化,然后都基于这个返回值进行操作,这个返回值恰巧是另一个函数:
362
+
363
+ var setup = function () {
364
+ alert(1);
365
+ return function () {
366
+ alert(2);
367
+ };
368
+ };
369
+
370
+ // using the setup function
371
+ var my = setup(); // alerts 1
372
+ my(); // alerts 2
373
+
374
+ 因为setup()把返回的函数作了包装,它创建了一个闭包,我们可以用这个闭包来存储一些私有数据,这些私有数据可以通过返回的函数进行操作,但在函数外部不能直接读取到这些私有数据。比如这个例子中提供了一个计数器,每次调用这个函数计数器都会加一:
375
+
376
+ var setup = function () {
377
+ var count = 0;
378
+ return function () {
379
+ return (count += 1);
380
+ };
381
+ };
382
+
383
+ // usage
384
+ var next = setup();
385
+ next(); // returns 1
386
+ next(); // 2
387
+ next(); // 3
388
+
389
+ ## 自定义函数
390
+
391
+
0 commit comments