谈谈javascript的Function中那些隐藏的属性/方法:caller/callee/apply/call/bind

jopen 9年前

javascript的Function中有不少不那么常用,又或者用了也是 知其然而不知其所以然 的属性/方法,本文就来谈谈这一系列属性/方法: caller / callee / apply / call / bind 。

caller 属性

直接上DEMO比较好理解:

// caller demo {  function callerDemo() {      if (callerDemo.caller) {        var a = callerDemo.caller.toString();        console.log(a);      } else {        console.log("this is a top function");      }  }  function handleCaller() {      callerDemo();  }    handleCaller();    //"function handleCaller() { callerDemo(); }"  callerDemo();    //"this is a top function"

我们先来按照思路一步一步来看这段代码:

  1. 首先我们看到定义了俩function:handleCaller和callerDemo,并且还可以看出handleCaller函数里是调用了callerDemo函数的。

  2. 在callerDemo函数里,我们看到了本文介绍的主角之一: caller 属性,并且可以看出这 caller 属性是函数对象本身的一个成员属性。

  3. 在callerDemo函数里,有一段判断 caller 属性是否存在的代码,这段代码有什么意义呢?这就要看最后的结果了:直接调用callerDemo()发现此时 callerDemo.caller 是为空的,而反观通过调用handleCaller()并在其内部调用callerDemo()则 callerDemo.caller 不为空。这说明只有在函数里调用函数,才会生成 caller 属性,而直接在全局环境里调用函数则不会生成。

  4. 继续看 var a = callerDemo.caller.toString();console.log(a); ,这里打印出来的居然是handleCaller整个函数体,这说明,此时的 callerDemo.caller 实际上就是对于handleCaller这个函数对象的一个引用。

这么分析下来, caller 属性就很容易明白了:

  • caller 属性是帮助我们在当前函数里获取调用当前函数的某个未知函数,之所以称 未知函数 ,是因为我们在写一个函数时,很可能根本不知道哪个函数会调用到我们的这个函数。

  • 在全局环境中调用函数是不会生成此 caller 属性,因为不符合此属性的存在意义/价值(见上条)。

  • 只有在当前函数的内部(上下文环境)才能调用当前函数的 caller 属性,不能从外部调用。

callee 属性

还是先放代码:

function calleeDemo() {       console.log(arguments.callee);   } 

有了上文对 caller 属性的认知, callee 属性就很好理解了,它实际上就是对当前函数对象的一个引用。有以下的点需要注意:

  • callee 属性隶属于Function的一个隐藏对象—— arguments 中,这个 arguments 对象大家应该不陌生,表示的就是当前函数传入的参数,一般用于函数不限制参数数量的传参。

  • 与 caller 属性一样,也是要在当前函数的内部(上下文环境)才有效。

  • 可配合 caller 属性一起使用: arguments.callee.caller ,这样就可以完全忽略到具体的函数名了。

  • 函数递归时用起来比用函数名调用函数更带感!

apply / call 方法

这俩方法性质一样,只是用法稍有不同,因此放在一起来介绍。还记得我上一篇文章《 javascript如何判断变量的数据类型 》中介绍的利用 Object.prototype.toString.call 来判断数据类型的方法么:

function type(obj) {    return Object.prototype.toString.call(obj).slice(8, -1);    //换成用apply方法亦可  }
  • apply / call 方法的意义在于 借用 其它对象的成员方法来对目标对象执行操作。

  • 借用 的过程中, apply / call 方法会改变被借用的成员方法的上下文环境:把 this 这一与上下文环境高度相关的变量指向目标对象,而非原来的对象。看下面的这段代码:

function Point(x, y){      this.x = x;      this.y = y;  }  Point.prototype.move = function(x, y) {      this.x += x;      this.y += y;  }  var p = new Point(0,0);  var circle = {x:1, y:1, r:1};    //只是一个普通的Object对象  p.move.call(circle, 2, 1);    //借用了Point类对象中的move方法  //p.move.apply(circle, [2, 1]);    //等价于p.move.call(circle, 2, 1);

这里的circle只是一个普通的Object对象,不含任何自定义的成员方法,但通过 apply / call 方法,可以借用Point类对象定义的move方法来帮助circle达到目的(本例其实是圆心在坐标轴上的移动)。在 借用 Point类对象的move方法时,move方法中的this就不再指向p,而是指向circle了,达到了上下文环境改变的效果。

另外,从代码里也可以看出, call 方法与 apply 方法的区别仅在于: call 方法直接把需要传入的参数列在目标对象其后,而 apply 方法则以数组的形式整体传入。

bind 方法

bind 方法与 apply / call 方法也非常类似,相当于稍微再封装了一下,仍以上述DEMO作为案例:

function Point(x, y){      this.x = x;      this.y = y;  }  Point.prototype.move = function(x, y) {      this.x += x;      this.y += y;  }  var p = new Point(0,0);  var circle = {x:1, y:1, r:1};  // p.move.call(circle, 2, 1);  // p.move.apply(circle, [2, 1]);  var circleMove = p.move.bind(circle, 2, 1);    //此时并不执行move方法  circleMove();    //此时才执行

从上面这段DEMO可以看出, bind 方法其实是给 apply / call 方法缓了一下,也可以说是封装了一下方便后续调用,其实质上相当于下面的这段代码:

function circleMove() {      p.move.call(circle, 2, 1);  }  circleMove();

bind 方法兼容性适应

bind 方法,即 Function.prototype.bind ,属于 ECMAScript 5 ,IE从 IE 10 版本才开始支持,那怎么做兼容性适应呢?

if(typeof Function.prototype.bind !== 'function') {    Function.prototype.bind = function() {      var thatFunc = this;      var args = [];      for(var i = 0; i < arguments.length; i++) {        args[i] = arguments[i];      }        return function() {        var context = args.shift();        thatFunc.apply(context, args);      }    }  }

其思路是利用 apply 方法来封装成 bind 方法。

</div>

来自: http://hao.jser.com/archive/9130/