一、参数默认值
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
的,甚至也不会有arguments
、super
、new.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
,所以用call
、bind
、apply
作用于箭头函数是无效的
六、绑定this
由于箭头函数可以绑定定义时this
的特性,所以大大减少了apply
、call
、bind
的使用。但是箭头函数并不能适用于所有的场合,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
返回调用当前函数的那个函数
所以这种情况下就不能进行尾调用优化了,因为尾调用优化会导致当前调用栈中的调用帧被覆盖,从而上面两个变量会失真