函数的扩展

函数参数的默认值

ES6 之前,不能直接为函数的参数指定默认值,只能采用变通的方法。

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

上面代码检查函数log的参数y有没有赋值,如果没有,则指定默认值为World。这种写法的缺点在于,如果参数y赋的值对应的布尔值为false,则该赋值不起作用。

function greet(name, greeting) {
  name = (typeof name !== 'undefined') ?  name : 'Student';
  greeting = (typeof greeting !== 'undefined') ?  greeting : 'Welcome';

  return `${greeting} ${name}!`;
}

greet(); // Welcome Student!
greet('James'); // Welcome James!
greet('Richard', 'Howdy'); // Howdy Richard!

为了避免这个问题,通常需要先判断一下参数y是否被赋值,如果没有,再等于默认值。

if (typeof y === 'undefined') {
  y = 'World';
}

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

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

log('Hello') // Hello World

剩余参数

剩余参数也用三个连续的点 ( ... ) 表示,使你能够将不定数量的元素表示为数组。

与解构赋值结合

剩余参数可以与解构赋值结合起来,用于生成数组。

const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest  // [2, 3, 4, 5]

可以将剩余参数看着展开运算符的对立面;如果展开运算符是拿出包装盒中的所有物品,那么剩余参数就是将所有物品放回包装盒中。

可变参数函数

剩余参数的另一个用例是处理可变参数函数。可变参数函数是接受不定数量参数的函数。

例如,假设有一个叫做 sum() 的函数,它会计算不定数量的数字的和。在运行期间,如何调用 sum() 函数?

sum(1, 2);
sum(10, 36, 7, 84, 90, 110);
sum(-23, 3000, 575000);

实际上有无数种方式可以调用 sum() 函数。不管传入函数的数字有多少个,应该始终返回数字的总和。

  • 使用参数对象

在之前版本的 JavaScript 中,可以使用参数对象处理这种类型的函数。参数对象是像数组一样的对象,可以当做本地变量在所有函数中使用。它针对传入函数的每个参数都包含一个值,第一个参数从 0 开始,第二个参数为 1,以此类推。

function sum() {
  let total = 0;  
  for(const argument of arguments) {
    total += argument;
  }
  return total;
}

现在可以正常运行,但是存在问题:

  1. 如果查看 sum() 函数的定义,会发现它没有任何参数。这容易引起误导,因为我们知道 sum() 函数可以处理不定数量的参数。

难以理解。

  1. 如果你从未使用过参数对象,那么看了这段代码后很可能会疑问参数对象来自何处。是不是凭空出现的?看起来肯定是这样。
  • 使用剩余参数
function sum(...nums) {
  let total = 0;  
  for(const num of nums) {
    total += num;
  }
  return total;
}
注意,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。

箭头函数

ES6 引入了一种新的函数,叫做箭头函数。箭头函数和普通函数的行为非常相似,但是在语法构成上非常不同。

普通函数可以是函数声明或函数表达式,但是箭头函数始终是表达式。实际上,它们的全称是“箭头函数表达式”

//普通函数
const upperizedNames = ['Farrin', 'Kagure', 'Asser'].map(function(name) { 
  return name.toUpperCase();
});

//箭头函数
const upperizedNames = ['Farrin', 'Kagure', 'Asser'].map(
  name => name.toUpperCase()
);

如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。

var f = () => 5;
// 等同于
var f = function () { return 5 };

var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
  return num1 + num2;
};

如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。

var sum = (num1, num2) => { return num1 + num2; }

箭头函数中的this

对于普通函数,this 的值基于函数如何被调用。对于箭头函数,this 的值基于函数周围的上下文。换句话说,箭头函数内的,this 的值与函数外面的 this 的值一样。

我们先看一个普通函数中的 this 示例,再看一个箭头函数是如何使用 this 的。

// constructor
function IceCream() {
  this.scoops = 0;
}

// adds scoop to ice cream
IceCream.prototype.addScoop = function() {
  setTimeout(function() {
    this.scoops++;
    console.log('scoop added!');
  }, 500);
};

const dessert = new IceCream();
dessert.addScoop();

运行上述代码后,你会认为半毫秒之后,dessert.scoops 会是1。但并非这样:结果是NaN

这是因为传递给 setTimeout() 的函数被调用时没用到 new、call() 或 apply(),也没用到上下文对象。意味着函数内的 this 的值是全局对象,不是 dessert 对象。实际上发生的情况是,创建了新的 scoops 变量(默认值为 undefined),然后递增(undefined + 1 结果为 NaN

// constructor
function IceCream() {
  this.scoops = 0;
}

// adds scoop to ice cream
IceCream.prototype.addScoop = function() {
  setTimeout(() => { // an arrow function is passed to setTimeout
    this.scoops++;
    console.log('scoop added!');
  }, 0.5);
};

const dessert = new IceCream();
 
console.log(dessert.scoops); //1

因为箭头函数从周围上下文继承了 this 值,所以这段代码可行!

如果我们将 addScoop() 方法也改为箭头函数,你认为会发生什么?

// constructor
function IceCream() {
    this.scoops = 0;
}

// adds scoop to ice cream
IceCream.prototype.addScoop = () => { // addScoop is now an arrow function
  setTimeout(() => {
    this.scoops++;
    console.log('scoop added!');
  }, 0.5);
};

const dessert = new IceCream();
dessert.addScoop();

这段代码因为同一原因而不起作用,即箭头函数从周围上下文中继承了 this 值。在 addScoop() 方法外面,this 的值是全局对象。因此如果 addScoop() 是箭头函数,addScoop() 中的 this 的值是全局对象。这样的话,传递给 setTimeout() 的函数中的 this 的值也设为了该全局对象!


escaple_plan
287 声望10 粉丝