函数的扩展

基本用法

ES6 之前不能为函数的参数指定默认值:

const hello = (name) => {
  name = name || 'World';
  return `Hello ${name}`;
}

log(hello()); // 'Hello World'
log(hello('China')); // 'Hello China'
// 缺点:如果参数赋值了,但是对应的布尔值是 false,则赋值不起作用
log(hello('')); // 'Hello World'
log(hello(false)); // 'Hello World'

为了避免上面这个问题,通常需要判断参数是否被赋值。

const hello = (name) => {
  if (typeof name === 'undefined') {
    name = 'World';
  }
  return `Hello ${name}`;
}

log(hello()); // 'Hello World'
log(hello('China')); // 'Hello China'
log(hello('')); // 'Hello '
log(hello(false)); // 'Hello false'

ES6 的语法:

const hello = (name = 'World') => {
  return `Hello ${name}`;
};

log(hello()); // 'Hello World'
log(hello('China')); // 'Hello China'
log(hello('')); // 'Hello '
log(hello(false)); // 'Hello false'

函数参数是默认声明的,所以不能再用 const 或 let 声明。

使用参数默认值时,不能有同名参数。

const hello = (name = 'World', name) => {
  return `Hello ${name}`;
};
// Uncaught SyntaxError: Duplicate parameter name not allowed in this context

参数默认值不是传值的,而是每次都重新计算默认值表达式的值。

let x = 10;

const calculate = (a = x + 1) => {
  return a;
};

log(calculate(x)); // 10
x = 11;
log(calculate(x)); // 11

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

const foo = ({x, y = 1}) => {
  console.log(x, y);
};

foo({}); // undefined 1
foo({x: 1, y: 2}); // 1 2
foo({x: 1}); // 1 1
foo(); // Uncaught TypeError: Cannot destructure property `x` of 'undefined' or 'null'.

提供函数参数的默认值,就不会报错。

const foo = ({x, y = 1} = {}) => {
  console.log(x, y);
};
foo(); // undefined 1

两种写法的差别:

const foo1 = ({x = 1, y = 2} = {}) => {
  console.log(x, y);
};

const foo2 = ({x, y} = {x: 1, y: 2}) => {
  console.log(x, y);
};

foo1(); // 1 2
foo2(); // 1 2

foo1({x: 3, y: 4}); // 3 4
foo2({x: 3, y: 4}); // 3 4

foo1({x: 3}); // 3 2
foo2({x: 3}); // 3 undefined

foo1({}); // 1 2
foo2({}); // undefined undefined

参数默认值的位置

有默认值的参数不是尾参数,不能只省略该参数,不省略后面的参数。

const foo = (x = 1, y) => {
  console.log(x, y);
};

foo(); // 1 undefined
foo(2); // 2 undefined
foo(undefined, 1); // 1 1
foo(, 2); // Uncaught SyntaxError: Unexpected token ,

传入 undefined 可以触发参数的默认值,null 不会。

foo(null, 1); // null 1

函数的 length 属性

返回没有指定默认值的参数的个数。

const foo = (x, y) => {};
log(foo.length); // 2
const foo = (x, y = 1) => {};
log(foo.length); // 1

length 的含义是该函数预期传入的参数个数,指定默认值后,预期传入的参数个数就不包括这个参数了。

rest 参数也不会计入参数个数。

const foo = (...rest) => {};
log(foo.length); // 0

如果设置的默认值参数不是尾参数,那么其后面的参数也不会计入参数个数。

const foo = (x = 1, y) => {};
log(foo.length); // 0

作用域

设置了参数默认值,函数只声明初始化时,参数会形成一个单独的作用域,等到初始化结束后,这个作用域会消失。在不设置默认值时,不出现这个作用域。

const x = 1;
const foo = (x, y = x) => { // <-- 在单独的作用域中,y 指向的是这个作用域中的 x,即 2
  console.log(y);
};
foo(2); // 2
const x = 1;
const foo = (y = x) => {
  console.log(y);
};
foo(); // 1 如果未传入 x,会指向全局的变量

全局变量 x 不存在,会报错。

const foo = (y = x) => {
  console.log(y);
};
foo(); // Uncaught ReferenceError: x is not defined
const x = 1;
const foo = (x = x) => {
  console.log(x);
};
foo(2); // 2
const x = 1;
const foo = (x = x) => {
  console.log(x);
};
foo(); // 暂时性死区 demo.js:143 Uncaught ReferenceError: Cannot access 'x' before initialization

应用

利用参数默认值,可以指定某个参数不可省略,如果省略就抛出错误。

const throwIfMissing = () => {
  throw new Error('Missing parameter.');
};

const foo = (name = throwIfMissing()) => {
  return name;
};

foo(); // Uncaught Error: Missing parameter.

rest 参数

用于获取函数的剩余参数。

const sum = (...values) => {
  return values.reduce((accumulator, current) => {
    return accumulator + current;
  }, 0);
};

console.log(sum(1, 2, 3)); // 6

rest 参数代替 arguments 变量:

const sum = (...values) => {
  return values.sort();
};

rest 参数后不能再有其他参数,否则会报错。

严格模式

只要函数参数使用了默认值,解构赋值或扩展运算符,函数内部就不能显示设定为严格模式。

略...

name 属性

返回函数的名称。

log((function sum(){}).name); // sum

匿名函数:

log((new Function).name); // anonymous

bind 返回的函数,name 属性会加上 bound 前缀。

const foo = () => {};
log(foo.bind({}).name); // bound foo

箭头函数

基本用法

const foo = () => 5;
const sum = (a, b) => a + b;

如果代码块部分多于一条语句,要使用大括号括起来,使用 return 语句返回。

const sum = (a, b) => { return a + b };

由于大括号被解释为代码块,如果箭头函数返回一个对象,必须在对象外面加(),否则报错。

// const foo = () => {a: 1, b: 2}; // Uncaught SyntaxError: Unexpected token :
const foo = () => ({a: 1, b: 2});

特殊情况:

const foo = () => {a: 1}; // 不报错
log(foo()); // undefined 结果错误

箭头函数可以与变量解构结合使用。

const name = ({firstName, lastName}) => `${firstName} ${lastName}`;
log(name({firstName: 'Wendy', lastName: 'Lu'})); // Wendy Lu

rest 参数与箭头函数结合。

const numbers = (...nums) => nums;
log(numbers(1, 2, 3)); // [1, 2, 3]

使用注意点

  • 函数体内的 this,是定义时所在的对象,不是使用时所在的对象。
  • 不可以当作构造函数使用,即不可使用 new 命令
  • 不可以使用 arguments 对象,在函数体内不存在
  • 不可以使用 yield 命令,因此箭头函数不可以用作 Generator 函数。

在箭头函数中,this 对象的指向是固定的。

function foo() {
  setTimeout(function () {
    console.log(this.id);
  }, 100);
}

var id = 'global';

foo.call({ id: 'foo' }); // 'global', this 指向全局对象


function foo2() {
  setTimeout(() => {
    console.log(this.id);
  }, 100);
}

foo2.call({ id: 'foo' }); // 'foo',this 指向定义生效时的对象

箭头函数让 this 指向固定化,有利于封装回调函数。

const handler = {
  id: '11',
  init() {
    document.addEventListener('click', event => {
      console.log(this, 1); // handler
      this.doSomething(event.type);
    }, false);
  },
  doSomething(type) {
    console.log(`Handling ${type} for ${this.id}`);
  }
};

handler.init();

const handler2 = {
  id: '22',
  init() {
    document.addEventListener('click', function(event) {
      console.log(this, 2); // document
      this.doSomething(event.type);
    }, false);
  },
  doSomething(type) {
    console.log(`Handling ${type} for ${this.id}`);
  }
};

handler2.init(); // Uncaught TypeError: this.doSomething is not a function

arguments、super、new.target 在箭头函数中不存在,指向外层函数。

function foo() {
  setTimeout(() => {
    console.log(arguments);
  }, 100);
}

foo(1, 2, 3); // [1, 2, 3]

箭头函数没有 this,所以不能用 bind、call、apply 去改变 this 的指向。

const result = (function() {
  return (() => this.name).bind({name: 'inner'})();
}).call({name: 'outer'});

log(result); // 'outer'

不适用的场合

const cat = {
  lives: 9,
  jumps: () => {
    console.log(this);
  }
};

cat.jumps(); // this 指向 window

需要动态 this 的时候:

const btn = document.querySelector('.btn');
btn.addEventListener('click', () => {
  console.log(this); // window
});

上面代码需要动态指向被点击的对象。

嵌套的箭头函数

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

用箭头函数改造:

const insert = (value) => ({into: (array) => ({after: (afterValue) => {
  array.splice(array.indexOf(afterValue) + 1, 0, value);
  return array;
}})});
log(insert(4).into([1, 2, 3, 5]).after(3)); // [1, 2, 3, 4, 5]

部署管道机制--前一个函数的输出是后一个函数的输入。

const pipe = (...functions) => 
  (val) => functions.reduce((accumulator, current) => current(accumulator), val);
const add = (a) => a + 1;
const multiply = (a) => a * 2;
log(pipe(add, multiply)(1)); // 4

尾调用优化

尾调用就是函数的最后一步是调用另外一个函数。

function bar(a) {}

function foo(a) {
  return bar(a);
}

下面三种情况不是尾调用:

function bar(a) {}

// 第一种
function foo(a) {
  const x = bar(a);
  return x;
}

// 第二种
function foo2(a) {
  return bar(a) + 1;
}

// 第三种
function foo3(a) {
  bar(a);
}

尾递归

尾调用自身,就叫尾递归。

略...

Last Updated: 6/6/2019, 2:44:45 PM