# 6. 函数的扩展

# 函数参数的默认值

指定函数参数默认值,如果传入的参数为undefined,才会使用默认值,否则不会默认赋值。

// ES6 之前,不能直接为函数的参数指定默认值,只能采用变通的方法。
function log(x, y) {
  y = y || 'World'; // 如没有x,默认值为 'world'
  console.log(x, y);
}

log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello World

// ES6默认值方式
function Point(x = 0, y = 0) {
  this.x = x;
  this.y = y;
}
const p = new Point();
p // { x: 0, y: 0 }

# rest参数

ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。

// 示例1:
function add(...values) {
  let sum = 0;
  for (var val of values) {
    sum += val;
  }
  return sum;
}
add(2, 5, 3) // 10

// 示例2: rest参数只能是最后一个参数,如果放到中间会报错
function push(array, ...items) {
  items.forEach(function(item) {
    array.push(item);
    console.log(item);
  });
}
var a = [];
push(a, 1, 2, 3)

# 严格模式

ES2016 做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。

# 函数name属性

// 示例1:
var f = function () {};
// ES5
f.name // ""
// ES6
f.name // "f"

// 示例2:
const bar = function baz() {};
// ES5
bar.name // "baz"
// ES6
bar.name // "baz"

# 箭头函数

ES6 允许使用“箭头”(=>)定义函数。可以简化回调函数,也可以不改变原来的this指向

# 基本用法

var f = v => v;
// 等同于
var f = function (v) {
  return v;
};

var f = () => 5;
// 等同于
var f = function () { return 5 };

// 箭头函数的一个用处是简化回调函数
// 正常函数写法
[1,2,3].map(function (x) {
  return x * x;}
);
// 箭头函数写法
[1,2,3].map(x => x * x);

# 注意事项

  • 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的变量
  • 不可以当做构造函数,也就是不可以用 new 命令
  • 不可以使用arguments参数,可以用rest参数代替
  • 不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);}

var id = 21;

foo.call({ id: 42 });
/**
 * 上面代码中,setTimeout的参数是一个箭头函数,这个箭头函数的定义生效是在foo函数生成时,而它的真正执行要等到 100 毫秒后。如果是普通函数,执行时this应该指向全局对象window,这时应该输出21。但是,箭头函数导致this总是指向函数定义生效时所在的对象(本例是{id: 42}),所以输出的是42。
 */

# 尾调用优化

尾调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。

// 函数的最后一次操作,是调用另一个函数
function f(x){
  return g(x);
}

问题:在ES5中,尾调用的实现与其他函数调用实现类似:创建一个新的栈帧(stack frame),将其推入调用栈来表示函数调用。也就是说,在循环调用中,每一个未用完的栈帧都会被保存在内存中,当调用栈变得过大时会造成程序问题,也就是我们常说的栈溢出(stack overflow)。

尾调用优化是ES6中在系统引擎优化上做的一个改进, 如果满足以下条件,尾调用不再创建新的栈帧,而是清除并重用当前栈帧。

  • 尾调用不访问当前栈帧的变量(也就是说函数不是一个闭包);
  • 在函数内部,尾调用是最后一条语句;
  • 尾调用的结果作为函数值返回;
// 不会进行尾调用优化例子
function addOne(a){
  var one = 1;
  function inner(b){
    return b + one;
  }
  return inner(a); 
}

// 尾调用优化例子
function f() {
  let m = 1;
  let n = 2;
  return g(m + n);
}
f();

# 尾递归

函数调用自身,称为递归。如果尾调用自身,就称为尾递归

递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。

# 优化案例1

// 非尾递归情况
function foo(n) {
    if (n === 1) return 1;
    return n * foo(n -1); // 这里最后一步操作是运算乘积,而不是直接调用函数
}

// 尾递归优化
function foo(n, total) {
    if (n === 1) return total;
    return foo(n -1, n * total)
}

// 继续优化,函数柯里化(currying),将多参数的函数转换成单参数的形式。这里也可以使用柯里化。
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

// 更简单的柯里化方法
function factorial(n, total = 1) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}
factorial(5) // 120

# 优化案例2(斐波拉切数列)

// 普通递归
function Fibonacci (n) {
  if ( n <= 1 ) {return 1};
  return Fibonacci(n - 1) + Fibonacci(n - 2);
}

// 尾递归优化, 不好理解,智商捉急.....
function Fibonacci (n, ac1 = 1, ac2 = 1) {
  if ( n <= 1 ) {return ac2};
  return Fibonacci(n-1, ac2, ac1 + ac2);
}
上次更新: 2020/10/28 18:32:03