Skip to content

Commit 55d5293

Browse files
committed
commit
1 parent 7b87352 commit 55d5293

File tree

2 files changed

+370
-12
lines changed

2 files changed

+370
-12
lines changed

README.markdown

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -85,17 +85,17 @@
8585

8686
- [背景知识](chapter4.markdown#a)
8787
- [术语释义](chapter4.markdown#a)
88-
- 声明 vs 表达式:命名与提前
89-
- 函数的name属性
90-
- 函数提前
91-
- 回调模式
92-
- 一个回调的例子
93-
- 回调和作用域
94-
- 异步事件监听
95-
- 超时
96-
- 库中的回调
97-
- 返回函数
98-
- 自定义函数
88+
- [声明 vs 表达式:命名与提前](chapter4.markdown#a)
89+
- [函数的name属性](chapter4.markdown#a)
90+
- [函数提前](chapter4.markdown#a)
91+
- [回调模式](chapter4.markdown#a)
92+
- [一个回调的例子](chapter4.markdown#a)
93+
- [回调和作用域](chapter4.markdown#a)
94+
- [异步事件监听](chapter4.markdown#a)
95+
- [超时](chapter4.markdown#a)
96+
- [库中的回调](chapter4.markdown#a)
97+
- [返回函数](chapter4.markdown#a)
98+
- [自定义函数](chapter4.markdown#a)
9999
- 立即执行的函数
100100
- 立即执行的函数的参数
101101
- 立即执行的函数的返回值

chapter4.markdown

Lines changed: 359 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,362 @@ JavaScript的函数具有两个主要特性,正是这两个特性让它们与
3030

3131
函数的第二个重要特性是它能提供作用域支持。在JavaScript中没有块级作用域(译注:在JavaScript1.7中提供了块级作用域部分特性的支持,可以通过let来声明块级作用域内的“局部变量”),也就是说不能通过花括号来创建作用域,JavaScript中只有函数作用域(译注:这里作者的表述只针对函数而言,此外JavaScript还有全局作用域)。在函数内所有通过var声明的变量都是局部变量,在函数外部是不可见的。刚才所指花括号无法提供作用域支持的意思是说,如果在if条件句内、或在for或while循环体内用var定义了变量,这个变量并不是属于if语句或for(while)循环的局部变量,而是属于它所在的函数。如果不在任何函数内部,它会成为全局变量。在第二章里提到我们要减少对全局命名空间的污染,那么使用函数则是控制变量的作用域的不二之选。
3232

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

Comments
 (0)