ES6 函数的扩展

注:这是一篇学习笔记,记录自己在ES6学习过程中按照自己的思路觉得应该记录的一些要点,方便以后查看和复习。参考:

阮一峰ES6的书籍中函数的扩展
我的ES6入门学习规划

参数及参数默认值

  • 参数不能在函数体内用let、const重新声明,但是可以用var、function重新声明

    1
    2
    3
    4
    5
    6
    function foo(x) {
    let x = 1;
    }//error
    function bar(x) {
    var x = 1;
    }//success
  • 当某参数有默认值时,函数参数列表内不能有同名参数;当没有参数有默认值时,参数列表可以有同名参数

    1
    2
    3
    4
    function foo(x, x, y=1) {
    }//error
    function bar(x,x,y) {
    }//success
  • 默认值可以简化函数调用时参数传递,基本形式:

    1
    2
    3
    4
    function foo(x, y=1, z=2) {
    console.log(x+y+z);
    }
    foo(-3);// 0
  • 默认值如果是一个函数,它是惰性求值的,每次函数调用的时候才会计算

  • 默认值可以与解构赋值一起使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //数组解构
    function foo([x, y]) {
    console.log(x + ',' + y + '!');
    }
    foo(['Hi','Andy']);//Hi,Andy!

    function bar({title, author}) {
    console.log('《'+title+'》\'author is ' + author + '.');
    }
    bar({title: 'ES6', author: 'ruanyifeng'});//《ES6》'author is ruanyifeng.
  • 与解构一起使用时,参数也能使用默认值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //数组解构
    function foo([x, y] = ['Hi','Andy']) {
    console.log(x + ',' + y + '!');
    }
    foo();//Hi,Andy!

    function bar({title, author} = {title: 'ES6', author: 'ruanyifeng'}) {
    console.log('《'+title+'》\'author is ' + author + '.');
    }
    bar();//《ES6》'author is ruanyifeng.
  • 注意解构本身也能使用默认值,所以参数默认值复杂的情况下,可能同时有参数本身默认值和解构的默认值,注意区分

  • 参数默认值应该是尾参数,否则就失去了默认值带来的作用
  • 参数可以传递undefined,显示触发默认值

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

    foo(undefined, null)
    // 5 null
  • 利用参数默认值,可以实现控制某些参数不可省略的函数调用控制。 简而言之,就是把不可省略的参数默认值设为一个函数,在这个函数内部抛出错误

参数作用域

当函数有参数带默认值时,参数部分会形成一个单独的作用域;当函数没有参数带默认值时,参数部分不会形成这个作用域。所以当参数有默认值时,要注意暂时性死区的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
function bar(x = y, y = 2) {
return [x, y];
}

bar(); // 报错
bar(1); // 不报错

function foo(x = x) {
return x;
}

foo(); // 报错
foo(1); // 不报错

前面的一个要点:

1
当某参数有默认值时,函数参数列表内不能有同名参数;当没有参数有默认值时,参数列表可以有同名参数

应该也是因为默认值引发的作用域的原因,因为有默认值的函数,在调用的时候,一定会先执行参数部分的作用域,而参数的作用域如果包含同名变量,绝对会报错(好比let不允许声明重复变量),所以“带默认值的函数,如果包含同名变量”,在声明的时候就会报错,还不用等到调用的时候。 前面2个暂时性死区的例子中包含的函数,在声明的时候是没有报错的,只有在一些特殊调用的情况下,才会出错。

参数默认值所带来的作用域,在函数执行时才会有效,它可以访问函数被调用的外层作用域,但是无法访问函数体内的作用域。

1
2
3
4
5
6
7
8
9
var x = 1;
function foo(x, y = function() { x = 2; /*这里面访问到的是前面的x参数*/}) {
var x = 3; // 这如果是用let声明的话,可是会报错的
y();
console.log(x);// 这个x是foo函数体第一行代码声明的x
}

foo() // 3
x // 1 这是全局变量x

rest参数

1
2
3
4
5
6
7
8
9
10
11
function add(...values) {
let sum = 0;

for (var val of values) {
sum += val;
}

return sum;
}

add(2, 5, 3) // 10
  • 可以代替arguments变量
  • 必须是尾参数

length参数

有了参数默认值以及后面要记录的rest参数,函数的length属性会失真。

  • 不计算有默认值的参数
  • 不计算rest参数
  • 不计算有默认值的参数,之后的其它没默认值的参数
    1
    console.log(function foo(x, y=1, z, u){}.length); // 1

严格模式

只要函数参数有默认值、解构赋值或rest参数,函数内部就不能用’use strict’启用严格模式。

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
// 报错
function doSomething(a, b = a) {
'use strict';
// code
}

// 报错
const doSomething = function ({a, b}) {
'use strict';
// code
};

// 报错
const doSomething = (...a) => {
'use strict';
// code
};

const obj = {
// 报错
doSomething({a, b}) {
'use strict';
// code
}
};

函数的name属性

  • 【1】 函数的name属性,返回函数的名称。

    1
    2
    function foo() {}
    foo.name // "foo"
  • 【1】 匿名函数赋给变量,变量的name属性为变量名

  • 【1】 具名函数赋给变量,变量的name属性为函数的名字
  • 【1】 Function构造的函数实例,name属性为anonymous
  • 【1】 bind返回的函数,name属性前面会加上bound前缀
  • 【2】 ES6的Class声明的类的name属性返回紧跟在class关键字后面的类名
  • 【2】 匿名class表达式赋给变量,变量的name属性为变量名

    1
    2
    let a = class {};
    a.name // a
  • 【2】 具名class表达式赋给变量,变量的name属性为class关键字后面的类名

    1
    2
    let b = class B{};
    b.name // B
  • 【3】 对象的属性如果指向函数或者class表达式,也基本上满足【1】【2】中记录的规律

  • 【3】 对象的属性如果是setter或者getter,那么name属性,需要通过拿到属性描述符才能看到

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const obj = {
    get foo() {},
    set foo(x) {}
    };

    const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo');

    descriptor.get.name // "get foo"
    descriptor.set.name // "set foo"
  • 【3】 对象的属性如果是symbol类型,那么name属性,会返回symbol值的描述

    1
    2
    3
    4
    5
    6
    7
    8
    const key1 = Symbol('description');
    const key2 = Symbol();
    let obj = {
    [key1]() {},
    [key2]() {},
    };
    obj[key1].name // "[description]"
    obj[key2].name // ""

箭头函数

  • 如果没有参数,或多个参数,需要用圆括号代表参数部分
  • 如果代码块多于一条语句,需要用大括号括起来,根据需要用return语句表示返回值
  • 如果箭头函数仅仅需要一条语句返回一个对象,需要用括号将对象括起来
  • 支持嵌套
  • 箭头函数内的this始终指向定义时所在的对象
  • 不可以作为构造函数
  • 不可以用arguments、super、yield、new.target
  • 不能用call()、apply()、bind()这些方法去改变this的指向

尾调用与尾递归

某个函数的最后一步是return调用另一个函数。

1
2
3
function f(x){
return g(x);
}

尾调用优化:
当某个函数最后一步return调用另一个函数时,如果能做到用内层函数调用帧替代外层函数的调用帧,就会节省函数调用时的内存消耗。

注意:
只有不再用到外层函数的内部变量,内层函数的调用帧才能取代外层函数的调用帧,否则无法进行尾调用优化。

尾递归:
如果某个递归函数,最后是尾调用自身,就称为尾递归。

作用:
防止递归层级过多,出现调用栈溢出。

实现方式:
就是把递归函数所有用到的内部变量,改写成函数参数。

开启:
ES6的尾调用优化只有在严格模式下才开启;目前浏览器支持不足,测试过chrome并未开启。可以通过在浏览器中调试以下代码:

1
2
3
4
5
6
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}

factorial(5, 1) // 120

通过debug,看看每一次递归的时候,函数的调用栈是否存在替代的情况。

尾逗号

ES2017 允许函数的最后一个参数有尾逗号。