ES6学习笔记9:函数的扩展

0 阅读1分钟

@[toc]

函数的扩展

函数参数的默认值

基本用法

在ES6允许为函数的参数设置默认值,即直接写在参数定义的后面

function Ponint(x = 0 , y = 0){
  this.x = x;
  this.y = y;
}
const p = new Ponint();
console.log(p); // { x : 0,y : 0 }

优点:

  1. 书写简洁
  2. 方便阅读
  3. 有利于将来代码优化
  • 注意:参数变量是默认声明的,所以不能使用letconst再次声明
function foo(x = 5){
  let x = 1; // 报错
  const x = 1; // 报错 
}
  • 使用参数默认值时,函数不能有同名的参数
function foo(x,x,y){
  // 不报错
}

function foo(x,x,y=1){
  // 报错
}
  • 参数默认传值不是传值的,而是每次都是重新计算默认值的表达式的值。参数默认值时惰性求值的。
let x = 99;
function foo(p = x + 1){
  console.log(p);
}

foo() // 100

x = 100;

foo(); // 101

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

参数默认值可以和解构赋值的默认值结合起来使用

function foo({x, y = 5}){
  console.log(x,y)
}
foo({}); // undefined 5
foo({x:1}); // 1 5
foo({x:1, y: 2}); // 1 2
foo(); // 报错

在这里使用了对象的解构赋值默认值,没有使用函数参数的默认值。当函数foo的参数为一个对象,xy才能通过解构赋值生成,当没有设置参数时,就会报错。

// 写法1
function foo1({x = 0, y = 0} = {}){
  return [x,y];
}
// 写法2
function foo2({x, y} = {x: 0, y: 0}){
  return [x,y];
}

// 在函数都没有参数的情况:
console.log(foo1()); // [0,0]
console.log(foo2()); // [0,0]

// 在函数的参数x,y都有值的情况
console.log(foo1({x: 1,y: 2})); // [1,2]
console.log(foo2({x: 1,y: 2})); // [1,2]

// 在函数的参数只有一个有值的情况
console.log(foo1({x: 1})); // [1,0]
console.log(foo2({x: 1})); // [1,undefined]

// 在函数的参数都没有值的情况
console.log(foo1({})); // [0,0]
console.log(foo2({})); // [undefined,undefined]

console.log(foo1({z: 3})); // [0,0]
console.log(foo2({z: 3})); // [undefined,undefined]

两种写法的区别:

  • 第一种写法函数参数的默认值是空对象,但是设置了对象解构赋值的默认值
  • 第二种写法函数参数的默认值是一个有具体属性的对象,但是没有设置对象解构赋值的默认值。

参数默认值的位置

通常情况:定义了默认值的参数,应该是函数的尾参数,如果非尾部的参数设置了默认值,那么这个参数就没有办法省略了。

function foo(x = 1, y){
  return [x,y];
}

console.log(foo()); // [1, undefined]
console.log(foo(2)); // [2, undefined]
console.log(foo(,1)); // 报错
console.log(foo(undefined,2)); // [1, 2]

当默认值的参数不是尾参数时,是不能省略这个参数的,除非设置这个参数为undefined

当只有传入undefined的时候,将会触发参数等于默认值,null是没有这个效果的。

函数的length属性

指定了默认值以后,函数的length属性将返回没有指定默认值的参数个数。

函数的length属性返回值 = 函数的参数个数 - 函数指定了默认值的参数个数

作用域

一旦设置了函数的默认值,函数进行声明初始化的时候,参数会形成一个单独的作用域,等到初始化结束这个作用域就结束了。在不设置参数的默认值的函数中,这个作用域是不存在的。

let x = 1;

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

foo(); // 1 

函数foo()被调用的时候,参数y = x形成单独的作用域,这个作用域中,变量x没有定义,所以指向外层的全局变量x.函数内部的局部变量不会影响默认值变量。当此时全局变量x不存在的时候,就会报错。

应用

  • 利用参数默认值,可以指定某一个参数不可以省略,如果省略抛异常。
function throwOfMissing(){
  throw new Error('Missing parameter');
}

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

foo(); // 报错:Missing parameter

上面的函数foo,如果调用的时候没有参数,就会调用默认值:throwOfMissing(),从而抛出一个错误。函数throwOfMissing()是有圆括号的,说明参数不是在定义时执行,而是在运行时执行。如果参数已经赋值,默认值中的函数就不会执行。

  • 将参数默认值设置为undefined表明这个参数是可以省略的
function foo( optional = undefined ){ ... }

rest参数

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

function add(...values){
  let sum = 0;
  
  for(const val of values){
    sum += val;
  }
  return sum;
}

console.log(add(1,2,3,4,5)); // 15

上面的函数,利用 rest参数,可以向这个函数传入任意个数的参数。

例子:rest参数代替arguments变量

// 使用 arguments 变量的写法
function sortNumbers() {
  return Array.prototype.slice.call(arguments).sort();
}
// 使用 rest 参数的写法
const sortNumber = (...numbers) => numbers.sort();

注意:

  • rest参数之后不能再有其他参数(即:只能是最后一个参数),否则会报错。
  • 函数的length属性不包括rest参数

严格模式的修改

在ES6中,函数参数使用了默认值、解构赋值、或者扩展运算符那么函数内部就不能显示设定为演示模式,否则会报错

使用严格模式:

'use strict';

规避这种限制的两种方法

  1. 设定全局的严格模式
'use strict';
function foo(a, b = a ){
  // ...
}
  1. 将函数包在一个无参数的立即执行函数里面
const foo = (function () {
  'use strict';
  return function(value = 42 ){
    return value;
  }
})

name属性

函数的name的属性返回这个函数的函数名

  • 将一个匿名函数赋值给一个变量,ES5的name属性是空字符串,ES6的name属性会返回实际的函数名
var f = function () {};

// ES5
f.name // ""

// ES6
f.name // "f"

  • 将一个具名函数赋值给一个变量,ES5和ES6的name属性都返回这个具名函数原本的名字
const bar = function baz(){};

// ES5
console.log(bar.name); // baz

// ES6
console.log(bar.name); // baz
  • Function构造函数返回的函数实例,name属性的值为anonymous.
console.log((new Function).name); // anonymous
  • bind返回的函数,name属性值会加上前缀bound.
function foo(){};
console.log(foo.bind({}).name); // bound foo

箭头函数

ES6 允许使用箭头(=>)定义函数

var f = v => v;
// 等同于
var f = function (v){
  return v;
}
  • 如果箭头函数不需要参数或者需要多个参数,就使用一个圆括号代表参数的部分。
  • 如果箭头函数的代码块部分多于一条语句,就使用大括号将它们括起来,并使用return 语句返回。
  • 如果箭头函数直接返回一个对象,必须在对象外面加上大括号,否则会报错。
let getTempItem = id => {id: id, name: 'Temp'}; // 报错
let getTempItem = id => ({id: id, name: 'Temp'}); // 不报错
  • 特殊情况,可以运行但是会得到错误的结果
let foo = () => {a : 1};
console.log(foo()); // undefined

上面的代码实际执行的语句是:1.

  • 如果箭头函数只有一行语句,且不需要返回值,可以采用如下写法:
let fn = () => void doesNotReturn();
  • 箭头函数可以与变量解构结合使用
const full = ({first, last}) => first + '' + last;
// 等同
function full(person){
  return person.first + '' + person.last;
}
  • 需要注意
  1. 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。

  2. 不可以当作构造函数,也就是不可以new.否则会抛出一个错误。

  3. 不可以使用arguments对象,这个对象在函数体内不存在,如果需要使用,可以使用rest参数代替。

  4. 不可以使用yield命令,因此箭头函数不能用做Generator函数。

  • 对于this对象指向是可变的,但是箭头函数中,它是固定的。
  • 指向外层函数的对应变量:argumentssupernew.target在箭头函数中是不存在的
  • 由于箭头函数没有自己的this,所以就不能在用call()apply()bind()这些方法去改变this的指向。

不能使用箭头函数的场景

由于箭头函数使得this从''动态''变成了''静态''

  • 定义对象的方法并且这个方法内部包括this.
const cat = {
  lives: 9,
  jumps: ()=>{
    this.lives--;
  }
}

这里的cat.jumps()方法是一个箭头函数,这里的错误的,当这里为普通函数的时候,这个方法内部的this指向的是cat,如果是箭头函数就使得this指向全局对象了,从而得不到预期的结果。

因为对象不构成单独的作用域,导致jumps箭头函数定义时的作用域就成了全局作用域。

  • 需要动态this的时候,也不应该使用箭头函数
const button = document.getElementById('press');
button.addEventListener('click', () => {
  this.classList.toggle('on');
}); // 点击时报错

因为button的监听函数是一个箭头函数,导致函数中的this就是全局对象,如果改成普通函数,this就会动态指向被点击的按钮对象

嵌套的箭头函数

箭头函数内部,还可以使用箭头函数。

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

console.log(insert(2).into([1,3]).after(1)); // [1,2,3]

尾调用优化

尾调用(Tail Call)是函数定义

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

function f(x){
  return g(x);
}

这里的函数f的最后一步就是调用函数g,这就是尾调用。

以下情况都不是尾调用

// 一
function f(x){
  let y = g(x);
  return y;
}

// 二
function f(x){
  return g(x) + 1;
}

// 三
function f(x){
  g(x);
}

尾调用不一定出现在函数尾部,只要是最后一步操作就可以了。

function f(x){
  if(x > 0){
    return m(x);
  }
  return n(x);
}

上面也是尾调用函数

尾调用优化

函数在调用的时候会在内存形成一个调用记录,又称“调用帧(call frame)”,保存调用位置和内部变量信息。如果函数A内部调用函数B,那么A的调用帧上方会形成一个B的调用帧。等B运行结束了,将结果返回给A,B的调用帧才会消失。如果函数B内部还调用函数C,那么还有一个C的调用帧,以此类推。所有的调用帧就形成一个“调用栈(call stack)”.

尾调用由于是函数的最后一个操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,所以直接用内层函数的调用帧,取代外层函数的调用帧就可以了。

 function f(){
   let m = 1;
   let n = 2;
   return g(m+n);
 }
f();
// 等同于
fucntion f(){
  return g(3);
}

// 等同于
g(3);

函数参数的尾逗号

ES2017允许函数的最后一个参数有逗号。在此之前,函数在定义和调用时都不允许最后一个参数后面出现逗号。

这样规定使得函数参数与数组和对象的尾逗号规则保持一致了。

Function.prototype.toString()

ES2019对函数实例的toString()方法返回代码本身,一模一样的原始代码,不会省略注视和空格。

在此之前,toString()方法返回的代码会省略注视和空格。

catch命令的参数省略

JavaScript 语言的try...catch解构,之前明确要求catch命令后面必须跟参数,接受try代码块抛出的错误对象。

try{
  // ...
}catch (err){
  // ...处理错误
}

在ES2019以后,catch语句省略参数

try{
  // ...
}catch{
  // ...
}

备注:本文是自己学习阮一峰老师的《ECMAScript 6 入门》所做的笔记,大部分例子来源于此书。