ES6语法总结-函数的扩展

Posted DA_GAMER

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ES6语法总结-函数的扩展相关的知识,希望对你有一定的参考价值。

1、函数参数的默认值

基本用法

function log(x, y = \'World\') {
  console.log(x, y);
}

log(\'Hello\') // Hello World log(\'Hello\', \'China\') // Hello China log(\'Hello\', \'\') // Hello
  • 使用参数默认值时,函数不能有同名参数
// 不报错 
function foo(x, x, y) {
 // ... 
}

// 报错
function foo(x, x, y = 1) {
 // ... 
} 
// SyntaxError: Duplicate parameter name not allowed in this context
  • 参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的
let x = 99;
function foo(p = x + 1) {
  console.log(p);
}

foo() // 100 
x = 100;
foo() // 101

与解构赋值默认值结合使用

参数默认值的位置

  • 定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的
// 例一 
function f(x = 1, y) {
  return [x, y];
}

f() // [1, undefined] 
f(2) // [2, undefined] 
f(, 1) // 报错 
f(undefined, 1) // [1, 1]  

// 例二 
function f(x, y = 5, z) {
  return [x, y, z];
}

f() // [undefined, 5, undefined] 
f(1) // [1, 5, undefined] 
f(1, ,2) // 报错 
f(1, undefined, 2) // [1, 5, 2]

函数的length属性

  • 函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真
  • 如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了
(function (a = 0, b, c) {}).length // 0 
(function (a, b = 1, c) {}).length // 1

作用域

  • 一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的
var x = 1;

function f(x, y = x) {
  console.log(y);
}

f(2) // 2
  • 下面代码中,函数f调用时,参数y = x形成一个单独的作用域。这个作用域里面,变量x本身没有定义,所以指向外层的全局变量x。函数调用时,函数体内部的局部变量x影响不到默认值变量x。如果此时,全局变量x不存在,就会报错
let x = 1;

function f(y = x) {
  let x = 2;
  console.log(y);
}

f() // 1
  • 下面这样写,也会报错。参数x = x形成一个单独作用域。实际执行的是let x = x,由于暂时性死区的原因,这行代码会报错”x 未定义“
var x = 1;

function foo(x = x) {
 // ... }

foo() // ReferenceError: x is not defined
  • 如果参数的默认值是一个函数,该函数的作用域也遵守这个规则。函数bar的参数func的默认值是一个匿名函数,返回值为变量foo。函数参数形成的单独作用域里面,并没有定义变量foo,所以foo指向外层的全局变量foo,因此输出outer。如果外层没有什么foo就会报错
let foo = \'outer\';

function bar(func = () => foo) {
  let foo = \'inner\';
  console.log(func());
}

bar(); // outer

应用

  • 利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误
function throwIfMissing() {
  throw new Error(\'Missing parameter\');
}

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

foo() // Error: Missing parameter
  • 可以将参数默认值设为undefined,表明这个参数是可以省略的

2、rest参数

  • ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中
  • 注意,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错
  • 函数的length属性,不包括 rest 参数

3、严格模式

  • 从 ES5 开始,函数内部可以设定为严格模式
function doSomething(a, b) {
  \'use strict\';
 // code 
}
  • ES2016 做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错

4、name属性

  • 函数的name属性,返回该函数的函数名
  • ES6 对这个属性的行为做出了一些修改。如果将一个匿名函数赋值给一个变量,ES5 的name属性,会返回空字符串,而 ES6 的name属性会返回实际的函数名
var f = function () {};
// ES5 f.name // ""  
// ES6 f.name // "f"
  • 如果将一个具名函数赋值给一个变量,则 ES5 和 ES6 的name属性都返回这个具名函数原本的名字
const bar = function baz() {};
// ES5 bar.name // "baz"  
// ES6 bar.name // "baz"
  • Function构造函数返回的函数实例,name属性的值为anonymous
  • bind返回的函数,name属性值会加上bound前缀
function foo() {};
foo.bind({}).name // "bound foo" 
(function(){}).bind({}).name // "bound "

5、箭头函数

  • 如果箭头函数只有一行语句,且不需要返回值,可以采用下面的写法,就不用写大括号了
let fn = () => void doesNotReturn();

使用注意点

1. 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对
2. 不可以当作构造函数,也就是说,不可以使用new命令
3. 不可以使用arguments对象
4. 不可以使用yield命令

不适合场合

  • 定义对象的方法,且该方法内部包括this。cat.jumps()方法是一个箭头函数,这是错误的。调用cat.jumps()时,如果是普通函数,该方法内部的this指向cat;如果写成上面那样的箭头函数,使得this指向全局对象,因此不会得到预期结果。这是因为对象不构成单独的作用域,导致jumps箭头函数定义时的作用域就是全局作用域
const cat = {
  lives: 9,
  jumps: () => {
    this.lives--;
  }
}
  • 需要动态this的时候,也不应使用箭头函数
var button = document.getElementById(\'press\');
button.addEventListener(\'click\', () => {
  this.classList.toggle(\'on\');
});

上面代码运行时,点击按钮会报错,因为button的监听函数是一个箭头函数,导致里面的this就是全局对象。如果改成普通函数,this就会动态指向被点击的按钮对象

嵌套的箭头函数

let insert = (value) => ({into: (array) => ({after: (afterValue) => {
  array.splice(array.indexOf(afterValue) + 1, 0, value);
  return array;
}})});

insert(2).into([1, 3]).after(1); //[1, 2, 3]
  • 管道用法

6、尾调用

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

/** 以下三种都不是尾调用 */
// 情况一 function f(x){
  let y = g(x);
  return y;
}
 // 情况二 function f(x){
  return g(x) + 1;
}
 // 情况三 function f(x){
  g(x);
}
  • 尾调用不一定出现在函数尾部,只要是最后一步操作即可

尾调用优化

  • 尾调用之所以与其他调用不同,就在于它的特殊的调用位置。其他函数调用会在内存形成一个“调用记录”,又称“调用帧”,保存调用位置和内存变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。以此类推,就形成了调用栈
  • 尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了
function f() {
  let m = 1;
  let n = 2;
  return g(m + n);
}
f();

// 等同于 
function f() {
  return g(3);
}


f();
// 等同于 
g(3);
  • “尾调用优化”(Tail call optimization),即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存

尾递归

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

严格模式

  • ES6 的尾调用优化只在严格模式下开启,正常模式是无效的

尾递归优化的实现

  • 就是采用“循环”换掉“递归”
function sum(x, y) {
  if (y > 0) {
    return sum(x + 1, y - 1);
  } else {
    return x;
  }
}

sum(1, 100000) // Uncaught RangeError: Maximum call stack size exceeded(…)

// 改写为蹦床函数
function trampoline(f) {
  while (f && f instanceof Function) {
    f = f();
  }
  return f;
}

function sum(x, y) {
  if (y > 0) {
    return sum.bind(null, x + 1, y - 1); // 这里会返回一个新的函数,并传入参数
  } else {
    return x;
  }
}
  • 蹦床函数并不是真正的尾递归优化,下面的实现才是
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;
    }
    return undefined;
  };
}

var sum = tco(function(x, y) {
  if (y > 0) {
    return sum(x + 1, y - 1) // 一直都是undefined,所以避免了递归
  }
  else {
    return x
  }
});

sum(1, 100000) // 100001

7、函数参数的尾逗号

8、Function.proptype.toString()

  • toString()方法返回函数代码本身,以前会省略注释和空格
function /* foo comment */ foo () {}

foo.toString() 
// function foo() {}
  • 修改后的toString()方法,明确要求返回一模一样的原始代码
function /* foo comment */ foo () {}

foo.toString() 
// "function /* foo comment */ foo () {}"

9、catch命令的参数省略

  • ES2019做出改变可以省略
try {
 // ... 
} catch {
 // ... 
}

以上是关于ES6语法总结-函数的扩展的主要内容,如果未能解决你的问题,请参考以下文章

ES6新增语法 + 内置对象扩展 + 箭头函数

总结常见的ES6新语法特性

VUE面试题总结

总结常见的ES6新语法特性。

ES6新语法之---函数扩展

ES6函数扩展