愿你坚持不懈,努力进步,进阶成自己理想的人

—— 2017.09, 写给3年后的自己

《ECMAScript6入门》学习笔记之函数的扩展

一、参数默认值

ES6之前,我们不能指定函数参数的默认值,通常的做法是:

function foo(x) {
    x = x || 'Default Value';
    return x;
}

但是这种方法有一些问题,比如说:

foo(false);
foo('');

都会使得x的值是Default Value,而非传进去的值,这种情况下,我们更好的做法是:

function foo(x) {
    x = x === undefined ? 'Default Value' : x;
    return x;
}

而ES6中更方便的是,直接使用新增的参数默认值语法,如:

function foo(x, y = 'World') {
    console.log(x, y);
}
foo('Hello'); // Hello, world
foo('Hello', false); // Hello false

可以发现,使用这种方法是很直观的
使用默认参数语法,需要注意的有:

1、参数变量是默认声明的,函数体内不能再用let/const声明

如:

function foo(x = 5) {
    let x;
}
foo(); // Identifier 'x' has already been declared

2、使用默认参数后,不能有同名参数

如:

// 不会报错
function foo(x, x) {
    console.log(x, x);
}
foo(1, 2); // 2

// 会报错:Duplicate parameter name not allowed in this context
function foo(x, x=2) {
    console.log(x, x);
}

3、默认参数是运行时求值的

如:

let x = 1;
function foo(y = x + 1) {
    return y;
}
foo(); // 2
x = 2;
foo(); // 3

4、默认值的位置

一般推荐将参数默认值放于函数参数的后面,如果非尾部的参数指定了默认值,则实际上相当于不能省略,如:

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

不过,可以使用undefined来跳过,如:

foo(undefined, 2); // 1 2

其他写法则是错误的,如:

foo(, 2); // Uncaught SyntaxError: Unexpected token ,
foo(null, 2); // null 2

5、length属性

length属性实际表达的含义为:函数预期得到的参数个数。但是如果指定了默认值,那么length的计算中就不会包含默认参数了,如:

(function foo(x, y, z){}).length; // 3
(function foo(x, y, z = 1){}).length; // 2
(function foo(x, y = 1, z){}).length; // 1,y后面的非默认参数也不会纳入计算

6、作用域

函数的参数声明内是会形成一个作用域的,如:

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

(x, y = x + 1)形成了一个作用域,y可以在这个作用域内找到x,从而就不会继续往上找了。其他例子:

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

尤其需要注意的是:

let x = 1;
function foo(x = x) {
    console.log(x);
}
foo(); // 报错

这是因为,foo(x = x)中的x = x相当于:

let x = x;

这个时候,就会形成暂时性死区,所以此时x是无法得到的


二、可变参数

在ES6之前,可变参数是用Array-Like的内部变量arguments获取的,如:

function foo(x) {
    console.log(x, arguments);
}
foo(1, 2, 3); // 1 [1, 2, 3]

也就是说,arguments是包含所有参数的,如果我们想要只获得可变参数部分,那么需要:

function foo(x) {
    console.log(x, Array.prototype.slice.call(arguments, 1));
}
foo(1, 2, 3); // 1 [2, 3]

而ES6中引入了可变参数语法,用法如:

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

剩余的参数会被自动放进rest里面,形成一个数组。和arguments是Array-Like的不同的是,rest是一个真正的Array,所以我们可以直接使用Array上的方法
注意: rest参数是不会参与fn.length的计算的

三、严格模式

从ES5开始,函数内部可以使用'use strict'来设为严格模式,但是ES6中进行了规定:

只要函数参数使用了默认值、解构赋值或者扩展运算符,那么函数内部就不能显示设定'use strict';,否则就会报错。

这是因为:'use strict'是设置在函数体内部的,所以只有运行函数体的时候,才能知道是否要以严格模式执行,但是参数的执行却是在这之前的。这样子就可能会造成一个矛盾:不知道是否应该以严格模式来执行参数。


四、name属性

ES6中,正式将name属性获取函数名写入了标准,即:

(function someFn() {}).name; // 'someFn'

但是和ES5不同的是,ES5中对于函数表达式的name,执行结果为:

const foo = function(){};
foo.name; // ''
const bar = function bar(){};
bar.name; // 'bar'

而在ES6中,两种情况下都会返回函数名。此外,用Function构造函数声明的函数实例,会有:

(new Function).name; // anonymous

而使用bind绑定的函数,会得到:

function foo(){}
foo.bind({}).name; // 'bound foo'

即会带上bound前缀


五、箭头函数

ES6中引入了箭头函数特性,使用方法为:
1、使用(参数1, 参数2, ... 参数n) => { 函数体 }的形式来声明一个函数
2、当参数只有一个的时候,可以省略圆括号,如:x => { 函数体 },而空参数的情况下,使用()占位
3、当函数体只有一句,且可以作为返回值的时候,可以省略花括号,如:

const fn = x => x > 10;

相当于:

function fn(x) {
    return x > 10;
}

注意:如果此时返回值是一个对象,那么需要用圆括号包起来,即:

const p = (name, age) => ({name, age});

1、this指向

根据作用域链的知识,我们知道普通函数在调用时,会将运行时环境作为活动对象(AO)推入作用域链中,从而绑定了运行时的this,从而不会往上寻找得到定义时的this。但是箭头函数中,执行时的AO中是不会包含有this的,甚至也不会有argumentssupernew.target,所以箭头函数中 this能够绑定的是定义时的对象,而不是作用时的对象,如:

const obj = {
    foo() {
        setTimeout(() => console.log(this));
    }
}
obj.foo(); // 输出了obj对象

而如果是普通函数的话,那么就会是:11

const obj = {
    foo() {
        setTimeout(function() {
            console.log(this);
        });
    }
}
obj.foo(); // 输出了window对象

2、其他注意点

1)箭头函数不能当做构造函数使用,不能使用new命令。这是因为,箭头函数自身就没有this
2)箭头函数内不存在arguments对象,如果要使用可变参数,可以使用...rest语法
3)箭头函数不能作为generator函数,所以也不可以用yield命令
4)因为箭头函数自身没有this,所以用callbindapply作用于箭头函数是无效的


六、绑定this

由于箭头函数可以绑定定义时this的特性,所以大大减少了applycallbind的使用。但是箭头函数并不能适用于所有的场合,ES7中推出了绑定this的新的语法糖::,用法如:
1)context::fn,这种情况下,将绑定fn中的this为context,它相当于:fn.bind(context)
2)context::fn(...arguments),相当于fn.apply(context, arguments)
3)如果有一个对象obj有方法fn,我们想要让fn中的this指向obj对象,那么可以使用以下的写法:

::obj.fn
// 它相当于
obj.fn.bind(obj)


七、函数参数的尾逗号

ES8中,允许函数的最后一个参数后面有逗号,即:

fn(
    'foo',
    'bar',
);


八、尾调用的优化

当函数的最后一步是调用另一个函数的时候,这种调用就叫做尾调用。如:

// 尾调用的情况
function f(x) {
    return g(x);
}
// 这也是尾调用,因为都是f()的最后一步的操作
function f(x) {
    if (x > 0) {
        return m(x);
    }
    return n(x);
}

// 不是尾调用的情况
function f(x) {
    let y = g(x);   // 除了调用函数,还有赋值操作
    return y;
}
function f(x) {
    return g(x) + 1; // 除了调用,还有其他操作
}
function f(x) {
    g(x);           // 这相当于 g(x); return undefined; 所以也不是尾调用
}

由计算机的基本知识可以知道,函数的调用过程是用栈实现的,如对于以下的代码:

function a() {
    return b();
}
function b() {
    return c();
}

这个过程中,会形成一个调用栈,如:

[ c ]
[ b ]
[ a ]

这种情况下,当函数有很多层调用的时候,就会有栈溢出的问题。而尾调用优化,则是对满足特定条件的函数调用进行优化,如果一个函数是尾调用的话,如:

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

当我们调用f(1)的时候,我们会发现,f(1)的结果仅仅取决于g(1)的结果,而g(1)的结果又仅仅取决于k(1)的结果,所以我们用完f(1)之后,f(1)的调用帧就用不到了,也无需保留,这时候可以用后一个调用帧来替代,这就是尾调用优化,即:

[ k(1) ]   
[ g(1) ]                
[ f(1) ]  --> 可以优化为 --> [f(1)]  --> [g(1)] --> [k(1)]

从而减少了内存的占用,避免爆栈的问题。
在ES6中,尾调用优化只在严格模式下生效,这是因为在正常模式下,函数中会有两个变量:
1)fn.arguments 返回调用时函数的参数
2)fn.caller 返回调用当前函数的那个函数
所以这种情况下就不能进行尾调用优化了,因为尾调用优化会导致当前调用栈中的调用帧被覆盖,从而上面两个变量会失真