《jquery实战》javascript 必知必会(2)

时间:2021-06-10 02:44:54

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)(有的地方叫函数声明的声明提前),而函数表达式不是。为了实际理解这个概念,思考下面的例子:

《jquery实战》javascript 必知必会(2)

例子中定义两个函数 funcDecl()funcExpr()。但是实际定义之前,我们执行了调用。首先调用(funcDecl();)成功,然后调用(funcExpr();)抛出错误。不同的行为是因为 funcDecl() 是升起的,而 funcExpr() 不是。

var 声明提前:

变量在声明它们的脚本或函数中都是有定义的,变量声明语句会被提前到脚本或函数的顶部。但是,变量初始化的操作还是在原来var语句的位置执行,在声明语句之前变量的值是undefined。

《jquery实战》javascript 必知必会(2)

下部分是实际执行的过程。

函数声明提前

函数声明是在预执行期执行的,就是说函数声明是在浏览器准备执行代码的时候执行的。因为函数声明在预执行期被执行,所以到了执行期,函数声明就不再执行了(人家都执行过了自然就不再执行了)。

函数声明和函数表达式 - 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 的属性。

《jquery实战》javascript 必知必会(2)

当函数通过这个属性引用调用时,代码如下:

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>

《jquery实战》javascript 必知必会(2)

  代码里的定义了三个对象,每个对象使用 handel 属性来区分对象的引用①。同样也为 handel 实例添加了属性,因此它易于辨认。

  然后定义了一个*函数,它可以返回任意作为任意函数上下文对象的 handel 属性的值②,并把同一个函数赋值给 obj1identifyMe 属性③。可以说在 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>

《jquery实战》javascript 必知必会(2)

这个例子中,我们创建了一个函数 timer(),在定义之后执行⑤。在 timer() 函数内声明了局部变量 local②,并且赋值为1,然后使用 window.setInterval() 方法来建立计时器,每隔2秒触发一次③。作为计时器的回调函数,我们指定了一个内敛函数来引用局部变量 local,通过向页面里名为 display 的元素附加 div 来展示当前时间和 local 变量的值①。作为回调函数的一部分,local 变量的值每次递增1④。

如果不了解闭包,也许会认为,因为回调函数会在 timer() 函数调用后2秒触发,local 变量的值在执行回调期间是未定义的。但是,加载页面并运行一小段时间,会看到如图A.4所示结果。

《jquery实战》javascript 必知必会(2)

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