var 函数名称 = function() {} vs function 函数名称() {}
我最近开始维护其他人的 JavaScript 代码。我正在修复错误、添加功能,同时也试图整理代码并使其更加一致。
之前的开发人员使用了两种声明函数的方式,我不知道这背后是否有原因。
这两种方式是:
var functionOne = function() {
// Some code
};
以及
function functionTwo() {
// Some code
}
使用这两种不同方法的原因是什么,每种方法的优缺点是什么?是否有一种方法可以做而另一种方法不能做的事情?
区别在于
functionOne
是一个
函数表达式
,因此仅在到达该行时才定义,而
functionTwo
是一个
函数声明
,并且在其周围的函数或脚本执行时立即定义(由于
提升
)。
例如,一个函数表达式:
// TypeError: functionOne is not a function
functionOne();
var functionOne = function() {
console.log("Hello!");
};
以及一个函数声明:
// Outputs: "Hello!"
functionTwo();
function functionTwo() {
console.log("Hello!");
}
从历史上看,在块内定义的函数声明在不同的浏览器之间的处理不一致。 严格模式 (在 ES5 中引入)通过将函数声明的范围限定在其封闭块内解决了这个问题。
'use strict';
{ // note this block!
function functionThree() {
console.log("Hello!");
}
}
functionThree(); // ReferenceError
首先我想纠正一下 Greg:
function abc(){} 也是有作用域的 — — 名称
abc
在遇到此定义的范围内定义。示例:
function xyz(){
function abc(){};
// abc is defined here...
}
// ...but not here
其次,可以结合两种样式:
var xyz = function abc(){};
xyz
将照常定义,
abc
在除 Internet Explorer 之外的所有浏览器中均未定义 — — 不要依赖于它的定义。但它将在函数体内定义:
var xyz = function abc(){
// xyz is visible here
// abc is visible here
}
// xyz is visible here
// abc is undefined here
如果要在所有的浏览器上为函数添加别名,请使用这种声明:
function abc(){};
var xyz = abc;
在本例中,
xyz
和
abc
都是同一对象的别名:
console.log(xyz === abc); // prints "true"
使用组合样式的一个令人信服的理由是函数对象的“name”属性( Internet Explorer 不支持 )。基本上,当您定义一个像
function abc(){};
console.log(abc.name); // prints "abc"
这样的函数时,它的名称会自动分配。但是当您像
var abc = function(){};
console.log(abc.name); // prints ""
这样定义它时,它的名称为空 — 我们创建了一个匿名函数并将其分配给某个变量。
使用组合样式的另一个好理由是使用简短的内部名称来引用自身,同时为外部用户提供一个长的不冲突的名称:
// Assume really.long.external.scoped is {}
really.long.external.scoped.name = function shortcut(n){
// Let it call itself recursively:
shortcut(n - 1);
// ...
// Let it pass itself as a callback:
someFunction(shortcut);
// ...
}
在上面的例子中,我们可以对外部名称执行相同的操作,但是它太笨重(并且更慢)。
(引用自身的另一种方法是使用
arguments.callee
,它仍然相对较长,并且在严格模式下不受支持。)
从深层次来看,JavaScript 对这两个语句的处理方式不同。这是一个函数声明:
function abc(){}
abc
在此处在当前作用域内随处定义:
// We can call it here
abc(); // Works
// Yet, it is defined down there.
function abc(){}
// We can call it again
abc(); // Works
此外,它还通过
return
语句提升:
// We can call it here
abc(); // Works
return;
function abc(){}
这是一个函数表达式:
var xyz = function(){};
xyz
在此处从赋值点定义:
// We can't call it here
xyz(); // UNDEFINED!!!
// Now it is defined
xyz = function(){}
// We can call it here
xyz(); // works
函数声明与函数表达式是 Greg 所展示的差异的真正原因。
有趣的事实:
var xyz = function abc(){};
console.log(xyz.name); // Prints "abc"
就我个人而言,我更喜欢“函数表达式”声明,因为这样我可以控制可见性。当我定义函数时,如
var abc = function(){};
我知道我是在本地定义该函数。当我定义函数时,如
abc = function(){};
我知道我是在全局定义该函数,前提是我在作用域链中的任何地方都没有定义
abc
。这种定义风格即使在
eval()
中使用时也具有弹性。虽然定义
function abc(){};
取决于上下文,可能会让您猜测它实际定义的位置,尤其是在
eval()
的情况下 — 答案是:它取决于浏览器。
以下是创建函数的标准形式的概述: (最初是为另一个问题编写的,但在移入规范问题后进行了改编。)
术语:
- ES5 : ECMAScript 第 5 版 ,2009 年
- ES2015 : ECMAScript 2015 (也称为“ES6”)
快速列表:
-
函数声明
-
“匿名”
function
表达式 (尽管有这个术语,但有时会创建带有名称的函数) -
命名
function
表达式 -
访问器函数初始化程序 (ES5+)
-
箭头函数表达式 (ES2015+) (与匿名函数表达式一样,不涉及显式名称,但可以创建带有名称的函数)
-
对象初始化程序中的方法声明 (ES2015+)
-
class
中的构造函数和方法声明 (ES2015+)
函数声明
第一种形式是 函数声明 ,如下所示:
function x() {
console.log('x');
}
函数声明是
声明
;它不是语句或表达式。因此,您不必在其后跟
;
(尽管这样做是无害的)。
当执行进入其出现的上下文时,函数声明将被处理,
在
执行任何分步代码之前。它创建的函数被赋予一个适当的名称(在上面的示例中为
x
),并且该名称被放入声明出现的作用域中。
因为它是在相同上下文中的任何分步代码之前处理的,所以您可以执行以下操作:
x(); // Works even though it's above the declaration
function x() {
console.log('x');
}
直到 ES2015,规范才涵盖如果将函数声明放在控制结构(如
try
、
if
、
switch
、
while
等)中,JavaScript 引擎应该做什么,如下所示:
if (someCondition) {
function foo() { // <===== HERE THERE
} // <===== BE DRAGONS
}
并且由于它们是在运行分步代码 之前 处理的,因此很难知道当它们处于控制结构中时该做什么。
尽管直到 ES2015 才 指定 这样做,但它一个 允许的扩展 ,用于支持块中的函数声明。不幸的是(并且不可避免地),不同的引擎做了不同的事情。
从 ES2015 开始,规范说明了要做什么。实际上,它给出了三件独立的事情:
- 如果在松散模式下 不 在 Web 浏览器上,JavaScript 引擎应该做一件事
- 如果在松散模式下在 Web 浏览器上,JavaScript 引擎应该做其他事情
- 如果在 严格 模式下(无论是否在浏览器上),JavaScript 引擎应该做另一件事
松散模式的规则很棘手,但在 严格 模式下,块中的函数声明很容易:它们是块的本地函数(它们具有 块作用域 ,这也是 ES2015 中的新功能),并且它们被提升到块的顶部。所以:
"use strict";
if (someCondition) {
foo(); // Works just fine
function foo() {
}
}
console.log(typeof foo); // "undefined" (`foo` is not in scope here
// because it's not in the same block)
“匿名”
函数
表达式
第二种常见形式称为 匿名函数表达式 :
var y = function () {
console.log('y');
};
与所有表达式一样,它会在逐步执行代码时进行求值。
在 ES5 中,此函数没有名称(它是匿名的)。在 ES2015 中,如果可能,会通过从上下文推断来为函数分配一个名称。在上面的示例中,名称将是
y
。当函数是属性初始化器的值时,也会执行类似操作。 (有关发生这种情况的时间和规则的详细信息,请在
规范
中搜索
SetFunctionName
- 它出现在
各个地方
。)
命名
函数
表达式
第三种形式是 命名函数表达式 (“NFE”):
var z = function w() {
console.log('zw')
};
这创建的函数有一个适当的名称(在本例中为
w
)。与所有表达式一样,在代码的逐步执行中达到它时会对其进行评估。函数的名称
未
添加到表达式出现的作用域中;名称
在
函数本身的范围内:
var z = function w() {
console.log(typeof w); // "function"
};
console.log(typeof w); // "undefined"
请注意,NFE 经常是 JavaScript 实现的错误来源。例如,IE8 及更早版本处理 NFE 完全错误 ,在两个不同的时间创建两个不同的函数。Safari 的早期版本也存在问题。好消息是,当前版本的浏览器(IE9 及更高版本、当前 Safari)不再存在这些问题。(但遗憾的是,截至撰写本文时,IE8 仍在广泛使用,因此在 Web 代码中使用 NFE 仍然存在问题。)
访问器函数初始化程序 (ES5+)
有时函数可能会悄悄潜入而不被注意; 访问器函数 就是这种情况。以下是示例:
var obj = {
value: 0,
get f() {
return this.value;
},
set f(v) {
this.value = v;
}
};
console.log(obj.f); // 0
console.log(typeof obj.f); // "number"
请注意,当我使用该函数时,我没有使用
()
!这是因为它是属性的
访问器函数
。我们以正常方式获取和设置属性,但在后台,该函数被调用。
您还可以使用
Object.defineProperty
、
Object.defineProperties
和
Object.create
的鲜为人知的第二个参数创建访问器函数。
箭头函数表达式 (ES2015+)
ES2015 为我们带来了 箭头函数 。以下是示例:
var a = [1, 2, 3];
var b = a.map(n => n * 2);
console.log(b.join(", ")); // 2, 4, 6
请注意
n => n * 2
的东西隐藏在
map()
调用中?那是一个函数。
关于箭头函数的几点说明:
-
它们没有自己的
this
。相反,它们 封闭 定义它们的上下文的this
。(它们还封闭arguments
和(如果相关)super
。)这意味着它们中的this
与创建它们的this
相同,并且无法更改。 -
正如您在上面注意到的那样,您不使用关键字
function
;而是使用=>
。
上面的
n => n * 2
示例是其中一种形式。如果您有多个参数要传递给函数,则使用括号:
var a = [1, 2, 3];
var b = a.map((n, i) => n * i);
console.log(b.join(", ")); // 0, 2, 6
(请记住
Array#map
将条目作为第一个参数传递,将索引作为第二个参数传递。)
在这两种情况下,函数主体只是一个表达式;函数的返回值将自动为该表达式的结果(您不使用显式的
return
)。
如果您要执行的不仅仅是一个表达式,请像平常一样使用
{} 和显式的
return
(如果您需要返回一个值):
var a = [
{first: "Joe", last: "Bloggs"},
{first: "Albert", last: "Bloggs"},
{first: "Mary", last: "Albright"}
];
a = a.sort((a, b) => {
var rv = a.last.localeCompare(b.last);
if (rv === 0) {
rv = a.first.localeCompare(b.first);
}
return rv;
});
console.log(JSON.stringify(a));
没有
{ ... } 的版本称为带有
表达式主体
或
简洁主体
的箭头函数。(也称为:
简洁
箭头函数。)带有
{ ... } 定义主体的版本是带有
函数主体
的箭头函数。 (另见:
详细
箭头函数。)
对象初始化器中的方法声明 (ES2015+)
ES2015 允许使用更短的形式声明引用函数的属性,这称为 方法定义 ;它如下所示:
var o = {
foo() {
}
};
ES5 及更早版本中几乎等同的形式为:
var o = {
foo: function foo() {
}
};
区别(除了详细程度以外)在于方法可以使用
super
,但函数不能。因此,例如,如果您有一个使用方法语法定义(例如)
valueOf
的对象,它可以使用
super.valueOf()
来获取
Object.prototype.valueOf
将返回的值(在可能对其进行其他操作之前),而 ES5 版本则必须执行
Object.prototype.valueOf.call(this)
。
这也意味着该方法具有对其定义对象的引用,因此如果该对象是临时的(例如,您将它作为源对象之一传递到
Object.assign
),方法语法
可能
意味着该对象保留在内存中,否则它可能会被垃圾回收(如果 JavaScript 引擎没有检测到这种情况并处理它,如果没有方法使用
super
)。
class
中的构造函数和方法声明(ES2015+)
ES2015 为我们带来了
class
语法,包括声明的构造函数和方法:
class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
getFullName() {
return this.firstName + " " + this.lastName;
}
}
上面有两个函数声明:一个是构造函数,它获取名称
Person
,一个是
getFullName
,它是分配给
Person.prototype
的函数。