一、函数的默认值
默认值可以解决在参数缺省的时候,防止出错。一般情况下,设置默认值的参数,应该是尾参数,这样比较容易看出,到底省略了那些参数。
如果传入参数是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,那就把这个中间变量改写成函数的参数。
七、函数柯里化
含义:将多参数的函数转化为单参数的形式