函数的扩展

  • 函数执行是先执行函数参数,再执行函数体
  • 如果函数参数使用了解构赋值、设置默认值、扩展运算符则不可以在函数体内显式设定为严格模式,否则会报错
  • “调用记录”,又称“调用帧”。用于保存调用位置和内部变量等信息。如果A调用了B,那么在A的调用帧上形成了B的调用帧,如果在B内部海调用了C,那么在B的上方又形成了C的调用帧,等到C的调用结束以后,将结果返回给B,C的调用帧才会消失。A、B、C的调用帧便形成了调用栈
  • 函数拥有name属性,可以获取函数名。

默认值

  • 普通设置默认值
  • 解构赋值设置默认值:其中给解构赋值的属性设置默认值给函数参数设置默认值之间的区别。
1
2
3
4
5
6
function m1({x = 0, y =0} = {}) {
return [x,y];
}
function m2({x,y} = {x:0,y:0}) {
return [x,y];
}

length属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
指定默认值以后,函数的length属性、函数参数的length将会失真。即如下:
let b = (function (a = 5) { // 注意这里
console.log(arguments.length) // 注意这里是不会调用的,可能是以为圆括号的问题
}).length
console.log(b)
// 0

function test (a = 5) { // 设置了默认值,但是如果在调用的时候采用test(10)给她传参是话,则结果是和下面test1一样,结果是因为下面是
console.log(arguments)
console.log(arguments.length)
}
test()
// [Arguments] {}
// 0

function test1 (a) { // 这没有设置默认值
console.log(arguments)
console.log(arguments.length)
}
test(10)
// [Arguments] {'0':10}
// 1

函数的length属性是:该函数预期传入的参数个数。所以length的大小是看调用该函数时传入的参数个数,而不是单纯看函数自己参数默认值的设置。

rest参数

1
2
3
4
5
6
7
8
function add(...values) { // 采用...values接收所有参数
let sum = 0;
for(var val of values) {
sum += val;
}
return sum;
}
add(2,5,3) // 10
1
2
3
一、...values与arguments的区别:
1、...values是真正的数组,可以使用数组的所有方法
2、aruments是一个对象数组
  • 注意点
    • rest参数之后不能有其他参数

箭头函数

  • 箭头函数体内的this对象,是函数定义时所在的对象,而不是使用时所在的对象。这是因为箭头函数没有this,不是因为它可以绑定this,所以箭头函数的this便是它定义的外部环境的this,因为没有this,自然不可以调用call、apply、bind来改变this的指向问题。

  • this外,argumentssupernew.target在箭头函数中也是不存在的,而且会和this一样,指向到定义时的外部环境一样。

  • 不可作构造函数,不可以使用new命令

  • 不可使用arguments对象,该对象在函数体内不存在,可以用rest参数代替

  • 不可使用yield命令,箭头函数不能用作Generator函数

  • 关于this对象的举例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    var s2 = 8; // 这里定义没个鸟用,因为调用Timer的是setTimeout,而不是全局
    function Timer(){
    this.s1 = 0;
    this.s2 = 0;

    setInterval(() => {
    console.log(this.s1) // 0,也就是说这里可以取到值,因为this = timer
    this.s1++
    console.log(this.s1) // 1
    }, 1000);

    setInterval(function(){
    console.log(this)
    console.log(this.s2) // undefined,取不到,以为全局下没有这个变量var s2 = 8;
    function Timer(){
    this.s1 = 0;
    this.s2 = 0;

    setInterval(() => {
    console.log(this.s1) // 0,也就是说这里可以取到值,因为this = timer
    this.s1++
    console.log(this.s1) // 1
    }, 1000);

    setInterval(function(){
    console.log(this) // 为setTimeout,因为是在setTimeout中调用了这个函数
    console.log(this.s2) // undefined,取不到,以为全局下没有这个变量
    this.s2++
    console.log(this.s2) // 后面执行为NaN
    },1000);
    }

    var timer = new Timer();

    setTimeout(() => console.log('s1:', timer.s1), 1000)
    setTimeout(() => console.log('s2:', timer.s2), 1000)
    this.s2++
    console.log(this.s2) // 后面执行为NaN
    },1000);
    }

    var timer = new Timer();

    setTimeout(() => console.log('s1:', timer.s1), 1000)
    setTimeout(() => console.log('s2:', timer.s2), 1000)
  • 多层this的嵌套:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function foo(){
    return () => {
    return () => {
    return () => {
    console.log('id:',this.id);
    }
    }
    }
    }

    var f = foo.call({id:1})
    var t1 = f.call({id:2})()(); // id:1
    var t2 = f().call({id:3})(); // id:1
    var t3 = f()().call({id:4}); // id:1
    • 题外话,这里的call()用法,参考《call()》一文

尾调用

1
2
3
4
含义:函数的最后一步是调用另一个函数。不一定是写在函数尾部,一定是最后一步操作,,实际上很大程度上,需要时return 某函数,因为return真正的结束了这个函数的操作。
function f(x) {
return g(x);
}

尾调用主要是为了实现尾调用优化,实际上是内存优化的一种方式。当执行到最后一条语句,也就是执行另一个函数的时候,因为这时候外层函数已经没有用了,不需要再去保存调用位置和变量信息,所以可以释放其调用帧来节省内存,官方语言可以说是该函数的调用帧取代了外层函数的调用帧。

所以一定要是最后一条执行语句的时候调用函数,否则就不会释放外层函数的调用帧,这样尾调用优化就不能实现,尾调用也失去了意义。

尾递归

这个就非常有意义了!不过ES6尾调用优化只在严格模式下才会开启,正常模式下是无效的。所以代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
function test(a,b){
'use strict'; // 如果不加这个,则输出; 如果加上,则报错
console.log(test.caller); // [Function]
console.log(test.arguments); // {'0':1,'1':2}
}
test(1,2);
/*
以上结果是因为在正常模式下,函数内部有两个变量可以跟踪函数的调用栈。
- func.arguments: 返回调用时函数的参数。
- func.caller: 返回调用当前函数的那个函数。
当尾调用优化发生的时候,函数的调用栈会改写,因此上面两个变量会失真,而严格模式下,该变量会被禁用,所以尾调用模式仅在严格模式下生效。
*/
  • 函数调用自身称为递归,尾调用自身则是尾递归。因为普通递归需要保存很多调用帧,非常耗费内存,但是尾递归则只会保存一个调用帧,大大的节省了内存,其复杂度就是O(1)。

  • ES6中使用尾递归,就不会发生栈溢出(或者层层递归造成的超时),相对节省内存

蹦床函数

将递归改变为循环执行,并且传递的参数是一个函数,并立即执行它。。。。呃。。

1
2
3
4
5
6
function foo(){
while(f && typeof f == Function) {
f = foo()
return f;
}
}