js中的函数
-
函数的基本概念
函数对于任何语言来说都是一个核心概念。通过函数可以封装任意多条语句,而且可以在任何地方任何时候调用执行。js中的函数使用function
关键字来声明,后跟一组参数以及函数体,函数的基本格式如下:
//函数基本格式
function functionName(arg0, arg1, ..., argN) {
statements
}
//函数示例
function sayHi(name, message) {
console.log("Hello" + name + "," + message);
}
函数通过其函数名来调用,后面还要加上一对圆括号和参数(圆括号中的参数如果有多个可以用逗号隔开)。具体如下所示:
sayHi("World", "The wind is very strong today."); //HelloWorld,The wind is very strong today.
js中的函数在定义时不必指定是否返回值。如果需要返回值,通过return
语句跟要返回的值来实现返回值。例如:
function sum(num1, num2) {
return num1 + num2;
}
sum(1, 2);
函数会在执行完return语句之后停止并立即退出。因此,位于return
语句之后的任何代码都永远不会执行。
function sum(num1, num2) {
return num1 + num2;
console.log("HelloWorld!"); //永远不会执行
}
return
语句也可以不带任何返回值,在这种情况下,函数在停止执行后将返回undefined
值。这种用法一般用在需要提前停止函数执行又不需要返回值的情况下。例如:
function sayHi(name, message) {
return;
console.log("Hello" + name + "," + message); //永远不会调用
}
推荐的做法是要么让函数始终都返回一个值,要么永远都不要返回值。否则,如果函数有时候返回值,有时候不返回值,会给代码调试带来不便。
-
定义函数的方法
函数实际上是对象。每个函数都是Function
类型的实例,而且都与其他引用类型一样具有属性和方法。由于函数是对象,因此函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定。
- 使用函数声明语法定义
function sum(num1, num2) {
return num1 + num2;
}
- 使用函数表达式定义
var sum = function(num1, num2) {
return num1 + num2;
};
3.使用Function构造函数
var sum = new Function("num1", "num2", "return num1 + num2"); //不推荐
从技术角度讲,第三种方法其实是一个函数表达式,但是不推荐使用这种方法定义函数,因为这种方法会导致两次解析代码(第一次是解析常规的js代码,第二次是解析传入构造函数中的字符串),从而影响性能。不过这种语法对于理解“函数是对象,函数名是指针”的概念非常直观。
由于函数名仅仅是指向函数的指针,因此函数名与包含对象指针的其他变量没有什么不同。也就是说,一个函数可能会有多个名字,例如:
function sum(num1, num2) {
return num1 + num2;
}
console.log(sum(1, 2)); //3
var anotherSum = sum;
console.log(anotherSum(1, 2)); //3
sum = null;
console.log(anotherSum(1, 2)); //3
-
函数声明与函数表达式
解析器向执行环境中加载数据时,会率先读取函数声明,并使其在执行任何代码之前可用(可以访问);至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正被解释执行。
console.log(sum(1, 2)); //3
function sum(num1, num2) {
return num1 + num2;
}
以上代码完全可以正常运行。因为在代码开始执行之前,解析器就已经通过一个名为函数声明变量提升的过程,读取并将函数声明添加到执行环境中。对代码求值时,js引擎在第一遍会声明函数并将它们放到源代码树的顶部。如果像下面的例子所示的,把上面的函数声明改为等价的函数表达式,就会在执行期间导致错误。
console.log(sum(1, 2)); //Uncaught TypeError: sum is not a function
var sum = function(num1, num2) {
return num1 + num2;
}
除了什么时候可以通过变量访问函数这一点区别之外,函数声明和函数表达式的语法其实是等价的。
也可以同时使用函数声明和函数表达式,例如
var sum = function sum() {}
。不过,这种语法在Safari中会导致错误。
-
函数的内部属性
在函数内部,有两个特殊的对象:arguments
和this
。其中,arguments
是一个类数组对象,包含着传入函数中的所有参数。虽然arguments
的主要用途是保存函数参数,但这个对象还有一个名叫callee
的属性,该属性是一个指针,指向拥有这个arguments
对象的函数。请看下面这个非常经典的阶乘函数。
function factorial(num) {
if(num <= 1) {
return 1;
} else {
return num * factorial(num - 1);
}
}
定义阶乘函数一般都要用到递归算法;如上面代码所示,在函数有名字,而且函数名字不会变的情况下,这样定义没有问题。但问题是这个函数的执行与函数名factorial
紧紧耦合在了一起。为了消除这种紧密耦合的现象,可以像下面这样使用arguments.callee
。
function factorial(num) {
if(num <= 1) {
return 1;
} else {
return num * arguments.callee(num - 1);
}
}
在这个重写后的factorial()
函数的函数体内,没有再引用函数名factorial
。这样,无论引用函数时使用的是什么名字,都可以保证正常完成递归调用。例如:
var anotherFactorial = factorial;
factorial = function() {
return 0;
};
console.log(anotherFactorial(5)); //120
console.log(factorial(5)); //0
函数内部的另一个特殊对象是this
,this
引用的是函数据以执行的环境对象——或者也可以说是this
值(当在网页的全局作用域中调用函数时,this
对象引用的就是window
)。
window.color = "red";
var o = {
color: "blue"
};
function sayColor() {
console.log(this.color);
}
sayColor(); //red
o.sayColor = sayColor;
o.sayColor(); //blue
-
函数的属性和方法
因为js中的函数是对象,因此函数也有属性和方法。每个函数都包含两个属性:length
和prototype
。其中length
属性表示函数希望接收的命名参数的个数。对于js的引用类型而言,prototype
是保存它们所有实例方法的真正所在。换句话说,诸如toString()
和valueOf()
等方法实际上都保存在prototype
名下,只不过是通过各自对象的实例访问罢了。在js中,prototype
属性是不可枚举的,因此用for-in
无法发现。
每个函数都包含两个非继承而来的方法:apply()
和call()
。这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内this
对象的值。
apply()
方法接收两个参数:一个是在其中运行函数的作用域,另一个是参数数组。其中,第二个参数可以是Array
的实例,也可以是arguments
对象。
function sum(num1, num2) {
return num1 + num2;
}
function callSum1(num1, num2) {
return sum.apply(this, arguments); //传入arguments对象
}
function callSum2(num1, num2) {
return sum.apply(this, [num1, num2]); //传入数组
}
console.log(callSum1(1, 2)); //3
console.log(callSum2(1, 2)); //3
call()
方法与apply()
方法的作用相同,区别仅在于接收参数的方式不同。对于call()
而言,第一个参数是this
不变,变化的是其余参数必须逐个列举出来传递给函数。
function sum(num1, num2) {
return num1 + num2;
}
function callSum(num1, num2) {
return sum.call(this, num1, num2);
}
console.log(callSum(1, 2)); //3
事实上,传递参数并非apply()
和call()
真正的用武之地;它们真正强大的地方是能够扩充函数赖以运行的作用域,对象不需要与方法有任何耦合关系。
window.color = "red";
var o = {
color: blue
};
function sayColor() {
console.log(this.color);
}
sayColor.call(this); //red
sayColor.call(window); //red
sayColor.call(o); //blue
es5中还定义了一个方法:bind()
。这个方法会创建一个函数的实例,其this
值会被绑定到传递给bind()
函数的值。
window.color = "red";
var o = {
color: blue
};
function sayColor() {
console.log(this.color);
}
var objSayColor = sayColor.bind(o);
objSayColor(); //blue
每个函数继承的
tolocaleString()
和toString()
方法始终都返回函数的代码。返回代码的格式因浏览器而异——有的返回的代码与源代码一样,而有的则返回函数代码的内部表示,即由解析器删除了注释并对某些代码作了改动之后的代码。由于存在这些差异,我们无法根据这两个方法返回的结果来实现任何重要功能;不过,这些信息在调试代码时倒是很有用。另外一个继承的方法valueOf()
方法同样也只返回函数代码。
参考书籍:《JavaScript高级程序设计(第3版)》。