函数参数的默认值
function foo(x = 5) {
let x = 1 // error
const x = 2 // error
}
函数变量是默认声明的,在函数体内不能使用let或const再次声明,否则或报错。
解构与默认值结合使用
function foo ({x, y = 5}) {
console.log(x, y)
}
foo({}); // undefined, 5
foo({x: 1}); // 1, 5
foo({x: 1, y: 2}); // 1, 2
foo(); //报错
上面的代码使用了对象的解构赋值默认值,而没有使用函数参数的默认值。只有当函数foo的参数是一个对象时,变量x和y才会通过解构赋值而生成,否则就会报错。
双重默认值
function foo(url, { method = 'GET'} = {}) {
console.log(method)
};
foo('http://www.baidu.com')
// "GET"
当函数没有传入第二个参数时,函数默认值生效。之后对象的解构赋值生效,变量method取得默认值。
// 函数参数的默认值是空对象,但设置了对象解构赋值的默认值
function m1({x = 0, y = 0} = {}) {
return [x, y]
};
// 函数参数的默认值是一个有具体属性的对象,但没有设置对象解构赋值的默认值
function m2({x, y} = {x: 0, y: 0}) {
return [x, y]
};
函数默认值的位置应该在函数的尾部,否则参数无法省略。
函数的length属性,返回没有指定默认值的参数个数。(如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数,总之就是很鸡肋)
作用域
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域。等到初始化结束,这个作用域会消失。这种语法是在不设置函数参数默认值时是不会出现的。
var x = 1;
function f(x, y = x) {
console.log(y)
}
f(2); // 2
如果在作用域里,变量并未定义,则会指向外层的全局变量,函数体内部的局部变量影响不到默认变量。
如果参数的默认值是一个函数,该函数的作用域也遵守这个规则。
let x = 1;
function f(y = x) {
let x = 2;
console.log(y)
}
f() // 1
如果此时,全局变量x不存在,就会报错。
var x = 1;
function foo(x, y = function () { x = 2 })
var x = 3;
y();
console.log(x)
}
foo(); // 3
x // 1
// 函数foo的参数形成一个单独的作用域,变量y的匿名函数内部的变量x指向同于作用域下的第一个参数x,而函数内部又声明了一个变量x,console的变量为函数内部声明的变量,调用匿名函数改变参数x的值,全局变量x从未改变
var x = 1;
function foo(x, y = function() { x = 2 }) {
x = 3
y()
console.log(x)
}
foo() // 2
x // 1
// 函数内部变量x指向第一个参数x,与匿名函数内部x是一致的。
rest参数
rest参数是一个数组,该变量将多余的参数放入其中。
function add(...value) {
let sun = 0
for(var val of value) {
sun += val
}
reutrn sun
}
add(2, 5, 3) // 10
rest参数之后不能有其他参数,否则会报错。
函数的length属性不包括rest参数。
只要函数参数使用了默认值、解构赋值或者扩展运算符,则不可在函数内部显示设定为严格模式,否则就会报错。
name属性
函数的name属性返回该函数的函数名。
箭头函数
ES6 允许使用箭头( => )定义函数
var f = v => v;
// 等同于
var f = function (v) {
return v
}
// 如果箭头函数不需要参数或需要多个参数,就使用圆括号代表参数部分
() => 5;
(num1, num2) => num1 + num2;
// 如果箭头函数的代码块部分多与=于一条语句,则需要使用大括号将其扩起来,并使用return语句返回
// 由于大括号被解释为代码块,所以如果箭头函数要返回对象,需在外面加上括号
箭头函数可以与变量解构结合使用。
注意事项
- 函数体内的this对象就是定义时所在的对象,而不是使用时所在的对象
- 不可以当作构造函数(不可以使用new命令,否则会报错)
- 不可以使用arguments对象,该对象在函数体内不存在
- 不可以使用yield命令(箭头函数不可以用作generator函数)
箭头函数根本没有自己的this,它的this指向外层代码块的this
箭头函数无法使用call(),apply(),bind()这些方法起改变this的指向
箭头函数内部还可以使用箭头函数。
绑定this
函数绑定运算符是并排的双冒号(::),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象作为上下文环境(即this对象)绑定到右边的函数上。
foo :: bar;
// 等同于
bar.bind(foo)
如果双冒号的左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上。
var method = obj::obj.foo;
// 等同于
var method = ::obj.foo
尾调用优化
某个函数的最后一步是调用另一个函数
function f(x) {
return g(x)
}
函数调用会在内存中形成一个“调用记录”(调用帧),保存调用的位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方还会形成一个B的调用帧。等B运行结束,将结果返回给A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧就形成一个调用栈。
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,直接用内层函数的调用帧取代外层函数的调用帧即可。
只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则无法进行“尾调用优化”
尾递归
函数调用自身称为递归,如果尾调用自身称为尾递归。
// 递归
function a(n) {
if(n === 1) return 1
return n * a(n - 1)
}
// 尾递归
function a(n, m) {
if( n === 1) return m
return a(n-1, n*m)
}
尾递归的改写
把所有要用到的内部变量改写成参数
严格模式
ES6 的尾调用优化只在严格模式下开启,正常模式下是无效的。
因为正常模式下函数内部有两个变量,可以跟踪函数的调用栈:
- func.arguments:返回调用时函数的参数
- func.caller:返回调用当前函数的函数
尾调用优化时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。
尾递归优化的实现
那么普通模式中就无法使用尾递归调用了吗?当然不是,我们可以使用一种曲线救国的政策 —— 蹦床函数
将递归执行转为循环执行
function a(f) {
while ( f && f instanceof Function) {
f = f()
}
return f
}