第8章 函数
函数的定义
1. 函数定义表达式
函数定义表达式定义一个JavaScript函数。表达式的值是这个新定义的函数。
一个典型的函数定义表达式包含关键字function
,跟随其后的是一对圆括号,括号内是一个逗号分割的列表,列表含有0个或多个标识符(参数名),然后再随后是一个由花括号包裹的JavaScript代码段(函数体),例如:
var square = function(x) { return x * x ;}
2. 函数声明语句
语法:
function funcname([arg1[,arg2[...,arn]]]) {
statements
}
funcname是要声明的函数的名称的标识符。函数名之后的圆括号中是参数列表,参数之间使用逗号分隔。当调用函数时,这些标识符则是指传入函数的实参。
函数声明语句通常出现在JavaScript代码的最顶层,也可以嵌套在其他函数体内。但在嵌套时,函数声明只能出现在所嵌套函数的顶部。也就是说,函数定义不能出现在if
语句,while
循环或其他任何语句中,正是由于声明位置的这种限制,ECMAScript标准规范中没有将函数声明归类为真正的语句。有一些JavaScript实现的确允许在出现语句的地方都可以进行函数声明,但是不同的实现在细节处理方式上有很大差别,因此将函数声明放在其他语句内的做法并不具备可移植性。
3. 以上二者区别
尽管函数声明语句和函数定义表达式包含相同的函数名,但二者仍然不同。
-
两种方式都创建了新的函数对象,但函数声明语句中的函数名是一个变量名,变量指向函数对象。
和通过
var
声明变量一样,函数定义表达式中函数被显示地“提前”到了脚本或函数的顶部。因此他它们在整个脚本和函数内都是可见的。 -
使用
var
的话,只有变量声明提前了-----变量的初始化代码仍然在原来的位置。然而使用函数声明语句的话,函数名称和函数体均被提前:脚本中所有函数和函数中所有嵌套的函数都会在当前上下文中其他代码之前声明。也就是说,可以在声明一个JavaScript函数之前调用它。
var foo = function () { console.log('foo1'); } foo(); // foo1 var foo = function () { console.log('foo2'); } foo(); // foo2
function foo() { console.log('foo1'); } foo(); // foo2 function foo() { console.log('foo2'); } foo(); // foo2
-
函数声明语句可以出现在全局代码里或者内嵌在其他函数中,但它们不能出现在循环,条件判断,或者
try
/catch
/finally
以及with
语句中。而函数定义表达式可以出现在JavaScript代码的任何地方。
-
函数调用
有四种方式来调用JavaScript函数:
- 作为函数
- 作为方法
- 作为构造函数
- 通过它们的
call()
,和apply()
间接调用
1. 函数调用:
对于普通的函数调用,函数的返回值成为调用表达式的值。如果该函数返回是因为解释器到达结尾,返回值就是undefined
。如果函数返回是因为解释器执行到一条return
语句,返回值就是return
之后的表达式,如果return
语句没有值,则返回undefined
。
根据ECMAScript3和非严格的ECMAScript5对函数调用的规定,调用上下文是全局对象,然而,在严格模式下,调用上下文则是undefined
。
以函数形式调用的函数通常不使用this关键字。不过,‘’this
‘’可以用来判断当前是否是严格模式。
//定义并调用一个函数来确定当前脚本运行时是否为严格模式
var strict = (function() { return !this;}());
2. 方法调用:
对方法调用的参数和返回值的处理,和上面所描述的普通函数调用完全一致。
但是方法调用和函数调用有一个重要的区别,即:调用上下文。
和变量不同,关键字this
没有作用域的限制,嵌套函数不会从调用它的函数中继承this
。如果嵌套函数作为方法调用,其this
的值指向调用它的对象。如果嵌套函数作为函数调用,其值不是全局对象就是undefined
。
很多人误以为调用嵌套函数时this
会指向调用外层函数的上下文。
如果你想访问这个外部函数的this
值,需要将this的值保存在一个变量里,这个变量和内部函数都在同一个作用域内。通常使用self
来保存this
,比如:
var o = {
m: function() {
var self = this;
console.log(this === o); //true
f();
function f() {
console.log(this === o); //false
console.log(self === o); //true
}
}
}
3. 构造函数调用
函数或者方法之前带有关键字new
,它就构成构造函数调用。
如果构造函数没有形参,JavaScript构造函数调用的语法是允许省略实参和圆括号的。
var o = new Object();
//上面代码等效于:
var o = new Object;
构造函数调用创建一个新的对象,并使用这个新对象作为调用上下文。
构造函数通常不使用return
关键字,它们通常初始化新对象,当构造函数的函数执行完毕时,它会显式返回。在这种情况下,构造函数调用表达式的计算结果就是这个新对象的值。然而如果构造函数显式使用return
语句返回一个对象,那么调用表达式的值就是这个对象。如果构造函数使用return
语句但没有指定返回值,或者返回一个原始值,那么这时将忽略返回值,同时使用这个新对象作为调用结果。
4. 间接调用
call()
和apply()
可以用来间接调用函数。
两个方法都允许显式指定调用所需的this
值,也就是说,任何函数可以作为任何对象的方法来调用,哪怕这个函数不是那个对象的方法。
两个方法都可以指定调用的实参。call()
方法使用它自有的实参列表作为函数的实参,apply()
方法则要求以数组形式传入参数。
函数的实参和形参
JavaScript中函数定义并未指定函数形参的类型,函数调用也未对传入的实参做任何类型检查。实际上,JavaScript函数调用甚至不继承传入形参的个数。
当调用函数的时候传入的实参比函数声明时指定的形参个数要少,剩下的形参都将设置为 undefined
值。
1. 可变长的实参列表:实参对象
当调用函数的时候传入的实参个数超过函数定义时的形参个数时,没有方法直接获得未命名值的引用。
参数对象解决了这个问题。在函数体内,标识符arguments
是指向实参对象的引用,实参对象是一个类数组对象,这样可以通过数字下标就能访问传入函数的实参值,而不用非要通过名字来得到实参。
假设定义了函数f,它的实参只有一个x。如果调用这个函数时传入两个实参,第一个实参可以通过参数名x来获得,也可以通过arguments[0]来得到。第二个实参只能通过arguments[1]来得到。
和真正的数组一样,arguments
也包含一个length
属性,用以标识其所包含元素的个数。因此,如果调用函数f()时传入两个参数,arguments.length
的值就是2。
function max(/*...*/) {
var max = Number.NEGATIVE_INFINITY;
//遍历实参,查找并记住最大值
for (var i = 0; i <arguments.length; i ++) {
if (var i = 0; i < arguments.length; i ++) {
max = arguments[i];
}
return max;
}
}
var largest = max(1,10,100,2,3,1000,4,5,10000,6); //10000
类似这种函数可以接收任意个数的实参,这种函数也称为“不定实参函数”
不定实参函数的实参个数不能为零。
arguments
并不是真正的数组,它是一个实参对象。每个实参对象都包含以数字为索引的一组元素以及length
属性,但它毕竟不是真正的数组。可以这样理解:它是一个对象,只是碰巧具有以数字为索引的属性。
作为值的函数
在JavaScript中,函数不仅仅是一种语法,也是值。也就是说,可以将函数赋值给变量,存储在对象的属性或数组的元素中,作为参数传入另一个函数等。
先看一个函数定义:
function square(x) { return x*x; }
这个定义创建一个新的函数对象,并将其赋值给变量square。函数的名字实际上看不见的,它仅仅是变量的名字,这个变量只带对象。
函数还可以赋值给其他的变量,并且仍可以正常工作:
var s = square;
square(4);
s(4);
函数同样可以赋值给对象的属性:
var o = { square: function(x) { return x*x ;} };
var y = o.square(16);
函数甚至不需要带名字,当把他们赋值给数组元素时:
var a = [ function(x) { return x*x ;}, 20 ];
a[0](a[1]); // 400
自定义函数属性
JavaScript中的函数并不是原始值,而是一种特殊的对象,也就是说,函数可以拥有属性。
当函数需要一个“静态”变量来在调用时保持某个值不变,最方便的方法就是给函数定义属性,而不是定义全局变量。。
//初始化函数对象的计数器属性
//由于函数声明被提前,因此这里是可以在函数声明之前给它的成员赋值的
uniqueInteger.counter = 0;
//每次调用这个函数都会返回一个不同的整数
//它使用严格属性来记住下一次将要返回的值
function unqueInteger() {
return uniqueInteger.counter++; //先返回计数器的值,然后计数器自增1
}
闭包
详细看:JavaScript 闭包,以下内容为补充内容。
很多人以为函数执行结束后,与之相关的作用域链似乎也不存在了,但在JavaScript中并非如此。
var a = (function() {
var counter = 0;
return function() {
console.log(counter++);
}
}());
a();//0
这段代码定义了一个立即调用的函数,以为整个函数的返回值赋值给变量a,而这个函数返回另一个函数,这是一个嵌套函数,嵌套的函数是可以访问作用域内的变量的。但外部函数返回之后,其他任务代码都无法访问counter变量,只有内部的函数才能访问它。
像counter一样的私有变量不是只能用在一个单独的闭包内,在同一个外部函数内定义的多个嵌套函数也可以访问它,这多个嵌套函数都共享一个作用域链,看一个例子:
function counter() {
var n = 0;
return {
count: function() { return n++; };
reset: function() { n = 0;}
};
}
var c = counter(), d= counter();//创建两个计数器
c.count()//0
d.count()//0
c.reset()//reset()和count()方法都可以访问私有变量n,共享状态
c.count()//0,因为什么重置了c
d.count()//1
count()
和reset()
两个方法都可以访问私有变量n.
每次调用counter()都会创建一个新的作用域和一个新的私有变量。因此,如果调用counter()两次,则会得到两个计数器对象,而且彼此包含不同的私有变量,调用其中一个计数器对象的count()或reset()不会影响到另一个对象。
函数属性,方法和构造函数
在JavaScript程序中,函数是值。对函数执行typeof
运输会返回字符串“function”,但是函数是JavaScript中特殊的对象。
接下来着重介绍函数属性和方法以及Function()构造函数。
1. length属性
在函数体里,arguments.length
表示传入函数的实参的个数。
而函数本身的length
属性则是有着不同含义。函数的length
属性是只读属性,它代表函数实参的数量,这里的参数指的是“形参”而非‘’‘实参’,也就是在函数定义时给出的实参个数,通常也是在函数调用时期望传入函数的实参个数。
arguments.length
:实际传入的实参个数
arguments.callee.length
:期望传入的实参个数
2. prototype属性
每个函数都包含一个prototype
属性,这个属性是指向一个对象的引用,这个对象称做“原型对象”。每一个函数都包含不同的对象原型。当将函数用做构造函数的时候,新创建的对象会从原型对象上继承属性。
3. call()方法和apply()方法
call()
和apply()
的第一个实参是要调用用函数的母对象,它是调用上下文,在函数体通过this
来获得对它的引用。
ECMAScript5 的严格模式中,call()
和apply()
的第一个实参都会变成this
的值,哪怕传入的实参是原始值甚至是null
或undefined
。
在ECMAScript3和非严格模式中,传入的null
和undefined
都会被全局对象代替,而其他原始值则会被相应的包装对象所替代。
要想以对象o的方法来调用函数f(),可以这样使用call()
和apply()
:
f.call(o);
f.apply(o);
对call()
来说,第一个调用上下文实参之后的所有实参就是要传入待调用函数的值。
比如,以对象o的方法的形式调用函数f(),并传入两个参数,可以使用这样的代码:
f.call(o,1,2);
apply()
方法和call()
类似,但传入实参的形式和call()
有所不同,它的实参都放入一个数组中:
f.apply(o,[1,2])
4. bind()方法
这个方法的主要作用就是将函数绑定至某个对象。
function f(y) { return this.x + y;}//这个是待绑定的函数
var o = { x :1 };//将要绑定的对象
var g = f.bind(o);//通过调用g(x)来调用o.f(x)
g(2) // 3
当函数f()上调用bind()
方法并传入一个对象o作为参数,这个方法将返回一个新的函数。
(以函数调用的方式)调用新的函数将会把原始的函数f()当做o的方法来调用。
ECMAScript5 中bind()
方法不仅仅是将函数绑定至一个对象,它还附带一些其他应用:除了第一个实参之外,传入bind()
的实参也会绑定至this
,这个附带的应用是一种常见的函数式编程技术。
var sum = function(x,y) { return x + y};//返回两个实参的和值
//创建一个类似sum的新函数,但this的值绑定到null
//并且第一个参数绑定到1,这个新的函数期望只传入一个实参
var succ = sum.bind(null,1);
succ(2)//3:x绑定到1,并传入2作为实参y
function f(y,z) {return this.x +y +z };//另外一个做累加计算的函数
var g = f.bind({x: 1},2);//绑定this和y
g(3)//6 :this.x绑定到1,y绑定到2,z绑定到3
5. Function()构造函数
不管是通过函数定义语句函数函数直接量表达式,函数的定义都要使用function
关键字。但函数还可以通过Function()
构造函数来定义:
var f = new Function("x", "y", "return x*y");
Function()
构造函数可以传入任意数量的字符串实参,最后一个实参所表示的文本就是函数体。它可以包含任意的JavaScript语句,每两条语句之间用分号分隔。传入构造函数的其他所有的实参字符串是指定函数的形参名字的字符串。如果定义的函数不包含任何参数,织须给构造函数简单传入一个字符串----函数体即可。
注意,Function()
构造函数并不需要通过传入实参以指定函数名。就像函数直接量一样,Function()
构造函数创建一个匿名函数。
关于Function()
构造函数有几点需要特别注意:
-
Function()
构造函数允许JavaScript在运行时动态地创建并编译函数。 -
每次调用
Function()
构造函数都会解析函数体,并创建新的函数对象。 -
Function所创建的函数并不是使用词法作用域,相反,函数体代码的编译总是会在顶层函数执行。正如下面代码:
var scope = "global"; function constructFunction() { var scope = "local"; return new Function("return scope"); //无法捕捉局部作用域 } //这一行代码返回global,因为通过Function()构造函数 // 所返回的函数使用的不是局部作用域 construcFunction()(); //"global"