Function 类型是 JavaScript 中的一种引用类型。所谓引用类型,就是一种将数据和功能组织在一起的数据结构(看起来很像类,但 JavaScript 里的引用类型与类并不是相同的概念)。在 JavaScript 中,每一个函数都是 Function 类型的实例。
函数的定义方式
一般情况下,Function 类型的对象(即函数)都通过函数声明语法来定义,如下function fname() {
函数体;
}
这样的函数声明方式有一个优点,即在代码执行之前,解析器会率先读取函数声明,并使其在任何代码执行前可用。下面的例子可以验证这一点
alert(text(1, 2)) // 3
// 并不会出现任何错误
function text(a, b) {
return a+b;
}
另一种定义函数的方法叫做函数表达式,它与函数声明法相差无几
var texe = function(a, b) {
return a+b;
};
// 注意最后的分号
不同于函数声明的是,函数表达式定义的函数必须等到解析器执行到他所在的代码行时,函数才会真正被定义。也就是说如下代码是不能被执行的
alert(sum(1, 2));
// 出错
var sum = function (a, b) { return a + b; };
最后一种定义函数的方法是使用 Function 类型的构造函数,就像构造其他引用类型的对象一样。但这是一种不被推荐的做法,因为这种语法给导致解析两次代码。例子如下
var sum = new Function("a", "b", "return a+b");
总结:常用的函数定义方法有两种:函数声明和函数表达式,它们除了语法的区别外,函数声明定义的函数在所有代码执行前可用,而函数表达式只有在执行到该行代码时才真正被定义。
PS:var 定义的变量与函数声明定义的函数有相同的特点,即都声明提前。函数的已经验证过了,下面是变量声明提前的例子
alert(a); // undefined
alert("这句会显示");
alert(b); // 由于 b 未定义而终止程序
alert("这句不会显示了");
var a = 1;
这个例子可以得出:JavaScript 中的 var 声明的变量在解释时会声明提前(会在所有代码执行之前被声明),但其值会留在原地。
函数名是变量
从上文函数定义的第二第三种方式不难看出,函数名其实就是一个变量。由于函数是对象,因此函数名实际上是一个指向函数对象的指针。函数名不会与某个函数绑定,一个函数也可以有多个函数名。像下面的例子function fname1() {
return "我是函数";
}
var fname2 = fname1;
alert("fname1:" + fname1()); //fname1:我是函数
alert("fname2:" + fname2()); //fname2:我是函数
fname1 = 2;
alert(fname1); // 2
alert("fname2:" + fname2()); //我是函数
函数中的属性和特殊对象
函数本身就是一个对象,它自然可以向其他对象一样拥有自己的属性的方法,而且它内部还有一些特殊的对象。
length 属性是函数对象的一个内置属性,它的值是函数期待得到的参数的数量。也就是函数命名参数的数量。看下例
function func(one, two, three) {
return "我是函数";
}
alert(func.length); // 3
arguments 对象是函数内部的一个特殊对象,它包含函数传入的所有参数。(ECMAScript 中的参数在内部使用一个数组来表示的,函数接收到的始终是这个数组,而不关心数组中参数的数量和类型。)在函数内部,可以通过 arguments 对象访问这个参数数组,从而获取传递给函数的每一个参数( arguments 并不是 Array 类型的对象,它只是与数组类似)。
下面的例子介绍了 arguments 对象的用法
function sum(num1, num2, num3) {
alert(arguments.length); // 3
// 通过访问其 length 属性可以得知总共传入了多少个参数
sum1 = num1 + num2;
sum2 = sum1 + arguments[2];
// 采用中括号的形式访问参数,就像数组,arguments[0] 表示第一个参数(本例中的 num1),arguments[1] 表示 第二个参数...
return sum2;
}
alert(sum(1, 2, 3)); // 6
PS:如果在函数内部修改命名参数的值,则 arguments 对象中与其对应的值也会被修改,即他们的值是同步的。但有一种情况例外,就是该命名参数没有被传入参数(会被默认为 undefined),此时修改命名参数的值不会被反映到 arguments 对象中,修改 arguments 对象中的值也不会被反映到相应的命名参数上。
arguments 对象还有一个名为 callee 的属性,该属性是一个指针,指向拥有该 arguments 对象的函数。看下面的例子
function test() {
// alert(arguments.callee == test); // true
return arguments.callee;
}
t_a_callee = test();
alert(t_a_callee == test); // true
// 这个属性用来写递归还是很不错的,可以消除函数名与函数的强耦合
this 对象是函数内部另一个特殊的对象。它引用的是函数执行的环境对象,在全局作用域中调用函数时,this 对象指向 window 。上面的是书上的话,而我的理解是:任何一个函数都有一个this,每一个函数也都各自属于某一个对象,而 this 就指向这个对象。为什么说“每一个函数也都各自属于某一个对象”?因为不属于任何其他对象的属性和方法,最终都是window对象的属性和方法(其实是 Global 对象,详见这里)下面来个例子
var a = "全局中的 a";
var obj = { a: "obj 中的 a" };
function test() {
var a = "test 中的 a";
// alert(this == window); // true
// 由于该函数在全局环境中调用,所以这里的 this 指向 window
alert("test中的this.a:" + this.a); // 全局中的 a
// 由于 test 函数在全局环境中调用,所以这里的 this 指向 window
function test_1() {
alert("test_1中的this.a:" + this.a);
alert("test_1中的this == window:" + (this == window));
}
alert("下面两个输出是在test中调用test_1")
test_1();
// 原本以为在 text 中调用 test_1 会输出 "test 中的 a",但结果是 "全局中的 a"
// 可见,执行环境并不能决定 this 的指向对象
// test_1 并没有直接的所属对象,所以他是属于 window 对象的(这句话很关键)
return test_1;
}
test_2 = test();
alert("下面两个输出是在全局中调用test_1")
test_2()
// 在全局调用 test_1 函数
// 由于该函数(test_1)在全局环境中被调用,所以函数体内的 this 还是指向 window
alert("下面两个输出是在全局中调用obj.test_1")
obj.test = test_2;
// text_1 和 test_2 和 obj.test 指向的是同一个函数
obj.test();
// 当我给 text_1 换了个所属对象后,函数中的 this 指向了 obj
// 此时的输出为:"obj 中的 a" 和 false
// 可见,this 指向谁,与它所在的函数属于哪个对象有关系
// alert("全局中的this.a:" + this.a); // 全局中的 a
// alert(this == window); // true
总结:以函数的形式调用时,this 永远指向 window 对象;以方法的形式调用时,this 则指向调用该方法的对象。
函数的方法
每个函数都包含两个非继承而来的方法:apply() 和 call()。这两个方法有强大且相同的功能:在特定的作用域中调用函数,即设置函数体内 this 对象的值。这使得它们能够扩充函数运行的作用域,让函数访问到原本无权访问的数据。
apply() 方法接受两个参数,第一个参数是在其中运行函数的作用域(即我们可以通过这个参数自定义函数的运行的环境);第二个参数是参数数组(传入函数的参数数组),它可以是 Array 的实例,或是 arguments 对象。
call() 方法的第一个参数与 apply() 方法相同,但不同的是其余参数都直接传递给函数,即传递给函数的参数必须逐个列举出来。
下面分别举例两个方法的用法
apply() 方法
var a = [1, 2, 3, 4, 5];
var obj = { a: [4, 5, 6, 7, 8] }
alert(Math.max(a)); // NaN,max的参数需要列举
function applyMax(arr) {
return Math.max.apply(this, arr);
}
alert(applyMax(a)); // 5
// 这样就不用一个一个的列举参数了,当然,它真正的作用并非如此
function sayA() {
alert(this.a);
}
sayA();
// 正常情况下,sayA 函数只能访问到其执行环境内的 a ,即 [1, 2, 3, 4, 5]
sayA.apply(obj); // 将 this 指向的对象设置为 obj
// 利用 apply() 方法,为其设置 this 对象,从而使其访问原本不能访问的数据,此处为 [4, 5, 6, 7, 8]
call() 方法
// 与 apply() 方法的功能一样,只是参数的传递形式不同
function sum(num1, num2) {
return num1 + num2;
}
function callSum(num1, num2) {
return sum.call(this, num1, num2)
// 这里必须要逐个列举参数
}
除了上面的那两种方法外,ECMAScript 5 还定义了一个方法,即 bind() 方法。这个方法的功能与 apply() 方法类似,但它更加直接。bind() 方法会创建一个函数实例,并使该函数实例的 this 值绑定到传给 bind() 方法的参数上面。看下例
var color = "red";
var obj = { color: "blue" }
function sayColor() {
alert(this.color);
}
sayColor(); // red
bindSayColor = sayColor.bind(obj);
// 将 bindSayColor 函数的 this 对象指向 obj
bindSayColor(); // blue