最近我开始维护别人的JavaScript代码。我正在修复错误,添加功能,并尝试整理代码并使其更加一致。
之前的开发人员使用了两种声明函数的方式,我无法确定是否有什么原因。
这两种方式是:
var functionOne = function() {
// Some code
};
而且,
function functionTwo() {
// Some code
}
为什么要使用这两种不同的方法,各自有哪些优缺点?是否有一种方法可以做到另一种方法无法实现的事情?
最近我开始维护别人的JavaScript代码。我正在修复错误,添加功能,并尝试整理代码并使其更加一致。
之前的开发人员使用了两种声明函数的方式,我无法确定是否有什么原因。
这两种方式是:
var functionOne = function() {
// Some code
};
而且,
function functionTwo() {
// Some code
}
为什么要使用这两种不同的方法,各自有哪些优缺点?是否有一种方法可以做到另一种方法无法实现的事情?
functionOne
是一个函数表达式,因此只有在到达该行时才被定义,而functionTwo
是一个函数声明,在其所在的函数或脚本执行时就被定义(由于hoisting)。// TypeError: functionOne is not a function
functionOne();
var functionOne = function() {
console.log("Hello!");
};
而且,函数声明:
// Outputs: "Hello!"
functionTwo();
function functionTwo() {
console.log("Hello!");
}
'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 —— 不要依赖它的定义。但是它将在其body内部被定义:
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"
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
,这仍然相对较长,并且不支持严格模式。)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()
的情况下——答案是:这取决于浏览器。
abc
的值不是函数本身,而是该函数的返回值。 对于abc.name为空是有意义的,因为abc返回一个未命名的函数。 @ikirachen提到删除()
,因为这就是调用函数的方式。 没有它,它只是被多余的括号包裹。 - Sinjaivar
在括号内声明的变量将像通常一样在函数作用域中,但是该匿名函数不再可在其所包含的括号之外访问。幸运的是,现在我们有了 let
,它使用了普通(理智的)人所期望的块级作用域。在我看来,最好假装 var
不存在。 - Sinjai函数声明
"匿名" function
表达式 (尽管这个术语有时会创建带有名称的函数)
命名 function
表达式
访问器函数初始化器(ES5+)
箭头函数表达式(ES2015+)(与匿名函数表达式一样,不涉及显式名称,但可以创建具有名称的函数)
对象初始化器中的方法声明(ES2015+)
class
中的构造函数和方法声明(ES2015+)
第一种形式是 函数声明,它看起来像这样:
function x() {
console.log('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
}
"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中,函数this创建没有名称(它是匿名的)。在ES2015中,如果可能的话,从上下文推断出函数的名称。在上面的示例中,名称将是y
。当函数是属性初始化程序的值时,也会执行类似的操作。(有关何时发生这种情况以及规则的详细信息,请搜索the specification中的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"
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
的第二个参数来创建访问器函数。var a = [1, 2, 3];
var b = a.map(n => n * 2);
console.log(b.join(", ")); // 2, 4, 6
在map()
调用中隐藏的n => n * 2
是一个函数。
有关箭头函数的一些事情:
它们没有自己的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允许使用一种更短的形式来声明引用函数的属性,称为方法定义;它看起来像这样:
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
的函数。就全局上下文而言,无论是使用 var
语句还是在结尾处使用 FunctionDeclaration
都会在全局对象上创建一个 不可删除的 属性,但两者的值都可以被覆盖。
两种方式之间的微妙差异在于,在实际代码执行之前,当 变量实例化 过程运行时,使用 var
声明的所有标识符都将被初始化为 undefined
,而此时函数声明所使用的标识符将从那一刻开始可用,例如:
alert(typeof foo); // 'function', it's already available
alert(typeof bar); // 'undefined'
function foo () {}
var bar = function () {};
alert(typeof bar); // 'function'
bar
函数表达式的赋值是在运行时发生的。
由FunctionDeclaration
创建的全局属性可以像变量值一样被轻易覆盖,例如:
function test () {}
test = null;
另一个明显的区别是,第一个函数没有名称,而第二个函数有名称,在调试(即检查调用堆栈)时非常有用。foo = function() { alert('hello!'); };
),这是一个未声明的赋值语句,我强烈建议您始终使用var
关键字。var
语句的情况下进行赋值,如果在作用域链中找不到引用的标识符,它将成为全局对象的可删除属性。ReferenceError
错误。FunctionDeclaration
声明的标识符不能被覆盖,但事实并非如此。你发布的两个代码片段在大多数情况下将表现出相同的行为。
然而,它们之间的区别在于第一种方式(var functionOne = function() {}
)只能在该代码点之后调用该函数。
而使用第二个方式(function functionTwo()
),该函数可供运行在函数声明之上的代码使用。
这是因为在第一种方式中,函数在运行时分配给变量foo
。而在第二种方式中,函数在解析时分配给标识符foo
。
更多技术信息
JavaScript有三种定义函数的方法:
eval()
的工作方式相同,而eval()
存在其问题。对Greg回答的更好解释
functionTwo();
function functionTwo() {
}
为什么没有错误?我们一直被教导表达式是从上到下执行的(??)
JavaScript解释器总是将函数声明和变量声明(
hoisted
)不可见地移动到其所在作用域的顶部,函数参数和语言定义的名称显然已经在那里。 Ben Cherry
这意味着像这样的代码:
functionOne(); --------------- var functionOne;
| is actually | functionOne();
var functionOne = function(){ | interpreted |-->
}; | like | functionOne = function(){
--------------- };
请注意,声明中的赋值部分没有被提升,只有名称被提升。
但是对于函数声明,在整个函数体也会被提升:
functionTwo(); --------------- function functionTwo() {
| is actually | };
function functionTwo() { | interpreted |-->
} | like | functionTwo();
---------------
其他评论者已经讲述了上述两种变体的语义差异。我想指出一种风格上的不同:只有“赋值”版本才能设置另一个对象的属性。
我经常使用以下模式构建JavaScript模块:
(function(){
var exports = {};
function privateUtil() {
...
}
exports.publicUtil = function() {
...
};
return exports;
})();
使用这种模式,您的公共函数都将使用赋值,而私有函数则使用声明。
(还要注意的是,语句后的赋值应该需要分号,而声明则禁止使用分号。)
首选第一种方法而不是第二种方法的一个例子是当您需要避免覆盖函数先前的定义时。
使用
if (condition){
function myfunction(){
// Some code
}
}
这个myfunction
的定义会覆盖任何之前的定义,因为它是在解析时完成的。
而
if (condition){
var myfunction = function (){
// Some code
}
}
只有在满足condition
时,才能正确定义myfunction
的工作。
一个重要的原因是将一个且仅将一个变量作为你的命名空间的“根”...
var MyNamespace = {}
MyNamespace.foo= function() {
}
或者var MyNamespace = {
foo: function() {
},
...
}
有许多命名空间技术。随着大量可用的JavaScript模块,它变得越来越重要。
Hoisting 是 JavaScript 解释器将所有变量和函数声明移动到当前作用域顶部的操作。
然而,只有实际的声明被提升,赋值保留在原处。
Javascript 被称为松散类型语言。这意味着 Javascript 变量可以保存任何 数据类型 的值。Javascript 会根据运行时提供的值/字面量自动处理变量类型的更改。
global_Page = 10; var global_Page; « undefined
« Integer literal, Number Type. ------------------- global_Page = 10; « Number
global_Page = 'Yash'; | Interpreted | global_Page = 'Yash'; « String
« String literal, String Type. « AS « global_Page = true; « Boolean
var global_Page = true; | | global_Page = function (){ « function
« Boolean Type ------------------- var local_functionblock; « undefined
global_Page = function (){ local_functionblock = 777;« Number
var local_functionblock = 777; };
// Assigning function as a data.
};
功能
function Identifier_opt ( FormalParameterList_opt ) {
FunctionBody | sequence of statements
« return; Default undefined
« return 'some data';
}
Default return value of function is 'undefined', Variable declaration default value also 'undefined'
Scope with respect to function-block global.
Scope with respect to page undefined | not available.
函数声明
function globalAccess() { function globalAccess() {
} ------------------- }
globalAccess(); | | function globalAccess() { « Re-Defined / overridden.
localAccess(); « Hoisted As « function localAccess() {
function globalAccess() { | | }
localAccess(); ------------------- localAccess(); « function accessed with in globalAccess() only.
function localAccess() { }
} globalAccess();
} localAccess(); « ReferenceError as the function is not defined
函数表达式
10; « literal
(10); « Expression (10).toString() -> '10'
var a;
a = 10; « Expression var a.toString() -> '10'
(function invoke() { « Expression Function
console.log('Self Invoking'); (function () {
}); }) () -> 'Self Invoking'
var f;
f = function (){ « Expression var Function
console.log('var Function'); f () -> 'var Function'
};
变量分配的函数示例:
(function selfExecuting(){
console.log('IIFE - Immediately-Invoked Function Expression');
}());
var anonymous = function (){
console.log('anonymous function Expression');
};
var namedExpression = function for_InternalUSE(fact){
if(fact === 1){
return 1;
}
var localExpression = function(){
console.log('Local to the parent Function Scope');
};
globalExpression = function(){
console.log('creates a new global variable, then assigned this function.');
};
//return; //undefined.
return fact * for_InternalUSE( fact - 1);
};
namedExpression();
globalExpression();
JavaScript 解释为
var anonymous;
var namedExpression;
var globalExpression;
anonymous = function (){
console.log('anonymous function Expression');
};
namedExpression = function for_InternalUSE(fact){
var localExpression;
if(fact === 1){
return 1;
}
localExpression = function(){
console.log('Local to the parent Function Scope');
};
globalExpression = function(){
console.log('creates a new global variable, then assigned this function.');
};
return fact * for_InternalUSE( fact - 1); // DEFAULT UNDEFINED.
};
namedExpression(10);
globalExpression();
您可以使用jsperf测试运行器
检查不同浏览器上的函数声明和表达式测试。
ES5构造函数类:使用Function.prototype.bind创建的函数对象
JavaScript将函数视为一等对象,因此作为对象,您可以向函数分配属性。
function Shape(id) { // Function Declaration
this.id = id;
};
// Adding a prototyped method to a function.
Shape.prototype.getID = function () {
return this.id;
};
Shape.prototype.setID = function ( id ) {
this.id = id;
};
var expFn = Shape; // Function Expression
var funObj = new Shape( ); // Function Object
funObj.hasOwnProperty('prototype'); // false
funObj.setID( 10 );
console.log( funObj.getID() ); // 10
ArrowFunction : ArrowParameters => ConciseBody
.
const fn = (item) => { return item & 1 ? 'Odd' : 'Even'; }; console.log( fn(2) ); // Even console.log( fn(3) ); // Odd
let
或const
来定义被其内部函数所引用的变量,那么这将是不可避免的,并且始终应该遵循这个规则,而不仅仅在不可避免的情况下。 - supercatvar functionOne
和function functionTwo
都会在某种程度上被提升——只是functionOne被设置为未定义(你可以称之为半提升,变量总是被提升到该程度),而functionTwo则完全被提升,因为它被定义和声明。当然,调用未定义的内容会抛出类型错误。 - rails_has_elegancelet functionFour = function () {...}
的语法时,var
关键字的写法略微有所不同。在这种情况下,声明let functionFour
会被提升,但是它不会被初始化,甚至没有被赋值为undefined
。因此会产生一个略微不同的错误:Uncaught ReferenceError: Cannot access 'functionFour' before initialization
。对于const
同样适用。 - Pavlo Maistrenko