JavaScript 中的 Function 类型及其 apply 方法

  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
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值