20. 函数基本用法
函数定义
函数是一段Javascript代码,它只定义一次,但可以被执行或调用任意次。在 JavaScript 里,函数是对象,程序可以随意操控它们。
<xmp>
function sum (num1, num2) {
return num1 + num2;
}
</xmp>
函数声明
函数表达式定义(不带名称时称为匿名函数):一个典型的函数定义表达式包含关键字function,跟随其后的是一对圆括号,括号内是一个以逗号分割的列表,列表含有0个或多个标识符(参数名),然后再跟随一个由花括号包裹的javascript代码段(函数体),例如:
<xmp>
var square = function(x){
return x*x;
}
</xmp>
函数声明:关键字function 用来定义函数。
<xmp>
function funcname([arg1[,arg2[...,argn]]){
statements
}
■ 函数名称标识符funcname :函数名称是函数声明语句必需的部分。函数表达式,这个名字是可选的。
■ 一对圆括号():其中包含0个或多个用逗号隔开的标识符组成的列表。这些标识符是函数的参数名称。
■ 一对花括号{}:其中包含0条或多条javascript语句。
</xmp>
「函数声明」和「函数表达式」的区别
解析器在向执行环境中加载数据时,对「函数声明」和「函数表达式」并非一视同仁。解析器会率先读取「函数声明」,并使其在执行任何代码之前可用(可以访问);至于「函数表达式」,则必须等到解析器执行到它所在的代码行,才会真正被解释执行。请看下面的例子。
<xmp>
alert(sum(10,10));
function sum(num1, num2){
return num1 + num2;
}
</xmp>
以上代码完全可以正常运行。因为在代码开始执行之前,解析器就已经通过一个名为函数声明提升(function declaration hoisting)的过程,读取并将函数声明添加到执行环境中。对代码求值时,JavaScript引擎在第一遍会声明函数并将它们放到源代码树的顶部。所以,即使声明函数的代码在调用它的代码后面,JavaScript 引擎也能把函数声明提升到顶部。
<xmp>
alert(sum(10,10));
var sum = function(num1, num2){
return num1 + num2;
};
</xmp>
以上代码之所以会在运行期间产生错误,原因在于函数位于一个初始化语句中,而不是一个函数声明。换句话说,在执行到函数所在的语句之前,变量 sum 中不会保存有对函数的引用;而且,由于第一行代码就会导致“unexpected identifier”(意外标识符)错误,实际上也不会执行到下一行。
没有重载
函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定。
<xmp>
function addSomeNumber(num){
return num + 100;
}
function addSomeNumber(num) {
return num + 200;
}
var result = addSomeNumber(100); //300
</xmp>
这个例子中声明了两个同名函数,而结果则是后面的函数覆盖了前面的函数。可以看成函数表达式,能更好的理解:
<xmp>
var addSomeNumber = function (num){
return num + 100;
};
addSomeNumber = function (num) {
return num + 200;
};
var result = addSomeNumber(100); //300
</xmp>
在创建第二个函数时,实际上覆盖了引用第一个函数的变量 addSomeNumber 。
函数调用
构成函数主题的Javascript代码再定义之时并不会执行,只有调用该函数时,它们才会执行。有4种方式来调用Javascript函数:
■ 作为函数
■ 作为方法
■ 作为构造函数
■ 通过它们的call()和apply()方法间接调用
1.作为函数调用
javascript中的调用表达式(invocation expression)是一种调用(或执行)函数或方法的语法表示。它以一个函数表达式开始,这个函数表达式指代了要调用的函数。例如:
<xmp>
//计算阶乘的递归函数(调用自身的函数)
function factorial(x){
if(x <= 1) return 1;
return x * factorial(x-1);
}
factorial(8); //factorial是一个函数,8是一个参数
</xmp>
在一个调用中,每个参数表达式(圆括号之间的部分)都会计算出一个值,计算的结果作为参数传递给另外一个函数。这些值作为实参传递给声明函数时定义的形参。在函数体中存在 一个形参的引用,指向当前传入的实参列表,通过它可以获得参数的值。
2.作为方法调用
一个方法无非是保存在一个对象的属性里的javascript函数。方法调用和函数调用有一个重要的区别,即:调用下上文。属性访问表达式由2部分组成:一个对象和属性名称。在像这样的方法调用表达式里,对象成为调用上下文,函数体可以使用关键词this引用该对象。
<xmp>
var calculator = { //对象直接量
operand1: 1,
operand2: 1,
add: function(){
//注意this关键字的用法,这里的this指代当前对象
this.result = this.operand1 + this.operand2;
}
};
calculator.add(); //这个方法调用计算1+1的结果
calculator.result // 2
</xmp>
this是一个关键字,不是变量,也不是属性名。Javascript的语法不允许给this赋值。关键字this没有作用域的限制,嵌套的函数不会从调用它的函数中继承this。
如果嵌套函数作为方法调用,其this的值指向调用它的对象。
如果嵌套函数作为函数调用,其this值不是全局对象(非严格模式下),严格模式下是undefined。
<xmp>
var o = { // 对象o
m: function(){ // 对象中的方法m()
var self = this; // 将this的值保存至 一个变量中
console.log(this === o); // 输出true,this就是这个对象o
f(); // 调用辅助函数f()
function f(){ // 定义一个嵌套函数f()
console.log(this === o); // “false” this的值是全局对象或undefined
console.log(self === o); // “true”self指外部函数的this值
}
}
};
</xmp>
3.作为构造函数调用
如果函数或方法调用前带有关键字new,它就是构造函数调用。构造函数调用和普通的函数调用以及方法调用在实参处理、调用上下文和返回值方面都有不同。
<xmp>
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
</xmp>
4.通过它们的call()和apply()方法间接调用
我们可以将call() 和 apply()看做是某个对象的方法,通过调研方法的形式来间接调用函数。
每个函数都包含两个非继承而来的方法:apply()和call()。 他们的用途相同,都是在特定的作用域中调用函数。
call 方法可以用来代替另一个对象调用一个方法。
<xmp>
obj1.method1.call(obj2,argument1,argument2)
</xmp>
<xmp>
function add(a,b){
alert(a+b);
}
function sub(a,b){
alert(a-b);
}
add.call(sub,3,1);
</xmp>
apply()用途和call()相同,但apply()接收两个参数,一个是函数运行的作用域(this),另一个是参数数组。
<xmp>
function sum(num1, num2) {
return num1 + num2;
}
console.log(sum.call(window, 10, 10)); //20
console.log(sum.apply(window,[10,20])); //30
</xmp>
函数的形参和实参
在讲解什么是形参和实参之前,先让我们来看一个例子:
<xmp>
function addNum(para1,para2){
var sum = para1+para2;
return sum;
}
addNum(3,5)
</xmp>
形参:指函数定义时候的参数,在这里就是para1和para2。
实参:指实际传送给函数的参数,在这里就是3和5。
javascript中的函数定义并未指定函数形参的类型,函数调用也未对传入的实参值做任何类型检测。下面来讨论当调用函数时的实参个数和声明的形参个数不匹配时出现的情况。以避免非法的实参传入函数。
1.可选形参
当调用函数的时候传入的实参比函数声明时指定的形参个数要少,剩下的形参将设置为undefined值。因此在调用函数时形参是否可选以及是否可以省略应当保持较好的适应性。为了做到这一点,应当给省略的参数赋一个合理的默认值,来看下这个例子:
<xmp>
//将对象o中可枚举的属性名称追加至数组a中,并返回这个数组a
//如果省略a,则创建一个新数组并返回这个新数组
function getPropertyNames(o,a){
if(a === undefined) a=[]; //如果未定义,则使用新数组
for(var property in o) {
a.push(property );
}
return a;
}
//这个函数调用可以传入1个或2个实参
var a = getPropertyNames(o); // 将o的熟悉存储到一个新数组中
getPropertyNames(p,a); // 将p的熟悉追加至数组a中
</xmp>
注:当函数的实参可选时,往往传入一个无意义的占位符,惯用做法是传入null作为占位符,当然也可以使用undefined作为占位符
2.可变长的实参列表:参数对象
当调用函数的时候传入的实参个数超过函数定义时的形参个数时,没有办法直接获取得未命名值的引用。参数对象解决了这个问题。在函数体内,标识符arguments是指向实参对象的引用,实参对象是一个类似数组的对象。这样可以通过数字下标就能传入函数的实参值,而不是非要通过名字来得到实参。
<xmp>
function max(){
var max = Number.NEGATIVE_INFINITY; //负无穷大,溢出时返回该值。
//遍历实参,查找并记住最大值
for(var i = 0; i max){
max = arguments[i];
}
}
//返回最大值
return max;
}
var largets = max(1,10,100,2,3); //100
</xmp>
类似这种函数可以接收任意个数的实参,这种函数也称为“不定实参函数”(varargs function)
3.将对象属性用做实参
当一个函数包含超过三个形参时,对于程序员来说,要记住调用函数中实参的正确顺序实在让人头疼。为了解决这个问题,定义函数时,传入的实参都希尔一个单独的对象之中,在调用的时候传入一个对象,对象中的名/值对是真正需要的实参数据。
<xmp>
function easycopy(args){
arraycopy(
args.from,
args.from_start || 0, //注意这里设置了默认值
args.to,
args.to_start || 0,
args.length
);
}
var a = [1,2,3,4], b = [];
easycopy({from:a, to:b,length:4});
</xmp>
作为值的函数
因为ECMAScript中的函数名本身就是变量,所以函数也可以作为值来使用。也就是说不仅可以像传递参数一样把一个函数传递给另一个函数,而且可以将一个函数作为另一个函数的结果返回。
<xmp>
function callSomeFunction(someFunction, someArgument){
return someFunction(someArgument);
}
function add10(num){
return num + 10;
}
var result1 = callSomeFunction(add10, 10);
alert(result1); //20
</xmp>
函数属性和方法
每个函数都包含两个属性: length 和 prototype 。其中, length 属性表示函数希望接收的命名参数的个数,如下面的例子所示:
<xmp>
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
</xmp>
对于ECMAScript 中的引用类型而言, prototype 是保存它们所有实例方法的真正所在。诸如toString() 和 valueOf() 等方法实际上都保存在 prototype 名下,只不过是通过各自对象的实例访问罢了。
在创建自定义引用类型以及实现继承时, prototype 属性的作用是极为重要的。
在 ECMAScript 5 中, prototype 属性是不可枚举的,因此使用 for-in 无法发现。