前言

本章介绍函数的扩展。有些不常用的知识了解即可。
本章原文链接:函数的扩展

函数参数的默认值

ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。
当函数形参没有被赋值时,才会将默认值赋值给函数参数。

// 默认值直接写在行参后面
function sampleFn(sample = 0, sample1 = 0) {
  return sample + sample1;
}

注意:

  • 参数变量是默认声明的,所以不能用letconst再次声明。
  • 使用参数默认值时,函数不能有同名参数。
  • 参数默认值是惰性求值的。
  • 函数的默认值指定后,函数length属性返回的是没有指定默认值的参数的个数。
  • 参数的默认值一旦设定,函数进行声明初始化时,参数会形成一个单独的作用域(context)。
// 默认值直接写在行参后面
function sampleFn(sample = 0, sample1 = 0,sample = 1) { // 不能有同名参数

  let sample = 1; // 不能再次声明

  return sample + sample1;
}

注意:通常情况下,定义了默认值的参数,应该是函数的尾参数。也就是放在最后面。

解构赋值默认值

// 函数的默认值与结构赋值的默认值可以结合使用
function sampleFn({ sample = 0, sample1 = 0 } = {}) { // 函数参数默认值
  return sample + sample1;
}

console.log(sampleFn({ sample: 23, sample1: 33 })); // 56 参数需对应解构赋值的类型

作用域

当函数参数设置了默认值,函数进行声明初始化时,函数参数会生成一个单独的作用域,等到初始化结束,该作用域就会消失。而且该行为只在函数参数指定了默认值才会出现。

let sample = 1;

/* 
    在声明的时候出现单独作用域
  在这个作用域中,变量没有定义,于是指向外层变量
    函数调用时,函数内部变量影响不到默认值变量
*/
function sampleFn(sample1 = sample) { 
  let sample = 2;
  console.log(sample1);
  return sample1;
}

sampleFn() // 1

rest 参数

ES6 引入 rest 参数 ,用于获取函数的多余参数。
arguments 对象是类数组,rest 参数是真正的数组。

形式为:...变量名,函数的最后一个命名参数以...为前缀。

// 下面例子中 ...values 为 rest参数 ,用于获取多余参数
const sample = function (title, ...values) {
  let sample = values.filter(
    (item) => {
      return item % 2 === 0;
    }
  )

  return (title + sample);
}

console.log(sample("求偶数", 1, 2, 6, 2, 1));  // 求偶数 2, 6, 2

注意:rest参数 只能是函数的最后一个参数,函数的length不包括rest参数

严格模式

在JavaScript中,只要在函数中的严格模式,会作用于函数参数和函数体。
ES2016 规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。

function sample() { // 参数使用默认值、解构赋值、扩展运算符 
  'use strict'; // 开启严格模式
}

name 属性

函数的name属性,返回该函数的函数名。

function sample() { };
let sample1 = function () { };
function sample2() {};

console.log(sample.name); // sample
console.log(sample1.name); //  sample1 

// bound sample2  使用了 bind 方法,输出会有bound前缀
console.log(sample2.bind({}).name); 
console.log((new Function).name); // anonymous  构造函数的name值为 anonymous

箭头函数

简单介绍

ES 6 新增一种函数定义方法,使用箭头连接参数列与函数题。
箭头函数相当于匿名函数,并且简化了函数定义,箭头函数没有prototype

// 普通函数
let sample = function (item) {
  return item;
};

// 上面函数等同于下面函数

// 使用箭头函数
let sample = (item) => { return item}; // 箭头函数

箭头函数简写

没错,箭头函数还可以简写

  1. 当参数只有一个时,可以省略箭头左边的括号,但没有参数时,括号不可以省略。
  2. 当函数体只有一个表达式时,可省略箭头右边的大括号,但同时必须省略return语句 并写在一行。
  3. 当函数体分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。
// 下面几种函数写法都相同
let sample = function (item) {
  return item;
};

let sample = (item) => { return item}; // 箭头函数 不省略

let sample = item => { return item}; // 省略左边圆括号

let sample = (item) => item; // 省略右边大括号和 return

let sample = item => item; // ✌省略左边圆括号和右边花括号和return

// 如果不需要返回值的特殊情况
let sample = item => void item;
console.log(sample()); // undefined

注意点

  • 箭头函数 的 This 默认指向定义它的作用域的 This
  • 箭头函数 不能用作构造函数。
  • 箭头函数 不可以使用 arguments 对象,该对象在函数体内不存在。
  • 箭头函数 不可以使用yield命令,也就不能作为 Generator 函数。

箭头函数的this

箭头函数 会继承自己定义时所处的作用域链上一层的this
箭头函数 this在定义的时候已经确定了,所以箭头函数this不会改变。
使用 call()apply() 方法时,也不能重新给箭头函数绑定thisbing()方法无效。

window.sample = "window 内 ";

function sampleFn() {
  let thiz = this;
  let sample = "sampleFn 内 ";

  let sampleObj = {
    sample: "sampleObj 内 ",

    // 普通函数
    sampleFn1: function () {
      console.log(thiz === this);
      console.log(this.sample);
    },

    // 箭头函数
    sampleFn2: () => {
      // 箭头函数的作用域为 sampleObj 上一层为 sampleFn
      console.log(thiz === this); //箭头函数的 this  
      console.log(this.sample);
    }
  }

  sampleObj.sampleFn1();  // false, sampleObj 内 
  sampleObj.sampleFn2();  // true, window 内
}

sampleFn();

尾调用优化

有两个概念

  1. 尾调用
    尾调用(Tail Call)是函数式编程的一个重要概念,就是指某个函数的最后一步是调用另一个函数。

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

ES6 明确规定,所有 ECMAScript 的实现,都必须部署“尾调用优化”。
这就是说,ES6 中只要使用尾递归,就不会发生栈溢出(或者层层递归造成的超时),相对节省内存。

这是什么意思呢?

尾调用的作用,在原文中是这样写的:

我们知道,函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。

换种方式解释吧

函数被调用的时候会有函数执行上下文被压入执行栈中,直到函数执行结束,对应的执行上下文才会出栈。
在函数A的内部调用函数B,如果函数B中有对函数A中变量的引用,那么函数A即使执行结束对应的执行上下文也无法出栈,如果函数B内部还有调用函数C那么要等函数C执行完,函数A、B对应的执行上下文才能出栈,以此类推,执行栈中要上一个函数(内层函数)的执行上下文,这就是尾调用优化。

// 尾递归
function sampleFn(sample) {
  if (sample <= 1) return 1;
  return sampleFn(sample - 1) + sample;
}
sampleFn(2);

注意 :

  • 当内层函数没有用到外层函数的内部变量的时候才可以进行尾调用优化。
  • 目前只有 Safari 浏览器支持尾调用优化,Chrome 和 Firefox 都不支持。

ES 6 的小修改

函数参数尾逗号

ES2017 允许函数的最后一个参数有尾逗号(trailing comma)。
这样的规定也使得,函数参数与数组和对象的尾逗号规则,保持一致了。

function sampleFn(
  sample1,
  sample2,
  sample3, // 可以在最后一个参数后面加 逗号 ','
) {}

toString()修改

Function.prototype.toString()
ES2019 对函数实例的toString()方法做出了修改。明确要求返回一模一样的原始代码。
toString()方法返回函数代码本身,ES6前会省略注释和空格。

function sampleFn() {
  // 注释
}

let sample = sampleFn.toString();
console.log(sample);
//输出 完全一样的原始代码,包括空格与注释
/*
function sampleFn() {
  // 注释
}
*/

catch 修改

ES2019 改变了catch语句后面必须携带参数的要求。允许catch语句省略参数。

try {
  // ...
} catch { // 不带参数
  // ...
}