var functionName = function() {} 与 function functionName() {} 有什么区别?

7613

最近我开始维护别人的JavaScript代码。我正在修复错误,添加功能,并尝试整理代码并使其更加一致。

之前的开发人员使用了两种声明函数的方式,我无法确定是否有什么原因。

这两种方式是:

var functionOne = function() {
    // Some code
};

而且,

function functionTwo() {
    // Some code
}

为什么要使用这两种不同的方法,各自有哪些优缺点?是否有一种方法可以做到另一种方法无法实现的事情?

41个回答

5578
区别在于functionOne是一个函数表达式,因此只有在到达该行时才被定义,而functionTwo是一个函数声明,在其所在的函数或脚本执行时就被定义(由于hoisting)。
例如,一个函数表达式:

// 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


函数定义在代码进入周围块时执行,而不是进入封闭函数时执行。我不知道事情是否总是这样工作的,但如果一个块使用letconst来定义被其内部函数所引用的变量,那么这将是不可避免的,并且始终应该遵循这个规则,而不仅仅在不可避免的情况下。 - supercat
48
“由于hoisting”这个句子可能会给人一种错误的印象,认为只有所命名的函数被提升。事实上,var functionOnefunction functionTwo都会在某种程度上被提升——只是functionOne被设置为未定义(你可以称之为半提升,变量总是被提升到该程度),而functionTwo则完全被提升,因为它被定义和声明。当然,调用未定义的内容会抛出类型错误。 - rails_has_elegance
5
当使用 let functionFour = function () {...} 的语法时,var 关键字的写法略微有所不同。在这种情况下,声明 let functionFour 会被提升,但是它不会被初始化,甚至没有被赋值为 undefined。因此会产生一个略微不同的错误:Uncaught ReferenceError: Cannot access 'functionFour' before initialization。对于 const 同样适用。 - Pavlo Maistrenko
@rails_has_elegance 那如果它的行为和“根本没有被举起”完全相同,那叫它“半举起”的意义是什么? - vanowm
5
@vanowm 它的行为与“根本没有被提升”不同。如果它没有被提升,你会得到一个 ReferenceError。由于它被提升了,所以你会得到一个 TypeError。 在控制台中比较这两个语句:
  1. hoisted(); var hoisted = function() {} 2. notHoisted(); const notHoisted = function() {}。在第一种情况下,它是一个 TypeError,因为你试图调用未定义的函数(虽然它确实被提升了,这就是为什么它至少是未定义的,这仍然比没有好)。在第二种情况下,它甚至不是未定义的,你只会得到一个普通的 ReferenceError。
- rails_has_elegance

2094

首先,我想纠正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;

在这种情况下,xyzabc都是同一对象的别名:
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()的情况下——答案是:这取决于浏览器。


1
var abc = function(){}; console.log(abc.name); // "abc" // 来自2021 - lfx_cool
5
显然,JS运行时变得更加智能了。现在包装它: var abc = (() => function(){})(); console.log(abc.name); // 什么也没有 - Eugene Lazutkin
@EugeneLazutkin 您正在定义一个函数并立即调用它,也称为IIFE(立即调用的函数表达式),这是实现词法作用域的一种方法(IIFE内部的任何内容都无法从外部访问)。 因此,abc的值不是函数本身,而是该函数的返回值。 对于abc.name为空是有意义的,因为abc返回一个未命名的函数。 @ikirachen提到删除(),因为这就是调用函数的方式。 没有它,它只是被多余的括号包裹。 - Sinjai
1
要明确一点,这是一种实现更严格作用域的方法,使用 var 在括号内声明的变量将像通常一样在函数作用域中,但是该匿名函数不再可在其所包含的括号之外访问。幸运的是,现在我们有了 let,它使用了普通(理智的)人所期望的块级作用域。在我看来,最好假装 var 不存在。 - Sinjai
@EugeneLazutkin var abc = (() => function(){})(); - Sinjai
显示剩余2条评论

741
以下是创建函数的标准形式的概述:(最初为另一个问题编写,但在移动到规范问题后进行了调整。) 术语: 快速列表:
  • 函数声明

  • "匿名" 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开始,规范说明了该怎么做。事实上,它给出了三个单独的操作:
1. 如果在松散模式下且不在Web浏览器上,则JavaScript引擎应该执行一项操作 2. 如果在Web浏览器上的松散模式下,则JavaScript引擎应该执行另一项操作 3. 如果在严格模式下(无论是在浏览器还是不在浏览器上),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中,函数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"

请注意,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.definePropertyObject.defineProperties 和较少人知道的 Object.create 的第二个参数来创建访问器函数。
箭头函数表达式(ES2015+)
ES2015 带来了箭头函数。以下是一个例子:
var a = [1, 2, 3];
var b = a.map(n => n * 2);
console.log(b.join(", ")); // 2, 4, 6

map()调用中隐藏的n => n * 2是一个函数。

有关箭头函数的一些事情:

  1. 它们没有自己的this。相反,它们在定义它们的上下文中闭合this。(它们还会关闭arguments和相关的super。)这意味着其中的this与它们创建的地方的this相同,并且不能更改。

  2. 正如您在上面看到的那样,您不使用关键字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的函数。

非常好的写作!我希望更多的人能像你一样解释得这么好。 - Antoine Weber

175

就全局上下文而言,无论是使用 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语句的情况下进行赋值,如果在作用域链中找不到引用的标识符,它将成为全局对象的可删除属性。
此外,在ECMAScript 5的Strict Mode下,未声明的赋值会抛出ReferenceError错误。
必读: 注意:本答案合并自另一个问题,其中OP的主要疑惑和误解是使用FunctionDeclaration声明的标识符不能被覆盖,但事实并非如此。

146

你发布的两个代码片段在大多数情况下将表现出相同的行为。

然而,它们之间的区别在于第一种方式(var functionOne = function() {})只能在该代码点之后调用该函数。

而使用第二个方式(function functionTwo()),该函数可供运行在函数声明之上的代码使用。

这是因为在第一种方式中,函数在运行时分配给变量foo。而在第二种方式中,函数在解析时分配给标识符foo

更多技术信息

JavaScript有三种定义函数的方法:

  1. 你的第一个代码片段展示了函数表达式。这涉及使用"function"操作符来创建一个函数 - 该操作符的结果可以存储在任何变量或对象属性中。函数表达式非常强大。函数表达式通常被称为“匿名函数”,因为它不需要一个名称。
  2. 你的第二个例子是一个函数声明。这使用"function"语句来创建一个函数。函数在解析时就可用,并且可以在该范围内的任何地方调用。你仍然可以稍后将其存储在变量或对象属性中。
  3. 定义函数的第三种方式是"Function()"构造函数,这在你的原始帖子中没有展示。不建议使用此方法,因为它与eval()的工作方式相同,而eval()存在其问题。

126

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();
                            ---------------

107

其他评论者已经讲述了上述两种变体的语义差异。我想指出一种风格上的不同:只有“赋值”版本才能设置另一个对象的属性。

我经常使用以下模式构建JavaScript模块:

(function(){
    var exports = {};

    function privateUtil() {
            ...
    }

    exports.publicUtil = function() {
            ...
    };

    return exports;
})();

使用这种模式,您的公共函数都将使用赋值,而私有函数则使用声明。

(还要注意的是,语句后的赋值应该需要分号,而声明则禁止使用分号。)


91

首选第一种方法而不是第二种方法的一个例子是当您需要避免覆盖函数先前的定义时。

使用

if (condition){
    function myfunction(){
        // Some code
    }
}

这个myfunction的定义会覆盖任何之前的定义,因为它是在解析时完成的。

if (condition){
    var myfunction = function (){
        // Some code
    }
}

只有在满足condition时,才能正确定义myfunction的工作。


72

一个重要的原因是将一个且仅将一个变量作为你的命名空间的“根”...

var MyNamespace = {}
MyNamespace.foo= function() {

}
或者
var MyNamespace = {
  foo: function() {
  },
  ...
}

有许多命名空间技术。随着大量可用的JavaScript模块,它变得越来越重要。

另请参见如何在JavaScript中声明命名空间?


64

Hoisting 是 JavaScript 解释器将所有变量和函数声明移动到当前作用域顶部的操作。

然而,只有实际的声明被提升,赋值保留在原处。

  • 页面内声明的变量/函数是全局的,可以在该页面的任何地方访问。
  • 在函数内部声明的变量/函数具有本地作用域。这意味着它们仅在函数体(作用域)内可用/访问,在函数体外部不可用。

Variable

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';
}
  • functions declared inside the page are hoisted to top of the page having global access.
  • functions declared inside the function-block are hoisted to top of the block.
  • 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

ES6 引入了箭头函数: 箭头函数表达式具有更短的语法,最适用于非方法函数,并且不能用作构造函数。

ArrowFunction : ArrowParameters => ConciseBody.

const fn = (item) => { return item & 1 ? 'Odd' : 'Even'; };
console.log( fn(2) ); // Even
console.log( fn(3) ); // Odd

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接