A2 一等公民函数
在传统 OO 语言里,对象包含数据和方法。这些语言里,数据和方法通常是不同的概念:javascript另辟蹊径。
与其他 js 的类型一样,函数可以作为对象处理,如String、Number、Date等。
与其他对象类似,函数可以通过javascript函数定义 —— 此时函数可以
- 赋值给变量;
- 赋值给对象属性;
- 作为参数传递;
- 作为函数结果返回;
- 使用文本创建。
因为这个语言里,函数与其他对象的处理方式类似,所以我们说函数是一等对象(first-class object)。
在 js 里,函数可以做不同的事,也可以用不同的方式定义。
A2.1 函数表达式与函数声明
函数不仅可以调用值,而且会证明这是正确的。其中一种定义方式就是 函数声明(function declaration)。思考下面代码:
function doSomethingWonderful(){
alert('Does something wonderful');
}
函数声明由关键字 function
组成,接着是函数的名字,还有参数或者无参及函数体。
正确的代码里,定义函数名字是 doSomethingWonderful
,这是无参的函数。当调用时,就会执行函数体,这里只调用了 alert()
弹出消息。看起来没有返回值,但是在 js 里,如果没前面提到的*定义的变量会作为 window
对象的属性。 Function
对象也不例外。如果之前函数声明在*层次,那么可以创建与函数名同名的window属性。因此,下面的语句是等价的
function hello() { alert('Hi there!'); }
hello = function hello() { alert('Hi there!'); }
window.hello = function hello() { alert('Hi there!'); }
在支持 ECMAScript 6
规范的浏览器里,可以通过名为 name
的函数属性名访问函数。
在 js 里,函数可以作为代码的一部分定义,而且被称为函数表达式(function expression)。函数表达式的值可以作为函数对象。
var myFunc = function(){
alert('this is a function');
}
定义了一个变量myFunc
,赋值了一个函数。因为这是一条代码,注意这个函数没有名字(name 属性是空字符串),所以不能使用函数名调用它。但是,因为已经赋值给一个变量,所以可以按下面方法执行:
myFunc();
这并非函数声明与函数表达式的唯一区别。另外一个重要的却别:函数声明是升起的(hoisted)(有的地方叫函数声明的声明提前),而函数表达式不是。为了实际理解这个概念,思考下面的例子:
例子中定义两个函数 funcDecl()
和 funcExpr()
。但是实际定义之前,我们执行了调用。首先调用(funcDecl();)成功,然后调用(funcExpr();)抛出错误。不同的行为是因为 funcDecl()
是升起的,而 funcExpr()
不是。
var
声明提前:
变量在声明它们的脚本或函数中都是有定义的,变量声明语句会被提前到脚本或函数的顶部。但是,变量初始化的操作还是在原来var语句的位置执行,在声明语句之前变量的值是undefined。
下部分是实际执行的过程。
函数声明提前
函数声明是在预执行期执行的,就是说函数声明是在浏览器准备执行代码的时候执行的。因为函数声明在预执行期被执行,所以到了执行期,函数声明就不再执行了(人家都执行过了自然就不再执行了)。
函数声明和函数表达式 - myvin
可以采用同样的方式给变量赋值函数表达式,也可以作为属性赋值给对象:
var myObj = {
bar: function() {}
};
A2.2 回调函数
处理事件或计时器,或执行 AJAX 请求时,WEB 页面代码的本性是异步的。其中异步编程最流行的概念就是 回调函数 。
下面看计时器例子。可以调用计时器来触发 —— 假设5秒钟,通过传递适当的间隔时间给 window.setTimeout()
方法。但是这个方法怎么让等待时间结束后执行想要的方法?也是通过调用设置的函数实现的。
function hello(){alert('Hi there!');}
setTimeout(hello, 5000);
第一个参数设置给 setTimeout()
方法,是函数的引用。传递函数作为参数与传递其他值没有区别,就好像传递一个数一样。
当计时器结束时,调用 hello
函数。因为 setTimeout()
方法在代码里发起一个回调,所以函数被称为 回调函数(callback function)。
因为只使用一次函数,没必要创建函数名 hello
。除非在其他地方多次调用函数,否则没必要创建 window
属性来存储 hello
函数并把它传递给回调参数。更加优美的代码:
setTimeout(function() {alert('Hi there!');}, 5000);
这里在参数列表直接定义(实际是内联匿名函数),无须生成函数名字。可在 jQuery 中经常看到这种用法,没必要赋值给*属性。
在这个例子里创建的函数或者*函数(作为 window
属性),或者赋值给函数调用。也可以复制 Function 对象
给对象的属性。
A2.3 寻根求源
OO 语言会自动一共一种方式来引用当前方法内对象的实例。在JAVA和C#这种语言中,this
变量指向当前实例的引用。在JS中,类似的概念存在,也使用this关键字,还可以访问与函数关联的对象。但是JS实现的 this
与OO语言的不同。
- 在OO语言中,
this
通常引用的是声明方法类的实例。 - 在JS中,函数是一等对象,不会作为其他东西的一部分,对于通过
this
引用 —— 称为函数上下文(function context) —— 不是由函数声明来决定而是由函数调用(invoked)来确定的。
这意味着相同的函数可以有不同的上下文依赖,取决于如何调用它。这一点非常诡异,但是非常有用。
默认情况下,函数调用的上下文(this)属性包含了对于调用函数引用的对象。我们来看看摩托车代码的例子,修改对象的创建代码如下:
var ride = {
make: 'Yamaha',
model: 'XT660R',
year: 2014,
purchased: new Date(2015, 7, 21),
owner: {
name: 'Tg',
occupation: 'bounty hunter'
},
whatAmI: function() {
return this.year + ' ' + this.make + ' ' + this.model;
}
};
新增了代码:
whatAmI: function() {
return this.year + ' ' + this.make + ' ' + this.model;
}
新代码添加了一个名为 whatAmI
的属性,它引用了一个函数 Function
实例。新的对象层次,使用 Function
实例赋值给了一个名为 whatAmI
的属性。
当函数通过这个属性引用调用时,代码如下:
var bike = ride.whatAmI();
函数上下文(this)设置为 ride
引用的对象实例。结果变量 bike
设置为 '2014 Ymaha XT660R',因为函数用过 this
获得了对象的属性。
对于*函数也一样。记住*函数是 window 对象的属性,所以调用函数上下文是 window 对象。
虽然是通常和隐含的行为,但是JS给了我们现实控制函数上下文的机会。可以通过 Function
方法 call()
或者 apply()
来设置调用函数的上下文。虽然看起有点疯狂,但是作为一等对象,函数有通过 Function 构造函数定义的方法。
call()
方法调用函数指定第一个对象参数作为上下文,而剩余的参数作为调用函数使用 —— call()
的第二个参数变成调用函数的第一个参数,依次类推。apply()
方法与此方法的工作方式类似,除了第二个参数是数组参数用来调用函数使用。
call() 和 apply()
在 JavaScript 中, 函数是对象。JavaScript 函数有它的属性和方法。
call() 和 apply() 是预定义的函数方法。 两个方法可用于调用函数,两个方法的第一个参数必须是对象本身。
两者的区别在于第二个参数: apply传入的是一个参数数组,也就是将多个参数组合成为一个数组传入,而call则作为call的参数传入(从第二个参数开始)。
在 JavaScript 严格模式(strict mode)下, 在调用函数时第一个参数会成为 this 的值, 即使该参数不是一个对象。
在 JavaScript 非严格模式(non-strict mode)下, 如果第一个参数的值是 null 或 undefined, 它将使用全局对象替代。
- 通过 call() 或 apply() 方法你可以设置 this 的值, 且作为已存在对象的新方法调用。
为了强化概念,我们看一个例子。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Function Conctext Example</title>
</head>
<body>
<script type="text/javascript">
var obj1 = { handle: 'obj1' };
var obj2 = { handle: 'obj2' };
var obj3 = { handle: 'obj3' };
var value = 'test';
window.handle = 'window';
function whoAmI(param) {
return this.handle + '' + param;
}
obj1.identifyMe = whoAmI;
console.log(whoAmI(value)); //windowtest
console.log(obj1.identifyMe(value));//obj1test
console.log(whoAmI.call(obj2, value));//obj2test
console.log(whoAmI.apply(obj3, [value]));//obj3test
</script>
</body>
</html>
代码里的定义了三个对象,每个对象使用 handel
属性来区分对象的引用①。同样也为 handel
实例添加了属性,因此它易于辨认。
然后定义了一个*函数,它可以返回任意作为任意函数上下文对象的 handel
属性的值②,并把同一个函数赋值给 obj1
的 identifyMe
属性③。可以说在 obj1
上创建了一个名为 identifyMe
的方法,虽然函数是和对象独立声明的。
当 obj
作为函数 func
调用的上下文时,函数 func
作为对象 obj
的方法。为了更进一步演示这个概念,思考下面的代码:
console.log(obj1.identifyMe.call(obj3)); //obj3undefined
虽然作为 obj1
的属性引用了函数,但这时调用函数上下文是 obj3
,也进一步强调了函数声明无法决定上下文而是取决于如何调用函数。
A2.4 闭包
闭包(closure)指的是 Function
实例,与其执行需要的局部变量耦合在一起。当声明函数时,它可以引用自己范围内的任意变量。但是对于闭包,这些变量被函数携带,甚至在声明点之后,已经超出了范围,关闭声明。
回调函数在声明的时候引用局部变量是一个编写高效 javascript 代码的必备工具。使用计时器我们来看一个例子,列表2。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Closure Example</title>
</head>
<body>
<div id="display"></div>
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript">
function timer() {
var local = 1;
window.setInterval(
function() {
$('#display').append(
'<div>At ' + new Date() + ' local=' + local + '</div>'
);
local++;
},
2000);
}
timer();
</script>
</body>
</html>
这个例子中,我们创建了一个函数 timer()
,在定义之后执行⑤。在 timer()
函数内声明了局部变量 local
②,并且赋值为1,然后使用 window.setInterval()
方法来建立计时器,每隔2秒触发一次③。作为计时器的回调函数,我们指定了一个内敛函数来引用局部变量 local
,通过向页面里名为 display
的元素附加 div
来展示当前时间和 local
变量的值①。作为回调函数的一部分,local
变量的值每次递增1④。
如果不了解闭包,也许会认为,因为回调函数会在 timer()
函数调用后2秒触发,local
变量的值在执行回调期间是未定义的。但是,加载页面并运行一小段时间,会看到如图A.4所示结果。
虽然当 ready
处理器已经退出,local
变量已经超出了范围,函数声明的闭包,包含 local
,仍然存在于函数的生命周期范围内。
闭包的另一特性,函数上下文从来不会作为闭包的一部分。例如,下面的代码不会按照我们的预期执行。
···js
this.id = 'someID';
$('*').each(function(){
alert(this.id);
});
记住每个函数调用都有自己的函数上下文,所以,这个代码的回调函数内存底给 each()
函数上下文是 jQuery 集合中的元素(DOM元素),不是外部函数设置的 someID
属性。每次调用回调函数都会轮流显示 jQuery 集合中的每个元素的 ID。
当访问作为函数上下文的对象时,可以在局部变量里使用常见的版本来创建 this
引用的拷贝,这个局部变量将会包括在闭包里。思考下面的代码:
<div id="display"></div>
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript">
this.id = 'someID';
//var outer = this; //this 引用 window
$('#display').each(function() {
var outer = this; // this 引用 display
console.log(outer.id);
});
</script>
变量(绝大部分时间命名为 that),变成了闭包的一部分,因为它已经在回调函数内部被引用了,因此可以被访问。outer
赋值给任意上下文,而不是回调函数定义的上下文。例如,前面的代码包括在名为 foo 的函数内,outer
变量会引用 foo
函数的上下文。如果前面的代码定义在 HTML 页面里,而没有被函数包裹,则 outer
变量将会引用 window
对象。
修改后的代码现在显示警告框来展示字符串 someID
任意多次,只要 jQuery 集合中元素就行。
我们发现闭包在使用 jQuery 异步回调来创建优美代码时非常重要,尤其是在编写 Ajax 请求和事件处理代码时。