返回的是没有指定默认值的参数个数,rest 参数也不计入length。
function throwIfMissing() {
throw new Error('Missing parameter');
}
function foo(mustBeProvided = throwIfMissing()) {
return mustBeProvided;
}
foo()
// Error: Missing parameter
参数mustBeProvided
的默认值等于throwIfMissing
函数的运行结果(注意函数名throwIfMissing
之后有一对圆括号),这表明参数的默认值不是在定义时执行,而是在运行时执行。如果参数已经赋值,默认值中的函数就不会运行。
ps:es6的函数最后一个参数运行有逗号’,’,在这之前是不允许有的,会报错。
// arguments变量的写法
function sortNumbers() {
//slice 方法可以用来将一个类数组(Array-like)对象/集合转换成一个新数组。
//你只需将该方法绑定到这个对象上
//可以简单的使用 [].slice.call(arguments) 来代替
return Array.prototype.slice.call(arguments).sort();
}
// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();
arguments对象不是数组,而是一个类似数组的对象。
rest 参数就是一个数组,数组的方法都可以使用。
这里会涉及一个常见面试题:arguments是数组吗?如果不是怎么变为数组?
除了上述使用[].slice.call(arguments)
,还可以:遍历arguments
然后将其push
到一个新数组中返回。
let foo = () => { a: 1 };
foo() // undefined
原始意图是返回一个对象{ a: 1 }
,但是由于引擎认为大括号是代码块,所以执行了一行语句a: 1
。这时,a
可以被解释为语句的标签,因此实际执行的语句是1
,然后函数就结束了,没有返回值。
function Timer() {
this.s1 = 0;
this.s2 = 0;
// 箭头函数:this绑定定义时所在的作用域(即Timer函数)
setInterval(() => this.s1++, 1000);
// 普通函数:指向运行时所在的作用域(即全局对象)
setInterval(function () {
this.s2++;
}, 1000);
}
var timer = new Timer();
setTimeout(() => console.log('s1: ', timer.s1), 3100);
setTimeout(() => console.log('s2: ', timer.s2), 3100);
// s1: 3
// s2: 0
箭头函数导致this总是指向函数定义生效时所在的定义域。
const cat = {
lives: 9,
jumps: () => {
this.lives--;
}
}
调用cat.jumps()
时,如果cat.jumps
是普通函数,该方法内部的this
指向cat
,可以实现lives--
;如果是箭头函数,this
指向全局对象,因此不会得到预期结果。这是因为对象不构成单独的作用域,导致jumps
箭头函数定义时的作用域就是全局作用域。
例子太复杂不便于理解,下面有个简单的函数嵌套
const plus1 = a => a + 1;
const mult2 = a => a * 2;
mult2(plus1(5)) //12
前一个函数的输出是后一个函数的输入,plus1
的返回结果是mult2
的输入,所以执行结果是12。
就是指某个函数的最后一步是调用另一个函数。如果调用完该函数还有别的其他操作,都不属于尾调用。比如
// 情况一
function f(x){
let y = g(x);
return y;
}
// 情况二
function f(x){
return g(x) + 1;
}
// 情况三
function f(x){
g(x);
}
//情况三等同于
function f(x){
g(x);
return undefined;
}
尾调优化
function f() {
let m = 1;
let n = 2;
return g(m + n);
}
f();
// 等同于
function f() {
return g(3);
}
f();
// 等同于
g(3);
只有不再用到外层函数的内部变量,才能进行“尾调用优化”。
function addOne(a){
var one = 1;
function inner(b){
return b + one;
}
return inner(a);
}
上面的函数不会进行尾调用优化,因为内层函数inner
用到了外层函数addOne
的内部变量one
。
目前只有 Safari 浏览器支持尾调用优化,Chrome 和 Firefox 都不支持
函数调用自身,称为递归。如果尾调用自身,就称为尾递归。
递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。
//计算n的阶乘
//最多需要保存n个调用记录,复杂度 O(n)
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
factorial(5) // 120
//尾递归
//只保留一个调用记录,复杂度 O(1) 。
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5, 1) // 120
如何改写递归为尾递归呢?
把所有用到的内部变量改写成函数的参数。可参考上面的例子,缺点就是不太直观,第一眼很难看出来为什么计算5的阶乘,需要传入两个参数5和1?
可以改写一下,便于理解。
方法一:
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 currying(fn, n) {
return function (m) {
return fn.call(this, m, n);
};
}
function tailFactorial(n, total) {
if (n === 1) return total;
return tailFactorial(n - 1, n * total);
}
const factorial = currying(tailFactorial, 1);
factorial(5) // 120
这里插个题外话,currying函数,可以参考张鑫旭:JS中的柯里化(currying)
方法二:ES6默认参数
function factorial(n, total = 1) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5) // 120
ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。所以我们可以自己实现尾调递归优化。尾递归之所以需要优化,原因是调用栈太多,造成溢出。那么只要减少调用栈,就不会溢出。怎么做可以减少调用栈呢?就是采用“循环”换掉“递归”。
function tco(f) {
var value;
var active = false;
var accumulated = [];
return function accumulator() {
accumulated.push(arguments);
if (!active) {
active = true;
while (accumulated.length) {
value = f.apply(this, accumulated.shift());
}
active = false;
return value;
}
};
}
var sum = tco(function(x, y) {
if (y > 0) {
return sum(x + 1, y - 1)
}
else {
return x
}
});
sum(1, 100000)
// 100001
上面代码中,
tco
函数是尾递归优化的实现,它的奥妙就在于状态变量active
。默认情况下,这个变量是不激活的。一旦进入尾递归优化的过程,这个变量就激活了。然后,每一轮递归sum
返回的都是undefined
,所以就避免了递归执行;而accumulated
数组存放每一轮sum
执行的参数,总是有值的,这就保证了accumulator
函数内部的while
循环总是会执行。这样就很巧妙地将“递归”改成了“循环”,而后一轮的参数会取代前一轮的参数,保证了调用栈只有一层