之前不能为参数指定默认值,必须写判断
function log(x, y) {
if(typeof y === 'undefined') {
y = 'world';
}
console.log(x, y);
}
log('hello'); // hello world
log('hello, 'china'); // hello china
现在可以直接写默认值,直接写在参数的后面
function log (x = 'hello', y ='world') {
console.log(x, y);
}
log(); // hello world
log('1'); // 1 world
log(1, 2); // 1 2
好处:可以让人知道那些参数是可以省略的,利于代码优化
参数变量是默认声明的,所以不能在用let和const再次声明,否则会报错。
参数默认值不是传值,每次调用函数会重新计算默认值表达式的值,不是一成不变的,若表达式值有所变化,默认值也会随之改变
let x = 9;
function foo(y = x + 1) {
console.log(p);
}
foo(); // 10
x = 10;
foo(); // 11
函数的参数可以解构、解构也可以设置默认值。也叫双重默认值。
解题方法先考虑解构,再考虑参数的默认值
function foo({x, y = 5}) {
console.log(x, y);
}
foo({}); // undefined 5
例子:
function m1({x = 0, y = 0} = {}) {
return [x, y];
}
function m2({x, y} = {x = 0, y = 0}) {
return [x, y];
}
m1(); // [0, 0] 先看解构,右侧没有值,但左侧有解构默认值为x = 0, y = 0
m2(); // [0, 0] 先看解构,解构右侧没有值,左侧解构出来了,但没有默认值,应该为undefined,但参数有默认值,所以以默认值为准x = 0, y = 0
m1({x: 3, y: 8}); // [3, 8] 直接解构
m1({x: 3, y: 8}); // [3, 8] 直接解构
m1({x: 3}); // [3, 0] 直接解构 右侧x存在为3, y不存在但解构出来y有默认值
m2({x: 3}); // [3, undefined]
m1({}); // [0, 0] 直接解构
m2({}); // [0, 0]
如果参数有默认值,要写在参数的末尾。
因为要不是写尾部,则必须填写这个参数,否则将这个参数写成undefined
才能使这个参数的默认值有效。
function f(x = 1, y) {
return [x, y];
}
f(, 1); // 报错
如果某个参数设置了默认值,则length属性时返回是不包括该参数
function f1(x, y, z = 1) {
}
f1.length // 2 因为参数z设置了默认值
因为函数length属性含义是该函数预期要传入的参数个数,因为某个参数有了默认值,则不用传也没事,所以就不包括。
只有在参数有默认值的情况下存在,如果参数一旦有了默认值,函数进行声明初始化时,参数就就会形成一个作用域,等到初始化结果后,这个作用域才会消失。
let x = 1;
function f(y = x) {
let x = 2;
console.log(y);
}
f(); // 1
在调用f时,参数y = x 会形成一个作用域,在这个作用域中x没有声明,就会往上找,找到全局x,赋值给参数y。如果全局也没有x,就会报错。
也存在暂时性死区,不能没有声明前就使用变量。
利用参数默认值可以指定某个参数不能省略,如果省略就报错。
function throwIfMissing() {
throw new Error('Missing parameter');
}
function foo(mustBeProvided = throwIfMissing()) {
return mustBeProvided;
}
foo(); // 报错
上面代码中,如果调用foo函数时,不传参数就直接会走默认值,而默认值是一个抛异常的函数。
形式为...变量名
,用来获取函数的多余参数。这样就不必使用arguments对象,rest参数搭配的变量是一个数组,该变量将多余的参数放入其中。
function add(...values) {
console.log(values);
}
add(1, 2, 3); // [1, 2, 3]
rest参数必须写在最后,否则就会报错。
函数中的length属性不会计算rest参数
(function(a) {}).length // 1
(function(...a){}).length // 0
如果函数参数使用了默认值,解构赋值或者扩展运算符,那么函数内部就不能显式设定严格模式,否则就会报错。
// 报错
function foo(a, b = a) {
'use strict'
// code
}
原因是因为,函数执行时,先执行参数,在执行函数体,而是否使用严格模式是在函数体内才知道。
例如:
function doSomething(value = 070) {
'use strict'
}
在严格模式下八进制不能使用0
表示,但是一开始的时候可以执行成功,后来又报错。
解决方法
设置成全局的严格模式
'use strict'
function foo (x =1) {}
将函数包在一个无参的立即执行函数内
const doSomething = (function() {
'use strict'
return function(value = 42) {
return value;
}
}())
返回该函数的函数名。
在ES5中一个匿名函数赋值给变量时,name返回空字符串,而在ES6中会返回实际的函数名。
Function构造函数返回的函数实例,name属性的值为anonymous
(new Function).name // anonymous
bind返回的函数,name属性值会加上bound前缀
function foo() {}
foo.bind({}).name // bound foo
(function(){}).bind({}).name // bound
使用一个箭头来定义函数,当参数为多个时,参数部分用括号包裹。
和变量解构结合使用
注意点:
由于箭头函数没有自己的this,所以也不能使用call()、apply()、bind()这些方法去改变this的指向。
箭头函数还可以嵌套使用
使用双冒号(::
),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象作为上下文环境(this对象)绑定到右边的函数上。
foo::bar
// 等同于
bar.bind(foo)
如果双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上。
var method = obj::obj.foo;
// 等同于
var method = ::obj.foo;
双冒号运算符返回的是原对象,所以可以采用链式写法。
是函数式编程的一个重要概念,就是指某个函数的最后一步是调用另一个函数。
注意点:
一定是最后一步,一定是调用另一个函数
不能是含有函数的表达式,或者返回的是上一步函数调用的结果
需要知道的是:在函数调用时会在内存中形成一个调用记录
,又称调用帧
,用来保存位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方还会形成一个B的调用帧,等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用C,那就还有一个C的调用帧,依次类推。所有的调用帧就形成一个调用栈
。
而尾调是函数的最后一步,不需要保留外层函数的调用帧,因为调用的位置,内部信息都不会再用到了,直接用内层函数的调用帧取代外层函数的即可。
尾调用优化就是只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时调用帧只有一项,就会大大节省内存,这也是尾调用优化的意义。
注意:
只有不再用到外层函数的内部变量,内存函数的调用帧才会取代外层函数的调用帧,否则就无法进行尾调用优化
。
函数调用自身就是递归,如果尾调用自身就称为尾递归。
递归非常耗费内存,因为需要同时保存成百上千个调用帧,很容易造成栈溢出
,但对于尾递归来说,只存在一个调用帧,因此不会发生栈溢出
。
在ES6中只要尾递归就一定不会造成栈溢出
。
尾递归的实现往往需要改写递归函数,来确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。
如何做到
function tailFactorial(n, total) {
if (n === 1) return total;
return tailFactorial(n - 1, n * total);
}
function factorial(n) {
return tailFactorial(n, 1);
}
factorial(5); // 120
function factorial(n, total = 1) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5); // 120
ES6的尾调模式只在严格模式下开启。