「这是我参与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)
通过上面👆🏻可以发现:
除了fn3(箭头函数)之外,其他方式定义的函数都有arguments,caller, length, name, prototype;
展开fn3的arguments与caller看一下:
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();
上面代码中,定义了一个函数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
通过上面可以看到,严格模式下,修改
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));
上面代码中,利用calcu 方法给,obj添加了一个名叫handleNum的方法,这个方法就是calcu接收的第三个参数(一个匿名函数)。
calcu(要绑定方法的对象, 绑定的方法名称, 匿名函数)
callee
- callee 是arguments对象的一个属性, 它是一个指向 arguments 对象所在函数的指针。
直接看下面👇🏻
function fnArgu(num){console.dir(arguments)};
fnArgu(123,'qwe');
- 可以让函数逻辑与函数名解耦 递归函数,这个大家一定晓得吧😄,经常说,递归就是函数自己调用自己,那么咱就来看一个简单的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所在函数的指针,所以在函数内部是一直可以取到的。
桥豆麻袋,如果是在严格模式下呢?
看,又出现上面提到过的那个错误了吧,严格模式下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()
上面例子很是显而易见了,应该没啥疑问,修改下demo,再来看下,非全局作用域下调用函数时:
function pfn(){
fn()
}
function fn(){
console.log(fn.caller)
}
pfn()
打印结果显示:
caller指向的是调用当前函数的函数;同样的在严格模式下会抛出错误。
今天就先到这,以上有描述不正确的,请多多指点~