ES6函数的扩展部分笔记

函数的length属性

返回的是没有指定默认值的参数个数,rest 参数也不计入length。

函数参数默认值的应用

function throwIfMissing() {
  throw new Error('Missing parameter');
}

function foo(mustBeProvided = throwIfMissing()) {
  return mustBeProvided;
}

foo()
// Error: Missing parameter

参数mustBeProvided的默认值等于throwIfMissing函数的运行结果(注意函数名throwIfMissing之后有一对圆括号),这表明参数的默认值不是在定义时执行,而是在运行时执行。如果参数已经赋值,默认值中的函数就不会运行。

ps:es6的函数最后一个参数运行有逗号’,’,在这之前是不允许有的,会报错。

rest参数和arguments

// 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,然后函数就结束了,没有返回值。

关于箭头函数的this
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。

尾调用(Tail Call)

就是指某个函数的最后一步是调用另一个函数。如果调用完该函数还有别的其他操作,都不属于尾调用。比如

// 情况一
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循环总是会执行。这样就很巧妙地将“递归”改成了“循环”,而后一轮的参数会取代前一轮的参数,保证了调用栈只有一层

发布了20 篇原创文章 · 获赞 38 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/jiamiao66/article/details/99863623