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

7613

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

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

这两种方式是:

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

而且,

function functionTwo() {
    // Some code
}

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

41个回答

53

  1. 函数的可用性(作用域)

以下代码有效是因为 function add() 的作用域在最近的块级范围内:

try {
  console.log("Success: ", add(1, 1));
} catch(e) {
  console.log("ERROR: " + e);
}

function add(a, b){
  return a + b;
}

以下代码无法正常工作,因为在给变量add赋予函数值之前,该变量被调用了。

try {
  console.log("Success: ", add(1, 1));
} catch(e) {
  console.log("ERROR: " + e);
}

var add=function(a, b){
  return a + b;
}

上面的代码与下面的代码在功能上是完全相同的。请注意,明确地赋值 add = undefined 是多余的,因为只需执行 var add;var add=undefined 是完全相同的。

var add = undefined;

try {
  console.log("Success: ", add(1, 1));
} catch(e) {
  console.log("ERROR: " + e);
}

add = function(a, b){
  return a + b;
}

以下内容无法运行,因为 var add= 开始一个表达式,导致后面的function add()成为表达式而不是块。命名函数只对它们自己和周围的块可见。由于 function add() 在此处是一个表达式,它没有周围的块,因此只对它自己可见。

try {
  console.log("Success: ", add(1, 1));
} catch(e) {
  console.log("ERROR: " + e);
}

var add=function add(a, b){
  return a + b;
}

  1. (function).name

一个函数的名称function thefuncname(){}声明时就是thefuncname

function foobar(a, b){}

console.log(foobar.name);

var a = function foobar(){};

console.log(a.name);

否则,如果一个函数被声明为function(){},那么function.name就是用来存储该函数的第一个变量。

var a = function(){};
var b = (function(){ return function(){} });

console.log(a.name);
console.log(b.name);

如果没有变量设置给函数,则该函数的名称为空字符串("")。

console.log((function(){}).name === "");

最后,虽然函数被分配的变量最初设置了名称,但连续设置为该函数的变量不会更改名称。

var a = function(){};
var b = a;
var c = b;

console.log(a.name);
console.log(b.name);
console.log(c.name);

  1. 性能

在Google的V8和Firefox的Spidermonkey中,JIT编译可能会有几微秒的差异,但最终结果是完全相同的。为了证明这一点,我们可以通过比较两个空代码片段的速度来检查JSPerf在微基准测试方面的效率。JSPerf测试可以在这里找到。而jsben.ch测试则可以在这里找到。正如你所看到的,当不应该有任何差异时,确实存在明显的差异。如果你像我一样真的很在意性能,那么尝试减少作用域中变量和函数的数量,特别是消除多态性(例如使用同一个变量存储两种不同类型)可能更值得你花费时间。

  1. 变量可变性

当你使用var关键字声明一个变量后,你可以重新分配一个不同的值给这个变量,就像这样:

(function(){
    "use strict";
    var foobar = function(){}; // initial value
    try {
        foobar = "Hello World!"; // new value
        console.log("[no error]");
    } catch(error) {
        console.log("ERROR: " + error.message);
    }
    console.log(foobar, window.foobar);
})();

然而,当我们使用const语句时,变量引用变为不可变。这意味着我们不能给变量赋新值。请注意,这并不使变量的内容不可变:如果您执行const arr = [],那么您仍然可以执行arr[10] = "example"。只有像arr = "new value"arr = []这样的操作才会抛出错误,如下所示。

(function(){
    "use strict";
    const foobar = function(){}; // initial value
    try {
        foobar = "Hello World!"; // new value
        console.log("[no error]");
    } catch(error) {
        console.log("ERROR: " + error.message);
    }
    console.log(foobar, window.foobar);
})();

有趣的是,如果我们将变量声明为function funcName(){},那么该变量的不可变性与使用var声明变量的方式相同。

(function(){
    "use strict";
    function foobar(){}; // initial value
    try {
        foobar = "Hello World!"; // new value
        console.log("[no error]");
    } catch(error) {
        console.log("ERROR: " + error.message);
    }
    console.log(foobar, window.foobar);
})();

最近的块

“最近的块”是最近的“函数”(包括异步函数、生成器函数和异步生成器函数)。然而,有趣的是,当一个function functionName() {}在非闭合块中时,对于闭合块外部的项目来说,它的行为就像var functionName = function() {}。请看以下示例。

  • 普通的var add=function(){}

try {
  // typeof will simply return "undefined" if the variable does not exist
  if (typeof add !== "undefined") {
    add(1, 1); // just to prove it
    console.log("Not a block");
  }else if(add===undefined){ // this throws an exception if add doesn't exist
    console.log('Behaves like var add=function(a,b){return a+b}');
  }
} catch(e) {
  console.log("Is a block");
}
var add=function(a, b){return a + b}

  • 普通的 function add(){}

try {
  // typeof will simply return "undefined" if the variable does not exist
  if (typeof add !== "undefined") {
    add(1, 1); // just to prove it
    console.log("Not a block");
  }else if(add===undefined){ // this throws an exception if add doesn't exist
    console.log('Behaves like var add=function(a,b){return a+b}')
  }
} catch(e) {
  console.log("Is a block");
}
function add(a, b){
  return a + b;
}

  • 功能

try {
  // typeof will simply return "undefined" if the variable does not exist
  if (typeof add !== "undefined") {
    add(1, 1); // just to prove it
    console.log("Not a block");
  }else if(add===undefined){ // this throws an exception if add doesn't exist
    console.log('Behaves like var add=function(a,b){return a+b}')
  }
} catch(e) {
  console.log("Is a block");
}
(function () {
    function add(a, b){
      return a + b;
    }
})();

  • 语句(例如 ifelseforwhiletry/catch/finallyswitchdo/whilewith

try {
  // typeof will simply return "undefined" if the variable does not exist
  if (typeof add !== "undefined") {
    add(1, 1); // just to prove it
    console.log("Not a block");
  }else if(add===undefined){ // this throws an exception if add doesn't exist
    console.log('Behaves like var add=function(a,b){return a+b}')
  }
} catch(e) {
  console.log("Is a block");
}
{
    function add(a, b){
      return a + b;
    }
}

  • 使用var add=function()的箭头函数

try {
  // typeof will simply return "undefined" if the variable does not exist
  if (typeof add !== "undefined") {
    add(1, 1); // just to prove it
    console.log("Not a block");
  }else if(add===undefined){ // this throws an exception if add doesn't exist
    console.log('Behaves like var add=function(a,b){return a+b}')
  }
} catch(e) {
  console.log("Is a block");
}
(() => {
    var add=function(a, b){
      return a + b;
    }
})();

  • 使用箭头函数与function add()

try {
  // typeof will simply return "undefined" if the variable does not exist
  if (typeof add !== "undefined") {
    add(1, 1); // just to prove it
    console.log("Not a block");
  }else if(add===undefined){ // this throws an exception if add doesn't exist
    console.log('Behaves like var add=function(a,b){return a+b}')
  }
} catch(e) {
  console.log("Is a block");
}
(() => {
    function add(a, b){
      return a + b;
    }
})();


50

因为其他人都详细地讨论了变量提升的部分,所以我想加入自己的回答。

我已经长时间思考哪种方式更好了,感谢 http://jsperf.com,现在我知道了 :)

enter image description here

函数声明更快,这才是 Web 开发中真正重要的事情,对吧? ;)


请看下面关于性能的回答,它可以提供不同的结果。 - d9k

41

一旦绑定建立,函数声明和赋值给变量的函数表达式的行为相同。

然而,在函数对象实际与其变量关联的方式时间上却有所不同。这种差异是由JavaScript中的变量提升机制引起的。

基本上,所有函数声明和变量声明都会被提升到声明所在的函数的顶部(这就是我们说JavaScript具有函数作用域的原因)。

  • 当函数声明被提升时,函数体将“跟随”提升,因此当评估函数体时,变量将立即绑定到函数对象。

  • 当变量声明被提升时,初始化不会跟随提升,而是“留下”。变量在函数体的开头被初始化为undefined,并且将在代码中原始位置分配一个值。(实际上,它将在每个出现相同名称变量的声明位置都分配值。)

变量提升的顺序也很重要:函数声明优先于具有相同名称的变量声明,并且最后一个函数声明优先于先前具有相同名称的函数声明。

以下是一些例子...

var foo = 1;
function bar() {
  if (!foo) {
    var foo = 10 }
  return foo; }
bar() // 10

变量foo被提升到函数顶部,被初始化为undefined,因此!footrue,所以foo被赋值为10bar作用域之外的foo无关紧要且不会受到影响。

function f() {
  return a; 
  function a() {return 1}; 
  var a = 4;
  function a() {return 2}}
f()() // 2

function f() {
  return a;
  var a = 4;
  function a() {return 1};
  function a() {return 2}}
f()() // 2

函数声明优先于变量声明,并且最后一个函数声明“生效”。

function f() {
  var a = 4;
  function a() {return 1}; 
  function a() {return 2}; 
  return a; }
f() // 4
在这个例子中,a被初始化为通过评估第二个函数声明而产生的函数对象,并且然后被赋值为4
var a = 1;
function b() {
  a = 10;
  return;
  function a() {}}
b();
a // 1

这里首先对函数声明进行变量提升,声明并初始化变量a。接下来,将变量a赋值为10。换句话说,赋值操作不会赋值给外部变量a


38

第一个示例是函数声明:

function abc(){}

第二个示例是一个函数表达式:

var abc = function() {};

主要区别在于它们被提升(升起并声明)的方式不同。在第一个示例中,整个函数声明都被提升了。而在第二个示例中,只有变量'abc'被提升,其值(即函数)将为undefined,并且函数本身保持在被声明的位置。

简单来说:

//this will work
abc(param);
function abc(){}

//this would fail
abc(param);
var abc = function() {}

我强烈推荐你访问这个 链接 来更深入地学习这个主题。


37

从代码维护成本的角度来看,命名函数更可取:

  • 与声明它们的位置无关(但仍受作用域限制)。
  • 更能抵御条件初始化等错误(如果需要,还可以进行重写)。
  • 通过将局部函数分配到作用域功能之外,使代码更易读。通常在作用域中,功能排在首位,其后是局部函数的声明。
  • 在调试器中,您将清楚地看到调用堆栈上的函数名称,而不是“匿名/计算”的函数。

我认为命名函数的优点还有很多。而所列出的命名函数的优点对于匿名函数来说则是一个劣势。

历史上,匿名函数出现是由于JavaScript作为一种语言无法使用命名函数列出成员:

{
    member:function() { /* How do I make "this.member" a named function? */
    }
}

33

在计算机科学中,我们谈论匿名函数和命名函数。我认为最重要的区别在于匿名函数没有被绑定到一个名称上,因此称之为匿名函数。在 JavaScript 中,它是一个在运行时动态声明的一等对象。

关于匿名函数和 λ 演算的更多信息,请参阅维基百科:匿名函数


33

格雷戈里的回答已经足够好了,但我仍然想补充一些我刚刚在观看道格拉斯·克罗克福德的视频时学到的东西。

函数表达式:

var foo = function foo() {};

函数声明:

function foo() {};

函数声明语句只是使用带有function值的var语句的速记。

因此,

function foo() {};

扩展为

var foo = function foo() {};

进一步展开:

var foo = undefined;
foo = function foo() {};

它们都被提升到代码的顶部。

视频截图


32

我在我的代码中使用变量方法,有一个非常具体的原因。这个理论已经以抽象的方式涵盖在上面了,但是举个例子可能会帮助像我一样JavaScript专业知识有限的人。

我有一个需要在160个独立设计品牌中运行的代码。大部分代码在共享文件中,但品牌特定的东西在单独的文件中,每个品牌一个文件。

有些品牌需要特定的函数,有些则不需要。有时我必须添加新的函数来做新的品牌特定的事情。我很乐意改变共享代码,但我不想去改变所有160组品牌文件。

通过使用变量语法,我可以在共享代码中声明变量(本质上是一个函数指针),并将其赋值为一个微不足道的存根函数或设置为空。

然后只有一两个品牌需要函数的特定实现,他们可以定义他们版本的函数并将其赋值给变量,而其他品牌则什么都不用做。我可以在执行共享代码前测试空函数。

从上面人们的评论中,我了解到可能也可以重新定义静态函数,但我认为变量解决方案很好很清晰。


26

@EugeneLazutkin给出了一个例子,他将分配的函数命名为shortcut()的内部引用以便于自我调用。John Resig在他的Learning Advanced Javascript教程中给出了另一个例子——将递归函数复制到另一个对象中。虽然将函数分配给属性并不是本文的重点,但我建议你积极尝试这个教程——通过点击右上角的按钮运行代码,并双击代码进行编辑。

教程中的例子:在yell()中进行递归调用:

当删除原始忍者对象时,测试会失败。(第13页)

function assert(predicate, message) { if(!predicate) { throw new Error(message); } }

var ninja = {
  yell: function(n){
return n > 0 ? ninja.yell(n-1) + "a" : "hiy";
  }
};
assert( ninja.yell(4) == "hiyaaaa", "A single object isn't too bad, either." ); 

var samurai = { yell: ninja.yell };
var ninja = null;

try {
  samurai.yell(4);
} catch(e){
  assert( false, "Uh, this isn't good! Where'd ninja.yell go?" );
}

如果您给将被递归调用的函数命名,测试就会通过。(第14页)

function assert(predicate, message) { if(!predicate) { throw new Error(message); } }

var ninja = {
  yell: function yell(n){
return n > 0 ? yell(n-1) + "a" : "hiy";
  }
};
assert( ninja.yell(4) == "hiyaaaa", "Works as we would expect it to!" );
 
var samurai = { yell: ninja.yell };
var ninja = {};
assert( samurai.yell(4) == "hiyaaaa", "The method correctly calls itself." );

console.log(samurai.yell(4));


21
另一个在其他答案中没有提到的区别是,如果您使用匿名函数并将其用作构造函数,如下所示:

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

var one = new functionOne();

console.log(one.constructor.name);

那么one.constructor.name将不会被定义。Function.name是非标准的,但被Firefox、Chrome、其他基于Webkit的浏览器和IE 9+支持。

使用

function functionTwo() {
    // Some code
}
two = new functionTwo();

可以使用two.constructor.name来将构造函数的名称作为字符串检索出来。

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