我最近开始维护其他人的 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!");
}
这也意味着您无法使用函数声明有条件地定义函数:
if (test) {
// Error or misbehavior
function functionThree() { doSomething(); }
}
上面实际上定义了functionThree
而不管test
的值 - 除非use strict
有效,在这种情况下它只会引发错误。
首先,我想纠正 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"
使用组合样式的一个令人信服的理由是函数对象的 “名称” 属性( 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
here 在当前范围内的任何位置定义:
// 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()
的情况下 - 答案是:它取决于浏览器。
这是创建函数的标准表单的纲要:( 最初是为另一个问题编写的,但在被移入规范问题后进行了调整。)
条款:
快速清单:
功能声明
“匿名” 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,规范没有涵盖 JavaScript 引擎应该做什么,如果你把一个函数声明放在一个控制结构,如try
, if
, switch
, while
等,如下所示:
if (someCondition) {
function foo() { // <===== HERE THERE
} // <===== BE DRAGONS
}
而且由于它们是在逐步运行代码之前进行处理的,所以当它们处于控制结构中时知道该怎么做是很棘手的。
尽管在 ES2015 之前没有指定这样做,但它是一个允许的扩展来支持块中的函数声明。不幸的是(并且不可避免地),不同的引擎做了不同的事情。
从 ES2015 开始,规范说明了该怎么做。事实上,它提供了三个单独的事情:
松散模式的规则很棘手,但在严格模式下,块中的函数声明很容易:它们是块的本地(它们具有块范围 ,这在 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)
function
表达第二种常见形式称为匿名函数表达式 :
var y = function () {
console.log('y');
};
与所有表达式一样,它是在逐步执行代码时达到的。
在 ES5 中,它创建的函数没有名称(它是匿名的)。在 ES2015 中,如果可能,通过从上下文推断该函数来为该函数指定名称。在上面的示例中,名称将为y
。当函数是属性初始值设定项的值时,会执行类似的操作。 (有关何时发生这种情况的细节和规则,搜索SetFunctionName
在规范 - 它似乎所有的地方。)
function
表达式第三种形式是命名函数表达式 (“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 仍然广泛使用,因此使用 NFE 和 Web 代码一般仍然存在问题。)
有时功能可以潜入大部分未被注意到; 这是访问者功能的情况。这是一个例子:
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 为我们带来了箭头功能 。这是一个例子:
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
示例是它们的一种形式。如果您有多个参数来传递函数,则使用 parens:
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 允许更短的形式声明引用函数的属性; 它看起来像这样:
var o = {
foo() {
}
};
ES5 及更早版本中的等价物是:
var o = {
foo: function foo() {
}
};
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
的函数。