函数定义
函数的定义通常有两种方法:函数声明和函数表达式。
// 函数声明定义函数
function sayName() {
alert('paper_crane');
}
// 函数表达式定义函数
var showName = function() {
alert('paper_crane');
}
在上面的例子中,我们定义了两个函数,一个是通过函数声明定义的sayName()函数,一个是通过函数表达式定义的showName()函数。注意, 虽然以上两个函数都没有返回值(即使用return返回值), 但在JavaScript中,每个函数都会有返回值,若无规定的返回值,则返回undefined。上面两种函数的声明方式的区别在于解析器会率先读取函数声明,并使其在执行任何代码之前可用(可以访问),即被放到了源代码树的顶部,称为 函数声明提升;至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正被解释执行,所以不能在函数被定义之前调用此函数。
sayName(); // paper_crane
showName(); // 报错
// 函数声明定义函数
function sayName() {
alert('paper_crane');
}
// 函数表达式定义函数
var showName = function() {
alert('paper_crane');
}
参数
在ES规范中,函数接收的参数在内部是用一个类数组arguments来表示的,函数接收到的始终是这个数组。当显式的传入命名参数的时候,即可通过参数名来访问参数,又可使用数组来访问参数。这就导致,在你显式规定了该传入多少个命名参数之后,无论你传入多少个参数都不会因此导致错误,顶多是因为参数不足而是使运算结果不符合预期。
function sayHi() {
alert('Hi,' + arguments[0] + ',' + arguments[1]);
}
sayHi(); // Hi,undefined,undefined
sayHi('paper_crane'); // Hi,paper_crane,undefined
sayHi('paper_crane', 'crane'); // Hi,paper_crane,crane
sayHi('paper_crane', 'crane', 'paper'); // Hi,paper_crane,crane
上面的例子定义了一个函数sayHi(),没有显式传入参数,并且在内部访问了参数数组arguments。然后以0个参数,一个参数,两个参数,三个参数来调用此函数。然而不论用那种方式调用,都不会报错,当参数不足时,会默认其参数值为undefined,而参数过多也不会有任何影响。同时,如果定义函数的时候规定接收两个参数,那么无论是使用参数名还是arguments数组都能正确访问参数值。例如:
function sayHi(param1, param2) {
alert('Hi,' + arguments[0] + ',' + arguments[1]);
alert('Hi,' + param1 + ',' + param2);
}
sayHi(); // Hi,undefined,undefined
sayHi('paper_crane'); // Hi,paper_crane,undefined
sayHi('paper_crane', 'crane'); // Hi,paper_crane,crane
sayHi('paper_crane', 'crane', 'paper'); // Hi,paper_crane,crane
没有重载
在JavaScript中,函数其实也是一个对象,每个函数都是Function类型的实例,而且和引用类型一样具有属性和方法。由于函数是对象,因此函数名也只是一个指向函数对象的指针,不与某个函数绑定。所以当给某个函数名重复定义函数体的时候,只是在不断的改变其指向的函数定义,那么也就不存在重载了。在重复的为一个函数名重定义函数体的情况下,其指向最后为其赋值的函数定义。
function addSomeNumber(num) {
return num + 100;
}
function addSomeNumber(num) {
return num + 200;
}
var addSomeNum = function(num) {
return num + 100;
}
addSomeNum = function(num) {
return num + 200;
}
alert(addSomeNumber(100)); // 300
alert(addSomeNumber(100)); // 300
在上面的例子中,我们分别使用了函数声明和函数表达式来定义了函数addSomeNumber(),addSomeNum()两次,而函数名只属于后定义的函数。
函数作为值被传递
因为函数名是一个指针,指向一个函数对象,那么函数名就可以被当做参数传入另一个函数中,并且在另一个函数内部调用此函数。
function sayHi(name) {
alert('Hi, ' + name + '!');
}
function callFunction(callback) {
var name = 'paper_crane';
callback(name);
}
callFunction(sayHi); // Hi, paper_crane!
上面的例子定义了一个sayHi()函数,同时声明了一个callFunction()函数,其中将sayHi当成参数传入callFunction函数并且在callFunction函数内部成功调用。此时sayHi()函数可以称为是callFunction()函数的回调函数,这种用法在解决异步处理程序很有用,除此之外,调用Array.sort()函数时,也常常将一个比较函数作为其的回调函数。
函数内部属性
在函数内部,有两个特殊的对象:arguments和this。其中this对象的话在此不打算讨论,有兴趣的读者可以看看笔者另一篇博文:《
JavaScript执行环境、作用域及this值》。本文在前面有提到过arguments对象,此对象是一个包含所有函数参数的类数组,即能够通过方括号索引来访问其中的元素,但却不是数组对象,其主要作用是保存函数参数。但是这个arguments对象还有一个属性callee,此属性是一个指向拥有此arguments对象的函数的指针。
function factorial(num) {
if (num <= 1) {
return num;
} else {
return factorial(num - 1) * num;
}
}
function recursive(num) {
if (num <= 1) {
return num;
} else {
return arguments.callee(num - 1) * num;
}
}
alert(factorial(5)); // 120
alert(recursive(5)); // 120
在上面的例子中,我们声明了两个函数factorial和recursive,这两个函数都是利用了递归实现阶乘。recursive函数使用arguments.callee来调用本身实现了 递归,用法还是比较简单的。factorial函数在返回值中使用其本身的函数名来调用自身实现递归,这种方法的缺点是:紧密耦合;如果我们需要修改函数名,那么返回值处也需要修改调用的函数名,维护成本较高;而且我们知道函数名只不过是一个指针,一旦将此函数赋值给另外一个函数名,那么将很危险:
function factorial(num) {
if (num <= 1) {
return num;
} else {
return factorial(num - 1) * num;
}
}
var recursive = factorial;
factorial = null;
alert(recursive(5)); // Uncaught TypeError: factorial is not a function
上面的例子只是由上上个例子改写的,可以看到,将factorial赋值给recursive并将factorial指向null后,我们调用recursive就会报错,究其原因就是因为recursive内部调用了factorial,但是factorial已经指向空,也就是已经不是函数了,这种紧密耦合的方式给代码维护带来了极大的不便。
但是使用arguments.callee方法来消除紧密耦合也不是完全没问题的,在严格模式下或者在某些浏览器中,这种方式会报错,所以如果要实现递归,可以使用命名函数表达式的方法:
var factorial = (function f(num) {
if (num <= 1) {
return num;
} else {
return f(num - 1) * num;
}
});
alert(factorial(5)); // 120
var recursive = factorial;
factorial = null;
alert(recursive(5)); // 120
在上面的例子中,创建一个命名函数f,f内部通过函数名f来调用本身达到递归的目的,由于我们无法更改此函数名称,所以无论将其赋值给factorial还是recursive,都能够正确的运行。
函数还有一个caller属性,此属性是指向调用此函数的函数的指针,在严格模式下,不能为其赋值,否则会报错。
function outer() {
inner();
}
function inner() {
alert(inner.caller);
}
outer(); // 返回outer函数的函数体
函数属性方法
每个函数都包含两个属性:length和prototype。其中,length属性表示函数希望接收的命名参数的个数。
function sayName(name) {
alert(name);
}
function sum(num1, num2) {
return num1 + num2;
}
function sayHi() {
alert('hi');
}
alert(sayName.length); // 1
alert(sum.length); // 2
alert(sayHi.length); // 0
每个函数都包含两个非继承而来的方法:apply()和call()。这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内this对象的值(查看更多有关this指针的内容可以查看笔者另一篇博文《 JavaScript执行环境、作用域及this值》)。这两个方法的区别在于:apply()方法接收两个参数:一个是在其中运行函数的作用域(this的指向),另一个是参数数组,第二个参数可以是Array的实例,也可以是arguments对象。而call()方法与apply()的区别在于接收参数的方式不同,接收的第一个参数是运行函数的作用域(this的指向),剩下的参数必须一一列举传入,而不是传入一个数组。
function sum(num1, num2) {
return num1 + num2;
}
function callSum(num1, num2) {
return sum.call(this, num1, num2);
}
function callSum1(num1, num2) {
return sum.apply(this, arguments);
}
function callSum2(num1, num2) {
return sum.apply(this, [num1, num2]);
}
alert(callSum(10, 10)); // 20
alert(callSum1(10, 10)); // 20
alert(callSum2(10, 10)); // 20
这两个方法除了传入参数的区别外,没有其他的区别,至于使用哪一种调用方法coder可自行选择。
然而,传递参数并非apply()和call()真正的用武之地;他们真正强大的地方是能够扩充函数赖以生存的作用域。看一看例子
window.color = 'red';
var obj = {color: 'blue'};
function sayColor() {
alert(this.color);
}
sayColor(); // red
sayColor.call(this); // red
sayColor.call(window); // red
sayColor.call(obj); // red
通过上面的例子我们可以看出,传入的参数能够改变函数内部的this对象的指向。第一次调用函数的时候,因为函数作为全局函数,此时内部函数this指针指向全局作用域window;第二次调用函数时传入参数this,此参数this指向的是window,函数内部this指向window;第三次调用函数时,传入的参数是window, 函数内部this指向window;第四次调用函数的时候,传入的参数是obj,那么函数内部this指向的是obj,所以输出了“blue”。
ES5还给函数定义了一个方法:bind()。这个方法会创建一个函数的实例,其this值会被绑定到传给bind()函数的值。
window.color = 'red';
var o = {color:'blue'};
function sayColor() {
alert(this.color);
}
var objectSayColor = sayColor.bind(o);
objectSayColor(); // blue
好了,函数的相关知识先说到这里,以后学习到新的知识再更新。