callee,caller,call()以及apply()的用法理解以及从中引申出的关于作用域跟this的理解

在函数内部,有两个特殊的对象:arguments和this。其中,arguments对象有一个名叫callee的属性,callee是一个一个指针,指向拥有arguments对象的函数。见一下的代码。
var factorial = function(num) {
    if (num <= 1) {
        return 1;
    }
    else {
        return num + factorial(num - 1);
    }
}
var newFactorial = factorial;
......
// factorial被重写或置为空等
factorial = null;
newFactorial(10); // 报错

在以上场景中,递归函数的执行与变量名factorial紧紧耦合在一起,因此若后续代码中变量的指向作了变动,则使用其他指向递归函数的指针调用函数时,调用失败。

使用arguments.callee则不会出现这种问题。

var factorial = function(num) {
    if (num <= 1) {
        return 1;
    }
    else {
        return num + arguments.callee(num - 1);
    }
}

caller是函数对象的一个属性。这个属性保存着调用当前函数的函数的引用,如果是在全局作用于中调用当前函数,它的值为null。

function outer1() {
    inner();
}

function outer2() {
    inner();
}

function inner() {
    // caller是保存着调用当前函数的函数的引用
    console.log(arguments.callee.caller);
}

outer1(); // 打印 outer1
outer2(); // 打印 outer2
这里一开始存在一个让我感到疑惑的地方,caller是指向调用当前函数的函数,那如果调用当前函数的是一个对象会如何,于是做了如下测试:

var obj = {
    testCaller: function() {
        console.log(arguments.callee.caller);
    }
};

obj.testCaller();  //打印出了null
结果打印出了null,可以这么理解,caller它指向的是作用域的创造者(全局则指向null),而作用域是以函数的执行来建立的,不是对象;想到这里,又想起一个问题,对this的理解,因此作了如下测试:
var color = "blue";
var obj = {
        color: "red",
        testCaller: function() {
            console.log(color);
            console.log(this.color);
        }
    };
obj.testCaller(); //第一行打印出了blue,第二行red
从如上结果可以看出,要理解作用域跟对象的概念,作用域是以函数执行来建立的,在作用域链的顶端是全局作用域,在使用变量的时候,会在作用域链当中一直往上层寻找;而对象,是一种数据结构,而不是代码的执行环境,因此并不具备自己的作用域,对象是不能建立作用域的,对象只是属于作用域中,活跃在作用域中的一种变量。当然严格讲函数也是对象,但在理解作用域这里要把它们区分开来。

this是指向对象的引用,因此调用this,相当于调用对象。

以上代码的执行,可以这样理解,首先是caller,可以理解为指向的是作用域的创造者(全局则指向null),因此对象调用函数,caller为null;console.log(color),首先在testCaller这个function创造的作用域里面寻找color,没找到则往上在全局作用域中寻找,找到的是blue;console.log(this.color),寻找的不是color,而是this指向的对象,即testCaller的调用者obj,也是在全局作用域中找到,打印出的是对象里面的属性color。

曾经受书本的误导,总是理解错误,把this或者说对象的属性调用,跟直接调用作用域中变量,这两者的不同,混淆在一起,要正确理解对象跟作用域,要知道作用域的产生是函数执行的效果。


言归正传,以上callee和caller,并没有在jQuery的源码中看到有使用,原因不清楚。


每个函数都包含两个非继承而来的方法:apply()和call()。这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内this对象的值。

call()跟apply()的区别仅在于接收参数的方式不同,call()跟apply()第一个参数是都this,apply()第二个参数(只有两个参数)可以是Array的实例,也可以是arguments对象,而call()的其余参数都直接传递给函数。这里只讨论call()如下代码:

function sum(num1, num2) {
    return num1 + num2;
}


function callSum(num1, num2) {
    return sum.call(this, num1, num2);// this指向window,在这里不起任何作用
}
console.log(callSum(5, 7));
可以看出使用call()可以调用sum函数本身,并且可以传入this,不过上面代码中,this不起任何作用。

再看如下代码:

var o = {
    color: "blue"
};
var color = "yellow";

function sayColor()
{
    console.log(this.color);
}
sayColor.call(this); // yellow
sayColor.call(o); // blue
传入的第一个参数不同,改变了sayColor()内部this对象,直接导致了结果的不同。书上如是说:apply()跟call()真正强大的地方是能够扩充函数赖以运行的作用域。

作用域链仍然是只有sayColor本身的以及全局作用域,作用域链本身没有改变,改变的是函数内部this的指向,相当于传了一个对象进去给this,即相当于作用域中添加了一个可直接用this来使用的新对象,从而达到作用域的扩充。

如果不用sayColor.call(o),而直接调用sayColor(),则this是指向sayColor()的调用者window。

综上,可以这样理解,有了call(),就可以根据函数内部本身对this的使用情况,以及本身业务的需要来传入不同对象然后调用函数,让函数使用不同对象里的数据去实现不同效果。

在书中代码中发现一处slice()使用call()的代码:

var nodeList = document.getElementById("test").childNodes;//test是测试时随意定的id
var nodeArray = Array.prototype.slice.call(nodeList, 0);
Element对象里的childNodes是一个NodeList(NodeList是DOM对象的一种),并不是Array的实例,但数据结构类似Array,可以使用Array的slice()方法将其转成Array。slice()如何实现大家可以轻松猜测,从这个例子,可以看出,任何函数,无论是哪一个对象的私有的函数,都可以通过call()去调用,并且第一个参数传入的对象可以为任何对象(只要传入对象的数据结构,跟函数内部实现不冲突,不出错,能够实现效果即可),这才是call()扩充函数作用域的真正强大之处,jQuery源码中多处使用了cal()。

书本上的介绍call()跟apply()的实例代码,是无法让读者能够理解到call()跟apply()真正强大之处的,多阅读不同开源组件的代码,才能更深刻的认识理解js语言的各种强大编码的可能性。




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值