深入理解ES6:3.函数

2019-11-28  本文已影响0人  独木舟的木

Tags:默认参数、不定参数、展开运算符、name 属性、元属性 new.target、箭头函数、尾调用优化、

函数形参的默认值

JavaScript 函数的特点:无论在函数定义中声明了多少形参,都可以传入任意数量的参数,也可以在定义函数时添加针对参数数量的处理逻辑,当已定义的形参无对应的传入参数时为其指定一个默认值。

在 ECMAScript 5 中模拟默认参数

创建函数,并为参数赋予默认值。

function makeRequest(url, timeout, callback) {

  // timeout、callback 为可选参数
  // 如果未传值,则通过逻辑或操作符 || 为缺失的参数设置默认值
  timeout = timeout || 2000;
  callback = callback || function() {};

  // 函数的其余部分
}

缺陷:如果我们想给 makeRequest 函数的第二个形参 timeout 传入值 0,即使这个值是合法的,也会被视为一个假值,并最终为 timeout 赋值 2000。

优化方案:通过 typeof 操作符检查参数类型:

function makeRequest(url, timeout, callback) {

  // timeout、callback 为可选参数
  // 如果未传值,默认值参数值为 "undefined"
  timeout =  (typeof timeout !== "undefined") ? timeout : 2000;
  callback = (typeof callback !== "undefined") ? callback : function() {};

  // 函数的其余部分
}

ECMAScript 6 中的默认参数值

ECMAScript 6 简化了为形参提供默认值的过程,如果没有为参数传入值,则为其提供一个初始值。

function makeRequest(url, timeout = 2000, callback = function() {}) {
  // 函数的其余部分
}

声明函数时,可以为任意参数指定默认值,在已指定默认值的参数后可以继续声明无默认值参数。

function makeRequest(url, timeout = 2000, callback) {
  // 函数的其余部分
}

此时,只有当不为第二个参数传入值或主动为第二个参数传入 undefined 时才会使用 timeout 的默认值:

// 使用 timeout 的默认值
makeRequest("/foo", undefined, function(body) {
  doSomething(body);
});

// 使用 timeout 的默认值
makeRequest("/foo");

// 不使用 timeout 的默认值
makeRequest("/foo", null, function(body) {
  doSomething(body);
});

默认参数值对 arguments 对象的影响

默认参数表达式

因为默认参数是在函数调用时求值,所以可以使用先定义的参数作为后定义参数的默认值。

在引用参数默认值时,只允许引用前面参数的值。

function add(first, second = first) {
  return first + second;
}

console.log(add(1, 1)); // 2
console.log(add(1)); // 2

处理无命名参数

在 JavaScript 函数中,无论函数已定义的命名参数有多少,都不限制调用时传入的实际参数数量,调用时总是可以传入任意数量的参数。

当传入更少数量的参数时,默认参数值的特性可以有效简化函数声明的代码。

当传入更多数量的参数时,ECMAScript 6 同样也提供了更好的方案。

ECMAScript 5 中的无命名参数

使用 arguments 对象来检查函数的所有参数。

// 返回一个给定对象的副本,包含原始对象属性的特定子集
function pick(object) {
  let result = Object.create(null);

  // 从第二个参数开始
  for (let i = 1; i < arguments.length; i++) {
    // 将 object 对象的属性值逐个赋值给第二个开始的参数
    result[arguments[i]] = object[arguments[i]];
  }

  return result;
}

let book = {
  title: 'Understanding ECMAScript 6',
  author: 'Nicholas C. Zakas',
  year: 2016
};

let bookData = pick(book, 'author', 'year');

console.log(bookData.author); // Nicholas C. Zakas
console.log(bookData.year); // 2016

不定参数

在函数的命名参数前添加三个点(...)就表明这是一个不定参数,该参数为一个数组,包含着自它之后传入的所有参数,通过这个数组名即可逐一访问里面的参数。

使用不定参数重写 pick() 函数。

// 不定参数 keys 包含的是 object 之后传入的所有参数。
function pick(object, ...keys) {
  let result = Object.create(null);

  for (let index = 0; index < keys.length; index++) {
    result[keys[i]] = object[keys[i]];
  }

  return result;
}

不定参数的使用限制

  1. 每个函数最多只能声明一个不定参数,而且一定要放在所有参数的末尾。
  2. 不定参数不能用于对象字面量 setter 之中。
let object = {

  // 语法错误:不可以在 setter 中使用不定参数
  set name(...value) {
    // 执行一些逻辑
  }
};

不定参数对 arguments 对象的影响

没有影响

无论是否使用不定参数,arguments 对象总是包含所有传入函数的参数。

function checkArgs(...args) {
  console.log(args.length); // 2
  console.log(arguments.length); // 2

  console.log(args[0], arguments[0]); // a a
  console.log(args[1], arguments[1]); // b b
}

checkArgs('a', 'b');

增强的 Function 构造函数

Function 构造函数很少用到,通常我们用它来动态创建新的函数

// Function 构造函数接受字符串形式的参数,分别为函数的参数和函数体。
var add = new Function('first', 'second', 'return first + second');

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

ECMAScript 6 增强了 Function 构造函数的功能,支持在创建函数时定义默认参数和不定参数。

定义默认参数:在参数名后添加一个等号及一个默认值

var add = new Function('first', 'second = first', 'return first + second');

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

定义不定参数,只需要在最后一个参数前添加...

var pickFirst = new Function('...args', 'return args[0]');

console.log(pickFirst(1, 2)); // 1

对于 Function 构造函数,新增的默认参数和不定参数特性,使其具备了与声明式创建函数相同的能力。

展开运算符

JavaScript 内建的 Math.max() 方法可以接受任意数量的参数,并返回值最大的那一个。

let value1 = 25,
    value2 = 50;

console.log(Math.max(value1, value2)); // 50

如果想从一个数组中挑选出最大的那个值应该怎么做?而 Math.max() 方法又不允许传入数组。

使用 apply() 方法手动遍历:

let array = [25, 50, 75, 100];

console.log(Math.max.apply(Math, array)); // 100

使用 ECMAScript 6 中的展开运算符:向 Math.max() 方法中传入一个数组,再在数组前添加不定参数使用符号...

let array = [25, 50, 75, 100];

console.log(Math.max(...array)); // 100

可以将展开运算符与其他正常传入的参数混合使用:

let array = [-25, -50, -75, -100];

// 传入限定值 0 ,保证最小值为正数
console.log(Math.max(...array, 0)); // 0

综上,展开运算符可以简化使用数组给函数传参的编码过程

name 属性

ECMAScript 6 为所有函数新增了 name 属性用于辅助辨别函数。

💡💡💡 函数 name 属性的值不一定引用同名变量,它只是协助调试用的额外信息,所以不能使用 name 属性的值来获取对函数的引用。

如何选择合适的名称

// 函数
function doSomething() {
  // 空函数
}

// 函数表达式
var doAnotherThing = function() {
  // 空函数
};

console.log(doSomething.name); // doSomething,对应声明的函数名
console.log(doAnotherThing.name); // doAnotherThing,对应被赋值为该匿名函数的变量名

name 属性的特殊情况

// 函数表达式的名字权重比变量名高
var doSomething = function doSomethingElse() {
  // 空函数
};

var person = {
  firstName() {
    return 'Nicholas';
  },
  sayName: function() {
    console.log(this.name);
  }
};

console.log(doSomething.name); // doSomethingElse
console.log(person.sayName.name); // sayName
console.log(person.firstName.name); // firstName

明确函数的多重用途

function Person(name) {
  this.name = name;
}

var person = new Person('Andy');
var notAPerson = Person('Andy');

console.log(person); // Person { name: 'Andy' }
console.log(notAPerson); // undefined

JavaScript 函数有两个不同的内部方法:[[Call]][[Construct]]

当通过 new 关键字调用函数时,执行的是 [[Construct]] 函数,它负责创建一个通常被称作实例的新对象,然后再执行函数体,将 this 绑定到实例上;

如果不通过 new 关键字调用函数,则执行 [[Call]] 函数,从而直接执行代码中的函数体。

具有 [[Construct]] 方法的函数被统称为构造函数

在 ECMAScript 5 中判断函数被调用的方法

在 ECMAScript 5 中,如果想确定一个函数是否通过 new 关键字被调用(或者说,判断该函数是否作为构造函数被调用),最流行的方式是使用 instacneof:

function Person(name) {
  // 检查 this 值是否是构造函数的实例
  // 因为 [[Construct]] 方法会创建一个 Person 的新实例,并将 this 绑定到新实例上。
  if (this instanceof Person) {
    this.name = name; // 如果通过 new 关键字调用
  } else {
    throw new Error('必须通过 new 关键字来调用 Person');
  }
}

var person = new Person('Andy');
var notAPerson = Person('Andy'); // 抛出错误
// 例外:不依赖 new 方法也可以将 this 绑定到 Person 的实例上
var stillWorkPerson = Person.call(person, 'Andy'); // 有效!!!

console.log(person); // Person { name: 'Andy' }
console.log(notAPerson);
console.log(stillWorkPerson); // undefined

元属性 new.target

当调用函数的 [[Construct]] 方法时,new.target 被赋值为 new 操作符的目标。通常是新创建对象实例,也就是函数体内 this 的构造函数;

如果调用 [[Call]] 方法,则 new.target 被赋值为 undefined

function Person(name) {
  if (typeof new.target !== "undefined") {
    this.name = name; // 如果通过 new 关键字调用
  } else {
    throw new Error('必须通过 new 关键字来调用 Person');
  }
}

块级函数

在 ES5 的严格模式下,在代码块内部声明函数时程序会抛出错误。ES6 中则视为块级函数,从而可以在定义该函数的代码块内访问和调用。

在代码块中,块级函数会被提升至块的顶部,而用 let 定义的函数表达式不会被提升。

在 ES6 的非严格模式下,块级函数不再被提升至代码块的顶部,而是提升至外围函数或全局作用域的顶部。

箭头函数

箭头函数语法

// 一个参数
let reflect = value => value;

// 实际上相当于
let reflect = function (value) {
  return value;
}
// 多个参数
let sum = (num1, num2) => num1 + num2;

// 实际上相当于
let sum = function (num1, num2) {
  return num1 + Number;
}

尾调用优化

尾调用指的是函数作为另一个函数的最后一条语句被调用

在 ES5 中,尾调用会创建一个新的栈帧(stack frame),而在 ES 6 中,尾调用会清除并重用当前栈帧。

ES 6 尾调用需要满足的条件:

尾调用优化的主要应用场景:递归函数

// 阶乘函数
function factorial(n) {
  if (n <= 1) {
    return 1;
  } else {
    // 无法优化,因为这里在返回后还执行了乘法操作
    // 也就是说,如果在尾调用返回后还执行了其他操作,即无法得到尾调用优化
    return n * factorial(n - 1);
  }
}

// 通过默认参数将乘法操作移出 return 语句
// 用 p 来保存乘法结果,下一次迭代中取出用于计算,不再需要额外的函数调用
function factorial(n, p = 1) {
  if (n <= 1) {
    return 1 * p;
  } else {
    
    let result = n * p;
    return factorial(n-1, result);
  }
}
上一篇下一篇

猜你喜欢

热点阅读