6种声明JavaScript函数的方式
翻译:道奇 作者:Dmitri Pavlutin 原文: 6 Ways to Declare JavaScript Functions
函数是一段参数化的代码块,定义一次可多次调用。在JavaScript
中,函数由许多组件组成并受它们影响:
- 函数体的代码
- 参数列表
- 可以从词法作用域访问的变量
- 返回值
- 调用该函数时的上下文
this
- 命名函数或匿名函数
- 保存函数对象的变量
- arguments对象(在箭头函数中没有)
这篇文章讲述六种声明JavaScript
函数的方法:语法、示例和常见的陷阱。此外,您将了解在特定的情况下何时使用特定的函数类型。
1.函数声明
"函数声明由
function
关键字、必需的函数名、一对括号中的参数列表(para1,…, paramN)
和一对花括号{…}
分包裹着主体代码。"
函数声明的一个例子:
// 函数
function isEven(num) {
return num % 2 === 0;
}
isEven(24); // => true
isEven(11); // => false
function isEven (num) {…}
是定义了isEven函数的函数声明,isEven
函数用来判断数字是否是偶数。
函数声明在当前域内创建一个变量,它的标识符就是函数名,它的值就是函数对象。
函数变量被提升到当前顶层作用域,这意味着可以在函数声明前就进行调用(请继续参阅本章看更多的细节)。
创建的函数会被命名,函数对象的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
环境在调用一个出现在{…}
的if
、for
或while
语句中的声明时会抛出异常。
让我们启用严格模式,看看当一个函数声明在条件语句中:
(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
具有参数numberA
和numberB
,并且主体返回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
方法来声明函数。主要是因为它打开了潜在的安全风险,不允许编辑器中的代码自动完成同时也不允许引擎优化。
转载自:https://juejin.cn/post/6844904003541663752