6种声明JavaScript函数的方式

翻译:道奇
做者:Dmitri Pavlutin
原文: 6 Ways to Declare JavaScript Functionsjavascript

函数是一段参数化的代码块,定义一次可屡次调用。在JavaScript中,函数由许多组件组成并受它们影响:java

  • 函数体的代码
  • 参数列表
  • 能够从词法做用域访问的变量
  • 返回值
  • 调用该函数时的上下文this
  • 命名函数或匿名函数
  • 保存函数对象的变量
  • arguments对象(在箭头函数中没有)

这篇文章讲述六种声明JavaScript函数的方法:语法、示例和常见的陷阱。此外,您将了解在特定的状况下什么时候使用特定的函数类型。express

1.函数声明

"函数声明function关键字、必需的函数名、一对括号中的参数列表(para1,…, paramN)和一对花括号{…}分包裹着主体代码。"数组

函数声明的一个例子:浏览器

// 函数
function isEven(num) {
  return num % 2 === 0;
}
isEven(24); // => true
isEven(11); // => false
复制代码

function isEven (num) {…}是定义了isEven函数的函数声明,isEven函数用来判断数字是不是偶数。安全

函数声明在当前域内建立一个变量,它的标识符就是函数名,它的值就是函数对象。bash

函数变量被提高到当前顶层做用域,这意味着能够在函数声明前就进行调用(请继续参阅本章看更多的细节)。闭包

建立的函数会被命名,函数对象的name属性值就是它的名称,name在查看调用堆栈时很是有用:在调试或读取错误消息时。编辑器

让咱们在一个例子中看看这些属性:函数

// 提高的变量
console.log(hello('Aliens')); // => 'Hello Aliens!'
// 命名的函数
console.log(hello.name)       // => 'hello'
// 变量保存了函数对象
console.log(typeof hello);    // => 'function'
function hello(name) {
  return `Hello ${name}!`;
}
复制代码

函数声明函数hello(name){…}建立一个变量hello,该变量被提高到当前做用域的顶部。hello变量保存函数对象,hello.name包含函数名:'hello'

1.1常规函数

当须要使用常规函数时,是比较适合使用函数声明的。常规的意思是一次声明该函数,而后在许多不一样的地方调用它。这是基本的场景:

function sum(a, b) {
  return a + b;
}
sum(5, 6);           // => 11
([3, 7]).reduce(sum) // => 10
复制代码

由于函数声明在当前做用域中建立了一个变量,同时还建立了常规函数调用,因此它对于递归或分离事件监听器很是有用。与函数表达式或箭头函数相反,它不经过函数变量的名称建立绑定。

例如,要递归计算阶乘,就必须访问的函数:

function factorial(n) {
  if (n === 0) {
    return 1;
  }
  return n * factorial(n - 1);
}
factorial(4); // => 24
复制代码

factorial()函数内部使用保存函数的变量进行递归调用:factorial(n - 1)

可使用一个函数表达式并将其赋给一个常规变量,例如const factorial = function(n){…}。可是函数声明function factorial(n)是紧凑的(不须要const=)。

函数声明的一个重要特性是它的提高机制,也就是同一域内容许在声明以前使用。

提高在某些状况下是颇有用的。例如,当您但愿在脚本的开头不须要阅读函数的具体实现就能够知道如何调用函数。函数具体实现能够放在文件的下面,所以不用滚到底部看。

您能够在这里阅读关于函数声明提高的更多细节。

1.2 和函数表达式的区别

函数声明和函数表达式很容易混淆。它们看起来很是类似,但产生的函数具备不一样的属性。

一个容易记住的规则:语句中的函数声明老是以关键字function开头,不然它就是一个函数表达式(见第2节)。

下面的示例是一个函数声明,它的语句以function关键字开头:

// 函数声明: 以 "function"开始
function isNil(value) {
  return value == null;
}
复制代码

在使用函数表达式的状况下,JavaScript语句不以function关键字开头(它出如今语句代码的中间):

// 函数表达式: 以"const"开头
const isTruthy = function(value) {
  return !!value;
};
// 函数表达式做为.filter()的参数
const numbers = ([1, false, 5]).filter(function(item) {
  return typeof item === 'number';
});
// 函数表达式(IIFE): 以 "("开头
(function messageFunction(message) {
  return message + ' World!';
})('Hello');
复制代码

1.3 条件语句中的函数声明

一些JavaScript环境在调用一个出如今{…}ifforwhile语句中的声明时会抛出异常。

让咱们启用严格模式,看看当一个函数声明在条件语句中:

(function() {
  'use strict';
  if (true) {
    function ok() {
      return 'true ok';
    }
  } else {
    function ok() {
      return 'false ok';
    }
  }
  console.log(typeof ok === 'undefined'); // => true
  console.log(ok()); // Throws "ReferenceError: ok is not defined"
})();
复制代码

当调用ok()时,JavaScript抛出ReferenceError: ok没有定义,由于函数声明在一个条件块中。

条件语句中的函数声明在非严格模式下是容许的,但这使得代码很混乱。

做为这些状况的通常规则,当函数应该在某些条件下才建立——使用函数表达式。让咱们看看如何处理:

(function() {
  'use strict';
  let ok;
  if (true) {
    ok = function() {
      return 'true ok';
    };
  } else {
    ok = function() {
      return 'false ok';
    };
  }
  console.log(typeof ok === 'function'); // => true
  console.log(ok()); // => 'true ok'
})();
复制代码

由于函数是一个常规对象,因此根据条件将它赋给一个变量。调用ok()工做正常,没有错误。

2. 函数表达式

"函数表达式由function关键字、可选函数名、一对括号中的参数列表(para1,…, paramN)和一对花括号{…}分隔主体代码组成。"

函数表达式的一些例子:

const count = function(array) { // 函数表达式
  return array.length;
}
const methods = {
  numbers: [1, 5, 8],
  sum: function() { // 函数表达式
    return this.numbers.reduce(function(acc, num) { // func. expression
      return acc + num;
    });
  }
}
count([5, 7, 8]); // => 3
methods.sum();    // => 14
复制代码

函数表达式建立了一个能够在不一样的状况下使用的函数对象:

  • 做为对象赋值给变量count = function(…){…}
  • 在对象上建立一个方法sum:function(){…}
  • 使用函数做为回调。reduce(function(…){…})

函数表达式是JavaScript中最重要的部分。一般,除了箭头函数以外,还要处理这种类型的函数声明(若是您喜欢简短的语法和词法上下文)。

2.1命名函数表达式

函数没有名字就是匿名的(name属性是一个空字符''):

(
  function(variable) {return typeof variable; }
).name; // => ''
复制代码

这是一个匿名函数,它的名字是一个空字符串。

有时能够推断函数名。例如,当匿名函数被赋给一个变量:

const myFunctionVar = function(variable) { 
  return typeof variable; 
};
myFunctionVar.name; // => 'myFunctionVar'
复制代码

匿名函数名是'myFunctionVar',由于myFunctionVar变量名用做函数名。

当表达式指定了名称时,就是命名函数表达式。与简单的函数表达式相比,它有一些额外的属性:

  • 建立一个命名函数,即name属性就是函数名
  • 在函数体内部,与函数同名的变量指向函数对象

让咱们使用上面的例子,可是在函数表达式中设置一个名称:

const getType = function funName(variable) {
  console.log(typeof funName === 'function'); // => true
  return typeof variable;
}
console.log(getType(3));     // => 'number'
console.log(getType.name);   // => 'funName'

console.log(typeof funName); // => 'undefined'
复制代码

函数funName(variable){…}是一个命名函数表达式。变量funName能够在函数域内访问,但不能在外部访问。不管哪一种方式,函数对象的name属性都是函数名称:funName

2.2指定函数表达式

当一个函数表达式const fun = function(){}被赋值给一个变量时,一些引擎会从这个变量中推断出函数名。可是,回调可能做为匿名函数表达式进行传值的,不会存储到变量中:所以引擎没法肯定它的名称。

支持命名函数和避免匿名函数能够得到如下好处:

  • 使用函数名时,错误消息和调用堆栈显示更详细的信息
  • 经过减小匿名堆栈名称的数量,使调试更加温馨
  • 从函数名能够看出函数的做用
  • 您能够在递归调用或分离事件监听器的范围内访问函数

3.简写方法定义

"简写方法定义可用于对象常量和ES2015类的方法声明。您可使用函数名来定义它们,后面跟着一对括号中的参数列表(para1,…, paramN)和一对花括号{…}分隔主体语句。"

下面的示例在对象常量中使用了一个简写方法定义:

const collection = {
  items: [],
  add(...items) {
    this.items.push(...items);
  },
  get(index) {
    return this.items[index];
  }
};
collection.add('C', 'Java', 'PHP');
collection.get(1) // => 'Java'
复制代码

collection对象中的add()get()方法是使用简短的方法定义进行定义的。这些方法像常规方法这样调用:collection.add(…)collection.get(…)

与传统的属性定义方法相比,使用名称、冒号和函数表达式add: function(…){…}这种简短方法定义的方法有如下几个优势:

  • 更短的语法更容易理解
  • 与函数表达式相反,简写方法定义建立一个指定的函数。它对调试颇有用。

类语法须要简短的方法声明:

class Star {
  constructor(name) {
    this.name = name;
  }
  getMessage(message) {
    return this.name + message;
  }
}
const sun = new Star('Sun');
sun.getMessage(' is shining') // => 'Sun is shining'
复制代码

3.1计算获得的属性名和方法

ECMAScript 2015增长了一个很好的特性:在对象常量和类中计算属性名。

计算属性使用稍微不一样的语法[methodName](){…},则方法定义以下:

const addMethod = 'add',
  getMethod = 'get';
const collection = {
  items: [],
  [addMethod](...items) {
    this.items.push(...items);
  },
  [getMethod](index) {
    return this.items[index];
  }
};
collection[addMethod]('C', 'Java', 'PHP');
collection[getMethod](1) // => 'Java'
复制代码

[addMethod] (…) {…}[getMethod](…){…}是具备计算属性名的简写方法声明。

4. 箭头

"箭头函数是使用一对包含参数列表(param1, param2,…,paramN)而后是一个胖箭头=>和一对花括号{…}分隔主体语句进行定义的。"

当箭头函数只有一个参数时,能够省略括号。当它包含一个语句时,花括号也能够省略。

基本用法:

const absValue = (number) => {
  if (number < 0) {
    return -number;
  }
  return number;
}
absValue(-10); // => 10
absValue(5);   // => 5
复制代码

absValue是一个计算数字绝对值的箭头函数。

使用胖箭头声明的函数具备如下属性:

  • 箭头函数不建立它的执行上下文,而是按词法处理它(与函数表达式或函数声明相反,它们根据调用建立本身的this)
  • 箭头函数是匿名的。可是,引擎能够从指向函数的变量中推断出它的名称。
  • arguments对象在箭头函数中不可用(与提供arguments对象的其余声明类型相反)。可是,您能够自由地使用rest参数(…params)

4.1上下文透明

this关键字是JavaScript的一个使人困惑的方面(查看本文以得到关于this的详细说明)。

由于函数建立本身的执行上下文,因此一般很难检测this的值。

ECMAScript 2015经过引入箭头函数改进了this的用法,该函数按词法获取上下文(或者直接使用外部域的this)。这种方式很好,由于当函数须要封闭的上下文时,没必要使用.bind(This)或存储上下文var self = This

让咱们看看如何从外部函数继承this:

class Numbers {
  constructor(array) {
    this.array = array;
  }
  addNumber(number) {
    if (number !== undefined) {
       this.array.push(number);
    } 
    return (number) => { 
      console.log(this === numbersObject); // => true
      this.array.push(number);
    };
  }
}
const numbersObject = new Numbers([]);
const addMethod = numbersObject.addNumber();

addMethod(1);
addMethod(5);
console.log(numbersObject.array); // => [1, 5]
复制代码

Numbers类有一个数字数组,并提供addNumber()方法来插入新数值。

当在不提供参数的状况下调用addNumber()时,返回一个容许插入数字的闭包。这个闭包是一个this等于numbersObject实例的箭头函数,由于上下文是从addNumbers()方法按词法获取的。

若是没有箭头函数,您必须手动修复上下文。它意味着就得使用像.bind()方法这样的进行变通:

//...
    return function(number) { 
      console.log(this === numbersObject); // => true
      this.array.push(number);
    }.bind(this);
//...
复制代码

或将上下文存储到一个单独的变量var self = this:

//...
    const self = this;
    return function(number) { 
      console.log(self === numbersObject); // => true
      self.array.push(number);
    };
//...
复制代码

当您但愿从封闭上下文获取的this保持原样时,可使用上下文透明性。

4.2短回调

在建立箭头函数时,对于单个参数和单个主体语句,括号对和花括号是可选的。这有助于建立很是短的回调函数。

让咱们建一个函数找出包含0的数组:

const numbers = [1, 5, 10, 0];
numbers.some(item => item === 0); // => true
复制代码

item => item === 0是一个简单的箭头函数。

请注意,嵌套的短箭头函数很难理解,使用最短的箭头函数方式的方便方法是单个回调(不嵌套)。

若是须要,在编写嵌套箭头函数时使用箭头函数的扩展语法。它只是更容易阅读。

5. 生成器函数

JavaScript中的生成器函数返回生成器对象。它的语法相似于函数表达式、函数声明或方法声明,只是它须要一个星号*

生成器函数的声明形式以下: a. 函数声明形式 function* <name>():

function* indexGenerator(){
  var index = 0;
  while(true) {
    yield index++;
  }
}
const g = indexGenerator();
console.log(g.next().value); // => 0
console.log(g.next().value); // => 1
复制代码

b. 函数表达式形式function* ():

const indexGenerator = function* () {
  let index = 0;
  while(true) {
    yield index++;
  }
};
const g = indexGenerator();
console.log(g.next().value); // => 0
console.log(g.next().value); // => 1
复制代码

c.简写方法定义形式 *<name>():

const obj = {
  *indexGenerator() {
    var index = 0;
    while(true) {
      yield index++;
    }
  }
}
const g = obj.indexGenerator();
console.log(g.next().value); // => 0
console.log(g.next().value); // => 1
复制代码

三种生成器函数都返回生成器对象g,以后g用于生成一系列自增数字。

6. 还有一件事:new Function

JavaScript中,函数是一类对象——函数是function类型的常规对象。

上面描述的声明方法建立了相同的函数对象类型。咱们来看一个例子:

function sum1(a, b) {
  return a + b;
}
const sum2 = function(a, b) {
  return a + b;
}
const sum3 = (a, b) => a + b;
console.log(typeof sum1 === 'function'); // => true
console.log(typeof sum2 === 'function'); // => true
console.log(typeof sum3 === 'function'); // => true
复制代码

函数对象类型有一个构造函数:Function

Function被做为构造函数调用时,new Function(arg1, arg2,…,argN,bodyString),将建立一个新函数。参数arg1, args2,…, argN传递给构造函数成为新函数的参数名,最后一个参数bodyString用做函数体代码。

让咱们建立一个函数,两个数字的和:

const numberA = 'numberA', numberB = 'numberB';
const sumFunction = new Function(numberA, numberB, 
   'return numberA + numberB'
);
sumFunction(10, 15) // => 25
复制代码

使用Function构造函数调用建立的sumFunction具备参数numberAnumberB,而且主体返回numberA + numberB

以这种方式建立的函数不能访问当前做用域,所以没法建立闭包,所以它们老是在全局域内建立。

在浏览器或NodeJS脚本中访问全局对象的更好方式是new Function的应用:

(function() {
   'use strict';
   const global = new Function('return this')();
   console.log(global === window); // => true
   console.log(this === window);   // => false
})();
复制代码

请记住,几乎不应该使用new Function()来声明函数。由于函数体是在运行时执行的,因此这种方法继承了许多eval()使用问题:安全风险、更难调试、没法应用引擎优化、没有编辑器自动完成。

7. 最后,哪一种方法更好?

没有赢家,也没有输家。选择哪一种声明类型取决于具体状况。

然而,在一些常见的状况下,你能够遵循一些规则。

若是函数从封闭的函数中使用this,则箭头函数是一个很好的解决方案。当回调函数有一个短语句时,箭头函数也是一个不错的选择,由于它建立了短而轻的代码。

在对象常量上声明方法时要使用更短的语法,简写方法声明更可取。

正常状况不该该使用new Function方法来声明函数。主要是由于它打开了潜在的安全风险,不容许编辑器中的代码自动完成同时也不容许引擎优化。

相关文章
相关标签/搜索