[函数] arguments、callee、caller是啥?

2,473 阅读6分钟

「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战」。

js中函数也是对象,他也有自己的属性和方法, 接下来就一起去看看吧~

function fn1(){};
let fn2 = function(){};
let fn3 = () => {};
let fn4 = new Function()
console.dir(fn1);
console.dir(fn2);
console.dir(fn3);
console.dir(fn4)

image.png

通过上面👆🏻可以发现: 除了fn3(箭头函数)之外,其他方式定义的函数都有argumentscaller, length, name, prototype;

展开fn3的arguments与caller看一下: image.png TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them at Function.
这个的意思是,caller、callee 和 arguments属性不能在严格模式函数或用于调用它们的参数对象上访问。
除此之外也能明显的发现,箭头函数没有prototype属性。

喝杯茶,坐下慢慢来看。(先搞清楚这些都是干毛线的,箭头函数的情况后期单独展开分析)

length

length 属性保存函数定义的命名参数的个数, 来看:

function fn11(){};
function fn22(num){};
function fn33(num1,num2){};
console.log(fn11.length); // 0
console.log(fn22.length); // 1
console.log(fn33.length); // 2

上面代码中,定义了三个函数,每个函数的命名参数都不一样,fn11()没有命名参数,length为0;fn22有一个命名参数,length为1; fn3有两个命名参数,length为2。

arguments

arguments是一个类数组对象,在函数调用时创建,它存储的是实际传递给函数的参数,并不局限于函数定义时的参数列表。
类数组对象特点: 具有index和length属性,但不具有数组的操作方法,比如push,shift等。

function fnArgu(num){console.dir(arguments)};
fnArgu(123,'qwe');
fnArgu();

image.png image.png 上面代码中,定义了一个函数fnArgu 想要接收一个参数, 但是我就不给它传一个参数,我给他传两个,我也可以不给他传,这有问题吗?没有问题!

为什么可以这样随意?
因为函数根本不关注我们具体传递几个参数,也不在乎传进来的参数是什么类型的,我们给函数传的参数都由arguments这个类数组对象掌管。 函数只关注arguments就可以了。所以在函数内部,通过arguments[index](index就是参数对应的下标)的方式可以取得每个参数。

function colors1(co1, co2){
    console.log(co1 + ',' + co2)
}
colors1('red','blue');//red,blue

function colors2(){
    console.log(arguments[0] + ',' + arguments[1])
}
colors2('yellow','green'); //yellow,green

上面代码中,定义了两个函数,第一个函数接收两个命名参数,内部可直接使用命名参数;第二个函数定义时没有写接收的参数,但在调用第二个时同样给它传两个参数,这时内部通过arguments可以取得对应的参数,没有问题。
所以命名参数不是不必须的,它存在就是为了方便我们操作。

当然,arguments对象也可以与命名参数一起使用,如下👇🏻

function calcu(num1,num2){
    console.log(num1 + '-' + arguments[0]);
    console.log(num2 + '-' + arguments[1]);
}
calcu(3,5);// 3-3; 5-5

从上面可以看出,arguments[0] 对应第一个参数, arguments[1] 对应第二个参数,相互 值都是一样的,所以用谁都一样。

那么问题来了,如果我使用arguments改变某个参数值,对应的命名参数会保持同步吗?

function calcu(num1,num2){
    num1 = 2;
    arguments[1] = 10;
    console.log(num1 + '-' + arguments[0]);
    console.log(num2 + '-' + arguments[1]);
}
calcu(3,5); //2-2; 10-10

通过上面可以看出,它们是的值是保持同步的。

它们的内存空间是独立的,但 它们的值会同步。

那么如果是在严格模式下呢?

"use strict";
function calcu(num1,num2){
    num1 = 2;
    arguments[1] = 10;
    console.dir(arguments)
    console.log(num1 + '-' + arguments[0]);
    console.log(num2 + '-' + arguments[1]);
}
calcu(3,5); // 2-3;  5-10

image.png 通过上面可以看到,严格模式下,修改arguments[1] ,但是num2并没有变化,依旧是传入的那个值。同样的修改了num1,也没有影响arguments[0]
严格模式下,命名参数与arguments对象是完全独立的。

函数重载?没有重载?

不懂就百度系列,感觉描述的十分清晰了

重载函数是函数的一种特殊情况,为方便使用,C++允许在同一范围中声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同,也就是说用同一个函数完成不同的功能。这就是重载函数。重载函数常用来实现功能类似而所处理的数据类型不同的问题。不能只有函数返回值类型不同。

JavaScript中没有像上面说的那样的重载功能;就拿同名函数来说,使用js的都知道同名的 后面的覆盖前面的定义的。主要就是js没有函数签名,在上上面有提到过,js中的函数他不在乎传递的参数个数或类型,它的参数就是一个可以存放0个或多个值得数组表示。
但是js这么牛,没有就想办法模拟一个呗!不就是通过不同的参数(个数、类型)实现不同的功能嘛

// 简单模拟一个
function calcu(num1, num2){
    if(arguments.length == 1){
        return num1 * 2
    }else if(arguments.length == 2){
        return num1 + num2
    }
}
console.log(calcu(2)); // 4
console.log(calcu(2,23)); // 25

上面函数内部,利用arguments,根据参数个数处理不同情况。
那么除了上面这种有没有更好的方式,哈嘿,还真找到一个不错的方式,扩展性也好。(闭包的应用之一)

function calcu(obj, name, fn){
    let old = obj[name]; //创建一个临时变量记录上次
    obj[name] = function() { // 重写了obj[name]的方法
        // 如果调用obj[name]方法时,传入参数与预期一致,就直接调用
        // fn.length 创建时命名参数个数
        // arguments.length 方法收到的实际参数
        if(fn.length === arguments.length) {
          return fn.apply(this, arguments);
        // 否则,判断old是否是函数,如果是,就调用old
        } else if(typeof old === "function") {
          return old.apply(this, arguments);
        }
      }
}
//使用方式
let obj = {
}
calcu(obj, 'handleNum', function(){
    return this
})
calcu(obj, 'handleNum', function(num){
    return num * 2
})
calcu(obj, 'handleNum', function(num1, num2){
    return num1 + num2
})
console.log(obj.handleNum());
console.log(obj.handleNum(2));
console.log(obj.handleNum(2,25));

image.png 上面代码中,利用calcu 方法给,obj添加了一个名叫handleNum的方法,这个方法就是calcu接收的第三个参数(一个匿名函数)。
calcu(要绑定方法的对象, 绑定的方法名称, 匿名函数)

callee

  1. callee 是arguments对象的一个属性, 它是一个指向 arguments 对象所在函数的指针。

直接看下面👇🏻

function fnArgu(num){console.dir(arguments)};
fnArgu(123,'qwe');

image.png

  1. 可以让函数逻辑与函数名解耦 递归函数,这个大家一定晓得吧😄,经常说,递归就是函数自己调用自己,那么咱就来看一个简单的demo
// 还是用经典的递归阶乘函数
function factorial(num) {
    if(num <= 1){
        return 1
    }else {
        return num * factorial(num - 1)
    }
}
let res = factorial(4);
console.log(res); //24

上面代码这么使用有问题吗?没有问题。 那么如果,把函数赋值给其他变量,然后把当前的这个factorial不小心设置为null,会怎样呢?

let fn = factorial;
factorial = null;
fn(4);//Uncaught TypeError: factorial is not a function

试一下就会发现报错了。 首先我们知道函数名factorial只是指向这个函数的指针(不知道的看这里 撬开Function的墙角
当把factorial设置为null后,切断了factorial与内存中函数的联系,内部再使用factorial去调用函数,这个时候就找不到了。
那么接下来就借助callee改一下这个递归函数

function factorial(num) {
    if(num <= 1){
        return 1
    }else {
        return num * arguments.callee(num - 1)
    }
}
let fn = factorial;
factorial = null;
console.log(fn(4)); // 24

完美解决,因为callee是指向当前arguments所在函数的指针,所以在函数内部是一直可以取到的。
桥豆麻袋,如果是在严格模式下呢?

image.png 看,又出现上面提到过的那个错误了吧,严格模式下callee不能用。(哈哈,白瞎了) 那怎么解决呢?可以使用函数表达式的方式定义,如下所示:

'use strict'
var factorial = function f(num) {
    if(num <= 1){
        return 1
    }else {
        return num * f(num - 1)
    }
}
console.log(factorial(4)) // 24
console.log(f) // Uncaught ReferenceError: f is not defined

上面代码中,定义了一个命名函数f,将其赋给了factorial变量;
这个时候的f只是变量factorial 指向的函数的内部标识符(外部是访问不到的哦😯)
OK,这样子严格模式下也可以了!

caller

  • ECMAScript 5 规范化了函数对象的属性:caller。
  • 这个属性中保存着调用当前函数的函数的引用, 如果是在全局作用域中调用当前函数,它的值为 null。

还是直接上代码吧

function fn(){
    console.log(fn)
    console.dir(fn)
    console.log(fn.caller)
}
fn()

image.png 上面例子很是显而易见了,应该没啥疑问,修改下demo,再来看下,非全局作用域下调用函数时:

function pfn(){
    fn()
}
function fn(){
    console.log(fn.caller)
}
pfn()

打印结果显示: image.png caller指向的是调用当前函数的函数;同样的在严格模式下会抛出错误。

今天就先到这,以上有描述不正确的,请多多指点~