声明函数的六种方式
# 声明函数的六种方式
# 前言
在 JavaScript 中,函数由许多组件组成并受它们影响:
- 函数体的代码
- 参数列表
- 可以从词法作用域访问的变量
- 返回值
- 调用该函数时的上下文
this
- 命名函数或匿名函数
- 保存函数对象的变量
- arguments 对象(在箭头函数中没有)
本文介绍六种声明 JavaScript 函数的方式,分别介绍他们的声明语法、示例和常见的陷阱。并总结在特定的情况下何时使用特定的函数类型。
# 函数声明
# 基本语法
函数声明由 function
关键字、必需的函数名、一对括号中的参数列表 (param1, …, paramN)
和一对包裹着主体代码的花括号 { … }
组成。
示例:
function name(param1, …, paramN) {
statements
}
2
3
- name:函数名,必须有。
- param:要传递给函数的参数的名称。不同引擎中的最大参数数量不同。
- statements:包含函数体的语句。
# 变量提升
函数声明会在当前作用域内创建一个变量,它的标识符就是函数名,它的值就是函数对象。
我们都知道变量提升,对于函数而言,它会被提升到当前作用域的顶层,因此在编写代码时,可以在函数声明前就进行调用。
函数声明的一个重要特性是它的变量提升机制,也就是同一作用域内允许在声明之前被调用。
变量提升在某些场景下是很有用的。例如,你想在一个 JavaScript 脚本的开头就知道某个函数是如何被调用的,并不关心这个函数的具体实现。那么就可以把函数具体实现放在文件的下面,这样在阅读时不用滚到底部查看。
此时创建的函数是一个具名函数,函数对象的 name
属性值就是它的名称。当你需要查看堆栈信息进行调试和查错时,该属性会非常有用。
可以通过下面的例子来理解:
// 变量提升
console.log(hello('Aliens')); // "Hello Aliens!"
// 命名的函数
console.log(hello.name); // "hello"
// 变量保存了函数对象
console.log(typeof hello); // "function"
function hello(name) {
return `Hello ${name}!`;
}
2
3
4
5
6
7
8
9
10
# 适用场景
函数声明适用于常规函数:即某个函数你只需声明一次,并在多个地方调用它。
示例:
function sum(a, b) {
return a + b;
}
sum(5, 6); // 11
([3, 7]).reduce(sum) // 10
2
3
4
5
6
由于函数声明在当前作用域中创建了一个变量,同时还是一个常规函数调用,所以它对于递归或分发事件监听器非常有用。与函数表达式或箭头函数相反,它不通过函数变量的名称创建绑定。
例如,要递归计算阶乘,就必须访问的函数:
function factorial(n) {
if (n === 0) {
return 1;
}
return n * factorial(n - 1);
}
factorial(4); // 24
2
3
4
5
6
7
在 factorial()
函数内部通过再次使用函数变量名 factorial(n - 1)
实现递归调用。
上述需求也可以通过把一个函数表达式赋值给一个常规变量实现,例如:
const factorial = function(n) { … }
但是函数声明 function factorial(n)
更简洁,因为不需要写 const
和 =
。
# 和函数表达式的区别
函数声明和函数表达式很容易混淆。它们看起来非常相似,但产生的函数具有不同的属性。
一个容易记住的规则:函数声明总是以关键字 function
开头,否则它就是一个函数表达式。
下面的示例是一个函数声明,它的语句以 function
关键字开头:
// 函数声明: 以 "function" 开始
function isNil(value) {
return value == null;
}
2
3
4
在使用函数表达式的情况下,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');
2
3
4
5
6
7
8
9
10
11
12
# 条件语句中的函数声明
一些 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"
})();
2
3
4
5
6
7
8
9
10
11
12
13
14
当调用 ok()
时,JavaScript 抛出 ReferenceError: ok is not defined
,因为函数声明在一个条件块中。
条件语句中的函数声明在非严格模式下是允许的,但这使得代码很混乱。
作为这些情况的一般规则,当函数应该在某些条件下才创建时,我们可以使用函数表达式。让我们看看如何处理:
(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"
})();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
因为函数是一个常规对象,所以根据条件将它赋给一个变量。调用 ok()
工作正常,没有错误。
# 函数表达式
# 基本语法
函数表达式由 function
关键字、可选的函数名、一对括号中的参数列表 (param1, …, paramN)
和一对包裹着主体代码的花括号 { … }
组成。
示例:
let function_expression = function [name](param1, …, paramN]) {
statements
}
2
3
- function_expression:被赋值的变量名。
- name:函数名称,可被省略,此种情况下的函数是匿名函数(anonymous)。函数名称只是函数体中的一个本地变量。
- statements:包含函数体的语句。
# 变量提升
注意
没有变量提升
JavaScript 中的函数表达式没有提升,不像函数声明,你在定义函数表达式之前不能使用函数表达式。
示例:
notHoisted(); // TypeError: notHoisted is not a function
var notHoisted = function() {
console.log('bar');
};
2
3
4
5
# 适用场景
函数表达式创建了一个可以在不同情况下使用的函数对象:
- 作为对象赋值给变量
count = function(…){…}
- 在对象上创建一个方法
sum: function(){…}
- 使用函数作为回调
reduce(function(…){…})
- 在函数体内部引用当前函数(递归调用),此时一定要是一个命名函数表达式
// 作为对象赋值给变量
const count = function(array) {
return array.length;
};
const methods = {
numbers: [1, 5, 8],
sum: function() { // 在对象上创建一个方法
return this.numbers.reduce(function(acc, num) { // 使用函数作为回调
return acc + num;
});
}
};
var math = {
'factorial': function factorial(n) {
if (n <= 1)
return 1;
return n * factorial(n - 1); // 在函数体内部引用当前函数
}
};
count([5, 7, 8]); // 3
methods.sum(); // 14
math.factorial(3); // 6
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
函数表达式是 JavaScript 中最常用的部分,通常情况下,如果你喜欢简短的语法和词法上下文,就能经常看到函数表达式和箭头函数。
# 匿名函数表达式
函数表达式中,当它没有函数名称的时侯,则称为匿名函数(平时所说的匿名函数就属于函数表达式),此时 name
属性是一个空字符 ''
。
示例:
(
function(variable) {return typeof variable; }
).name; // ''
2
3
被函数表达式赋值的那个变量会有一个 name
属性,如果你把这个变量赋值给另一个变量的话,这个 name
属性的值也不会改变。
如果函数是一个匿名函数,那 name
属性的值就是被赋值的变量的名称(隐式命名)。如果函数不是匿名的话,那 name
属性的值就是这个函数的名称(显式命名)。这对于箭头函数也同样适用(箭头函数没有名字,所以 name
属性的值就是被赋值的变量的名称)。
示例:
var foo = function() {}
foo.name // "foo"
var bar = foo
bar.name // "foo"
console.log(foo === bar); //true
2
3
4
5
6
7
# 命名函数表达式
当表达式指定了名称时,就是命名函数表达式。与简单的函数表达式相比,它有一些额外的特点:
name
属性就是函数名。- 在函数体内部,与函数同名的变量指向函数对象。
- 在命名函数表达式中,函数名称可以在函数作用域内访问,但不能在外部访问。
var bar = function foo() {}
console.log(typeof foo) // "undefined"
console.log(foo.name) // ReferenceError: foo is not defined
console.log(typeof bar) // "function"
console.log(bar.name) // "foo"
2
3
4
5
6
7
推荐命名函数和避免匿名函数可以获得以下好处:
- 显式指定函数名时,错误消息和调用堆栈显示更详细的信息。
- 通过减少匿名堆栈名称的数量,使调试更加舒适。
- 从函数名可以看出函数的作用。
- 可以在函数的作用域内访问函数,以进行递归调用或分发事件侦听器。
# 方法的定义
从 ECMAScript 2015 开始,在对象初始化中引入了一种更简短定义方法的语法,这是一种把方法名直接赋给函数的简写方式,可用于对象常量和 ES2015 类的方法声明。
# 基本语法
你可以使用函数名来定义它们,后面跟着一对括号中的参数列表 (param1, …, paramN)
和一对包裹着主体代码的花括号 { … }
。
示例:
const obj = {
foo() {
return 'bar';
}
};
console.log(obj.foo()); // "bar"
2
3
4
5
6
7
# 适用场景
封装对象方法:
const collection = {
items: [],
add(...items) {
this.items.push(...items);
},
get(index) {
return this.items[index];
}
};
collection.add('C', 'Java', 'PHP');
collection.get(1) // "Java"
2
3
4
5
6
7
8
9
10
11
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"
2
3
4
5
6
7
8
9
10
# 计算得到的属性名和方法
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"
2
3
4
5
6
7
8
9
10
11
12
13
[addMethod] (…) {…}
和 [getMethod](…){…}
是具有计算属性名的简写方法声明。
# 箭头函数
# 基本语法
箭头函数表达式的语法比函数表达式更简洁,它是用一对括号定义的,其中包含参数列表 (param1, param2, ……, paramN)
,然后是一个胖箭头 =>
和一对包裹着主体代码的花括号 { … }
。
并且没有自己的 this
,arguments
,super
或 new.target
。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。
当箭头函数只有一个参数时,可以省略括号。当它包含一个语句时,花括号也可以省略。
示例:
const absValue = (number) => {
if (number < 0) {
return -number;
}
return number;
};
absValue(-10); // 10
absValue(5); // 5
2
3
4
5
6
7
8
absValue
是一个计算数字绝对值的箭头函数。
# 上下文透明性
this
关键字是 JavaScript 的一个令人困惑的方面。因为函数创建自己的执行上下文,所以通常很难判断 this
的值。
ECMAScript 2015 通过引入箭头函数改进了 this
的用法,该函数按词法获取上下文(或者直接使用外部域的 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]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Numbers
类有一个数字数组,并提供 addNumber()
方法来插入新数值。
当在不提供参数的情况下调用 addNumber()
时,返回一个允许插入数字的闭包。这个闭包是一个 this
等于 numbersObject
实例的箭头函数,因为上下文是从 addNumbers()
方法按词法获取的。
如果没有箭头函数,就必须手动指定上下文,使用像 .bind()
这样的方式进行变通:
//...
return function(number) {
console.log(this === numbersObject); // true
this.array.push(number);
}.bind(this);
//...
2
3
4
5
6
或将上下文存储到一个单独的变量 var self = this
:
//...
const self = this;
return function(number) {
console.log(self === numbersObject); // true
self.array.push(number);
};
//...
2
3
4
5
6
7
当我们希望从封闭上下文中获取 this
时,就可以利用箭头函数的这种上下文透明性。
# 特点
箭头函数具有以下特点:
- 箭头函数不会创建它的执行上下文,而是按词法处理它(与函数表达式或函数声明相反,它们根据调用创建自己的
this
)。 - 箭头函数是匿名的。但是,可以从保存函数的变量推断出它的名称。
arguments
对象在箭头函数中不可用(与提供arguments
对象的其他声明类型相反)。但是,您可以自由地使用rest
参数(param1, param2, ...rest) => { statements }
。
# 短语法
在创建箭头函数时,对于单个参数和单个主体语句,括号对和花括号是可选的。这有助于创建非常简洁的回调函数。
让我们创建一个函数找出包含 0 的数组:
const numbers = [1, 5, 10, 0];
numbers.some(item => item === 0); // true
2
但要注意,嵌套的短箭头函数很难理解,推荐的短语法使用场景是单个回调(不嵌套)。
# 生成器函数
JavaScript 中的生成器函数返回生成器对象。它的语法类似于函数表达式、函数声明或方法声明,只是它需要一个星号 *
。
这里简单介绍一下,更推荐阅读阮一峰老师编写的《ES6标准入门(第3版)》中 Generator (opens new window) 相关的两个章节。
生成器函数的声明形式如下:
- 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
2
3
4
5
6
7
8
9
- 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
2
3
4
5
6
7
8
9
- 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
2
3
4
5
6
7
8
9
10
11
三种生成器函数都返回生成器对象 g
,之后 g
用于生成一系列自增数字。
# 构造函数
# 基本语法
new Function (param1, …, paramN, functionBody)
- param1, …, paramN:被函数使用的参数的名称,形式是一个有效的 JavaScript 标识符的字符串,或者一个用逗号分隔的有效字符串的列表。
- functionBody:一个含有包括函数定义的 JavaScript 语句的字符串。
# 实例用法
上面介绍的五种声明方法,本质上创建了相同的函数对象类型。我们来看一个例子:
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
2
3
4
5
6
7
8
9
10
函数对象类型有一个构造函数:Function
。
当 Function
被作为构造函数调用时,new Function(param1, param2, …, paramN, bodyString)
,将创建一个新函数。参数 param1, param2, …, paramN
传递给构造函数成为新函数的参数名,最后一个参数 bodyString
用作函数体代码。
让我们创建一个函数,两个数字的和:
const numberA = 'numberA', numberB = 'numberB';
const sumFunction = new Function(numberA, numberB,
'return numberA + numberB'
);
sumFunction(10, 15) // 25
2
3
4
5
使用 Function
构造函数调用创建的 sumFunction
具有参数 numberA
和 numberB
,并且主体返回 numberA + numberB
。
以这种方式创建的函数不能访问当前作用域,因此无法创建闭包,因此它们总是在全局域内创建。
在浏览器或 Node.js 脚本中访问全局对象的更好方式是 new Function
的应用:
(function() {
'use strict';
const global = new Function('return this')();
console.log(global === window); // true
console.log(this === window); // false
})();
2
3
4
5
6
请记住,几乎不应该使用 new Function()
来声明函数。因为函数体是在运行时执行的,所以这种方法继承了许多 eval()
使用问题:安全风险、更难调试、无法应用引擎优化、没有编辑器自动补全。
# 参考资料
- MDN - Function declaration (opens new window)
- MDN - Function expression (opens new window)
- MDN - Method definitions (opens new window)
- MDN - Arrow function (opens new window)
- MDN - Generator function (opens new window)
- MDN - Function constructor (opens new window)
- 6 Ways to Declare JavaScript Functions (opens new window)(酌情翻译)
(完)