一个函数一次性定义的代码块可以多次调用。在JavaScript中,一个函数有很多元素组成,同时也受很多元素影响:
- 函数体的代码
- 函数的参数列表
- 接受外部变量域的变量
- 返回值
- 当函数被调用时,
this
指上下文 - 命名和匿名函数
- 函数对象作为变量声明
arguments
对象(在ES6中的箭头函数中将丢弃这个)
这些元素都会影响到函数,但具体影响函数的行为还是取决于函数的声明类型。在JavaScript中常见的声明类型有以下几种方法:
函数声明类型对函数代码的影响只是轻微的。重要的是函数如何与外部组件交互功能(比如外部作用域、闭包、对象自身拥有的方法等)和调用方式(普通函数调用、方法调用和构造函数调用等)。
例如,你需要通过this
在一个函数调用封闭的下下文(即this
从外部函数继承过来)。最好的选择是使用箭头函数,很清楚的提供了必要的下下文。
比如下面示例:
class Names {
constructor (names) {
this.names = names;
}
contains(names) {
return names.every((name) => this.names.indexOf(name) !== -1);
}
}
var countries = new Names(['UK', 'Italy', 'Germany', 'France']);
countries.contains(['UK', 'Germany']); // => true
countries.contains(['USA', 'Italy']); // => false
箭头函数传给.every()
的this
(一个替代Names
类)其实就是一个contains()
方法。使用一个箭头(=>
)来声明一个函数是最适当的声明方式,特别是在这个案例中,上下文需要继承来自外部的方法.contains()
。
如果试图使用一个函数表达式来调用.every()
,这将需要更多的手工去配置上下文。有两种方式,第一种就是给.every(function(){...}, this)
第二个参数,来表示上下文。或者在function(){...}.bind(this)
使用.bind()
作为回调函数。这是额外的代码,而箭头函数提供的上下文透明度更容易让人理解。
这篇文章介绍了如何在JavaScript中声明一个函数的六种方法。每一种类型都将会通过简短代码来阐述。感偿趣?
函数声明(Function declaration)
函数声明通过关键词function
来声明,关键词后面紧跟的是函数的名称,名称后面有一个小括号(()
),括号里面放置了函数的参数(para1,...,paramN)
和一对大括号{...}
,函数的代码块就放在这个大括号内。
function name([param,[, param,[..., param]]]) {
[statements]
}
来看一个函数声明的示例:
// function declaration
function isEven (num) {
return num % 2 === 0;
}
isEven(24); // => true
isEven(11); // => false
function isEven(num) {...}
是一个函数声明,定义了一个isEven
函数。用来判断一个数是不是偶数。
函数声明创建了一个变量,在当前作用域,这个变量就是函数的名称,而且是一个函数对象。这个函数变量存在变量生命提升,它会提到当前作用域的顶部,也就是说,在函数声明之前可以调用。
函数声明创建的函数已经被命名,也就是说函数对的name
属性就是他声明的名称。在调试或者错误信息阅读的时候,其很有用。
下面的示例,演示了这些属性:
// Hoisted variable
console.log(hello('Aliens')); // => 'Hello Aliens!'
// Named function
console.log(hello.name); // => 'hello'
// Variable holds the function object
console.log(typeof hello); // => 'function'
function hello(name) {
return `Hello ${name}!`;
}
函数声明function hello(name) {...}
创建了一个hello
变量,并且提升到当前作用域最顶部。hello
变量是一个函数对象,以及hello.name
包括了函数的名称hello
。
一个普通函数
函数声明匹配的情况应该是创建一个普通函数。普通的意思意味着你声明的函数只是一次声明,但在后面可以多次调用它。它下的示例就是最基本的使用场景:
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()
函数做递归计算时调用了开始声明的函数,将函数当作一个变量:factorial(n - 1)
。当然也可以使用一个函数表达式,将其赋值给一个普能的变量,比如:var factorial = function (n) {...}
。但函数声明function factorial(n)
看起来更紧凑(不需要var
和=
)。
函数声明的一个重要属性是它的提升机制。它允许在相同的作用域范围内之前使用声明的函数。提升机制在很多情况下是有用的。例如,当你一个脚本内先看到了被调用的函数,但又没有仔细阅读函数的功能。而函数的功能实现可以位于下面的文件,你甚至都不用滚动代码。
你可以在这里了解函数声明的提升机制。
与函数表达式区别
函数声明和函数表达式很容易混淆。他们看起来非常相似,但他们具有不同的属性。
一个容易记住的规则:函数声明总是以function
关键词开始,如果不是,那它就是一个函数表达式。
下面就是一个函数声明的示例,声明是以function
关键词开始:
// Function declaration: starts with "function"
function isNil(value) {
return value == null;
}
函数表达式不是以function
关键词开始(目前都一般出现在代码的中间地方):
// Function expression: starts with "var"
var isTruthy = function(value) {
return !!value;
};
// Function expression: an argument for .filter()
var numbers = ([1, false, 5]).filter(function(item) {
return typeof item === 'number';
});
// Function expression (IIFE): starts with "("
(function messageFunction(message) {
return message + ' World!';
})('Hello');
条件中的函数声明
当函数声明出现if
、for
或while
这样的条件语句块{...}
时,在一些JavaScript环境内可能会抛出一个引用错误。让我们来看看在严格模式下,函数声明出现在一个条件语句块中,看看会发生什么。
(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 is not defined"
,因为函数声明出现在一个条件语句块内。注意,这种情况适用于非严格模式环境下,这让人更感到困惑。
一般来说,在这样的情况之下,当一个函数应该创建在基于某些条件内时,应该使用一个函数表达式,而不应该使用函数声明。比如下面这个示例:
(function() {
'use strict';
var 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()
函数也能正常工作,不会抛出任何错误。
函数表达式
函数表达式是由一个function
关键词,紧随其后的是一个可选的函数名,一串参数(para1,...,paramN)
放在小括号内和代码主体放在大括号内{...}
。
一些函数表达式的使用方法:
var count = function(array) { // Function expression
return array.length;
}
var methods = {
numbers: [1, 5, 8],
sum: function() { // Function expression
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中经常使用。大多数的时候,开发人员处理这种类型的函数,喜欢使用箭头函数。
命名函数表达式
当函数没有一个名称(名称属性是一个空字符串)时这个函数是一个匿名函数。
var getType = function(variable) {
return typeof variable;
};
getType.name // => ''
getType
就是一个匿名函数,其getType.name
的值为''
。
当表达式指定了一个名称时,这就是一个命名函数表达式。它和简单的函数表达式相比具有一些额外的属性。
- 创建一个命名函数,其
name
属性就是函数名 - 在函数体中具有和函数对象相同名称的一个变量
我们使用上面的例子,不同的是在函数表达式内指定了一个名称:
var 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 === 'function'); // => false
function funName(variable) {...}
是一个命名函数表达式。在函数作用范围内存一个funName
变量。函数对象的name
属性就是函数的名称funName
。
支持命名函数表达式
当变量赋值时使用一个函数表达式var fun = function() {}
,很多引擎可以推断这个变量的函数名。回调时常常给其传递的是一个匿名函数表达式,并没有存储到变量中,所以引擎不能确定它的名字。
在很多情况之下,使用命名函数和避免匿名函数似乎是很在理的。而且这也会带来一系列的好处:
- 在调试时,错误信息和调用堆栈时使用函数名能显示更详细的信息
- 调试时更舒服,可以减少
anonoymous
堆栈的名字出现的次数 - 函数名有助于快速理解其功能
- 在函数递归调用的范围内或事件监听时可以按名称来访问函数
方法定义
方法定义可以在object literals和ES6 class时定义。可以使用一个函数的名称,并紧随其后跟一对小括号放置参数列表(para1,...,paramN)
和函数主体代码放在一个大括内{...}
。
下面的示例是基于object literals上使用方法定义函数。
var collection = {
items: [],
add(...items) {
this.items.push(...items);
},
get(index) {
return this.items[index];
}
};
collection.add('C', 'Java', 'PHP');
collection.get(1) // => 'Java'
add()
和get()
方法在collection
对象使用方法定义。这些方法可以像这样调用collection.add(...)
和collection.get(...)
。
方法定义和传统的属性定义有点类似,通一个冒号:
把名称和函数表达式连接在一起,比如add:function(...) {...}
。
- 更短的语法更易读和写
- 方法定义创建命名函数,和函数表达式刚好相反。有利于用于调试
注意,使用class
语法需要短形式方法来声明:
class Star {
constructor(name) {
this.name = name;
}
getMessage(message) {
return this.name + message;
}
}
var sun = new Star('Sun');
sun.getMessage(' is shining') // => 'Sun is shining'
计算属性名和方法
ES6中增加了一个很好的特性:在object literals和class中可以计算属性。
计算属性的方法和[methodNmae(){...}]
略有不同,其定义的方法这样的:
var addMethod = 'add',
getMethod = 'get';
var 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](...) {...}
使用了计算属性名快速方法声明。
箭头函数
箭头函数的定义是使用一对小括号,括号内是一系列的参数(param1,param2,...,paramN)
,后面紧跟=>
符号和{...}
,代码主体放置在这对大括号内。
当箭头函数只有一个参数时,可以省略这对小括号,另外它只包含一个声明时,大括号都可以省略。
下面的示例就是一个箭头函数的基本用法:
var absValue = (number) => {
if (number < 0) {
return -number;
}
return number;
}
absValue(-10); // => 10
absValue(5); // => 5
absValue
是一个箭头函数,这个函数主要功能就是计算一个数的绝对值。
函数声明使用箭头函数,其中=>
具有以下属性:
- 箭头函数不创建执行自己的上下文(函数表达式或函数声明式相反,创建不创建取决于
this
的调用) - 箭头函数是一个匿名函数:
name
是一个空字符串''
(函数声明式相反,它有一个名字) arguments
对象不可使用箭头函数(与其它声明类型相反,其他类型提供arguments
对象)
Context transparency
this
关键词的使用在JavaScript中让很多同学都感到困惑。(这篇文章详细介绍了this
关键词的使用)。
因为函数创建了自己的可执行的上下文(execution context),这也造成一般情况很难确定this
所指。
ES6引用箭头函数改善了这种用法(context lexically)。这是一个很好的特性,因为从现在开始函数需要封闭的上下文时没有必要使用.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);
};
}
}
var numbersObject = new Numbers([]);
numbersObject.addNumber(1);
var addMethod = numbersObject.addNumber();
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);
//...
或者将上下文(context)存给一个变量var self = this
:
//...
var self = this;
return function(number) {
console.log(self === numbersObject); // => true
self.array.push(number);
};
//...
context transparency这个属性可以让你在一个封闭的环境内任意使用this
。
短回调
前面也说过了,当创建的箭头函数只有一个参数,或者主体只有一个声明时,小括号()
和花括号{}
都可以省去。这有助于创建一个非常短的回调函数。
让我们创建一个函数,如果数组只有0
这个元素,将它找出来。
var numbers = [1, 5, 10, 0];
numbers.some(item => item === 0); // => true
item => item === 0
是一个箭头函数,它看上去非常简单。
有时候嵌套短的箭头函数会让代码阅读起来增加困难。所以最方便的方式是当这它是一个回调函数(没有嵌套)可以使用短的箭头函数方式。如果有必要,添加花括号之来,这样有利于代码的阅读。
函数生成器
生成函数在JavaScript中会返回一个Generator对象。其语法类似于函数表达式、函数声明式和方法声明,不同的是,它需要在function
后添加一个*
符号。
生成器函数可以按以下这些方式来声明函数:
函数声明function* <name>()
:
function* indexGenerator() {
var index = 0;
while(true) {
yield index++;
}
}
var g = indexGenerator();
console.log(g.next().value); // => 0
console.log(g.next().value); // => 1
函数表达式function* ()
:
var indexGenerator = function* () {
var index = 0;
while(true) {
yield index++;
}
};
var g = indexGenerator();
console.log(g.next().value); // => 0
console.log(g.next().value); // => 1
方法生成*<name>
:
var obj = {
*indexGenerator() {
var index = 0;
while(true) {
yield index++;
}
}
}
var g = obj.indexGenerator();
console.log(g.next().value); // => 0
console.log(g.next().value); // => 1
上面三种方式生成的函数都会返回一个生成器对象g
。然后g
可以生成一系列的数字。
函数构造器: new Function
在JavaScript函数中第一个类(class object)对象: 函数是一个普通的对象类型是function
。
这种声明的方式创建相同的函数对象类型,来看一个示例:
function sum1(a, b) {
return a + b;
}
var sum2 = function(a, b) {
return a + b;
}
var sum3 = (a, b) => a + b;
console.log(typeof sum1 === 'function'); // => true
console.log(typeof sum2 === 'function'); // => true
console.log(typeof sum3 === 'function'); // => true
函数对象类型有一个构造器(constructor):Function
。
当Function
当作构造器(constructor)new Function(arg1,arg2,...,argN,bodyString)
,那么Function
构造器会创建一个新的 Function
对象(new Function
)。其中参数arg1,arg2,...,argN
会传递给构造器(constructor)成为新函数的参数,而且最后一个参数bodyString
用作函数体代码。
来看一个示例,创建一个函数,求两个数的和:
var numberA = 'numberA', numberB = 'numberB';
var sumFunction = new Function(numberA, numberB,
'return numberA + numberB'
);
sumFunction(10, 15) // => 25
sumFunction
创建的Function
构造器调用了numberA
和numberB
两个参数,并且在函数主体内执行return numberA + numberB
。
这种方式创建的函数不能访问当前的作用域,因为没办法创建闭包。他们总是在全局作用域内创建的。
一个可能就用new Function
的最佳方式是浏览器或NodeJs脚本访问一个全局对象:
(function() {
'use strict';
var global = new Function('return this')();
console.log(global === window); // => true
console.log(this === window); // => false
})();
如种方式最好
没有孰好孰坏,函数的声明类型的决定要视实际情况而定。但有一些规则还是值得大家一起遵循。
如果要在一个闭包内使用this
,那么箭头函数是一个很好的解决方案。另外回调函数是一个简短声明时,箭头函数也是一个很好的选择,因为它的代码短。
当在object literals上需要一个更短的语法时,方法声明是可取的。
new Function
这种方法一般不用来声明函数。主要因为它存在很多问题。
我认为这篇文章另一个作用是让大家写出更具可读性的代码,和减少函数使用的bug。因为他们像细胞一样存在任何一个应用程序当中。
本文根据@Dmitri Pavlutin的《Six ways to declare JavaScript functions》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:https://rainsoft.io/6-ways-to-declare-javascript-functions/。
如需转载,烦请注明出处:http://www.w3cplus.com/javascript/6-ways-to-declare-javascript-functions.html