深入理解函数

一、函数的默认值

默认值可以解决在参数缺省的时候,防止出错。一般情况下,设置默认值的参数,应该是尾参数,这样比较容易看出,到底省略了那些参数。

如果传入参数是undefined,将触发该参数等于默认值,如果是null,则没有这个效果。

function foo(x = 1, y = 2) {
   console.log(x, y);
}

foo(undefined, null);  // 1 null

【函数的length属性】:函数预期传入的参数个数。设有默认值的参数和rest参数都不计入length属性

 

指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数

(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2

而且如果设置了默认值的参数不是尾参数,那么后面的参数则不计入length

(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1

【作用域】

一旦参数设置了默认值,在函数进行初始化的时候,参数会形成一个单独的参数作用域(不同于函数内部的作用域,但仍可以通过作用域链访问到全局作用域),等到初始化结束后,这个作用域就会消失。

var x = 1;
function foo (x, y = x){
   console.log(y);
}
foo(2);  // 2

上面的例子中,在函数初始化的时候,参数形成单独的参数作用域,包含x和y,而其中的y指向x,所以最后打印结果是2。在看下面的例子:

let x = 1;
function f(y = x) {
  let x = 2;
  console.log(y);
}
// 如果全局没有声明x,则会报错
f() // 1

二、arguments对象——类数组对象

arguments[0]指向第一个参数,然后一次类推,具有length属性,和一个指向argument拥有者的指针。

arguments不是数组,要想使用数组的方法,就需要进行转换

var arr = Array.prototype.slice.call(arguments);

var arr = [].slice.call(arguments);

var arr = Array.from(arguments);

递归调用的例子:【使用callee指针指向代码区,跟函数名称没有关系】

function factorial(num){
  if(num <= 1){
    return 1;
   } else {
   return num * arguments.callee(num - 1);
  }
}
var s = factorial(5);
console.log(s);    // 120

三、rest参数——数组

用于获取多余的参数,但rest参数之后不能在有其他参数,否则会报错

function foo(a, ...rest){
  console.log(a);
  console.log(rest);
}
foo(1,2,2,3,4);
// 1
// [2,2,3,4]   

四、箭头函数

箭头函数有几个使用注意点。
(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。

(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数,当然也就不能用call()、apply()、bind()这些方法去改变this的指向。

五、apply,call,bind

apply和call均可以改变函数运行时的上下文,即this的指向。两者的主要区别在参数的传递方式上区别,第二个参数可以传arguments。

apply(对象,参数数组)

call(对象,参数1[,参数2,参数3......])

bind可以实现上下文的绑定,但它是新创建一个函数,然后把它的上下文绑定到bind()传入的参数上,然后将它返回。所以,bind后函数不会执行,而只是返回一个改变了上下文的函数副本,而call和apply是直接执行函数。而且多次调用bind是没有用的,只有第一次的调用会生效。

bind(对象)

总结:

六、尾调用

含义:某个函数的最后一步是调用另一个函数。

function f(x){
  return g(x);
}

【尾调用优化】

  函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。

【尾递归】

如果尾调用自身,就称为尾递归。递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。

function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}
factorial(5) // 120 

/***********改为尾调用***********/
function factorial(n, total) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}
factorial(5, 1) // 120

上面的例子改为尾调用之后,复杂度有原先的O(n)变为了O(1)。下面在看一下著名的斐波那契数列的例子

function Fibonacci (n) {
  if ( n <= 1 ) {return 1};
  return Fibonacci(n - 1) + Fibonacci(n - 2);
}
Fibonacci(10) // 89
Fibonacci(500) // 堆栈溢出

/************改为尾调用**********/
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
  if( n <= 1 ) {return ac2};
  return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}
Fibonacci2(100) // 573147844013817200000
Fibonacci2(1000) // 7.0330367711422765e+208

看了上面的例子,有没有明白如何改写函数递归调用呢?

  尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。比如上面的例子,阶乘函数 factorial 需要用到一个中间变量total,那就把这个中间变量改写成函数的参数。

七、函数柯里化

含义:将多参数的函数转化为单参数的形式

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值