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 请求和事件处理代码时。