var functionName = function() {} vs function functionName() {}

IT技术 javascript function syntax idioms
2020-12-27 21:58:05

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

以前的开发人员使用了两种声明函数的方式,我无法弄清楚它背后是否有原因。

这两种方式是:

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

使用这两种不同方法的原因是什么,每种方法的优缺点是什么?有什么可以用一种方法完成而另一种方法无法完成的吗?

6个回答

不同之处在于它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

感谢 Greg 和 @Ben_Aston 解释差异。但是,你们中的一个人是否也可以按照用户的要求对“优点”和“缺点”给出一些说明?
2021-02-08 21:58:05
函数定义在代码进入周围块时执行,而不是在它进入封闭函数时执行。我不知道事情是否总是这样工作,但是如果使用块letconst定义一个被其中的函数关闭的变量将是不可避免的,并且始终应用该规则可能比仅在不可避免时应用它更好.
2021-02-13 21:58:05
“由于提升”这句话可能会给人一种错误的印象,即只有命名的函数会被提升。事实上,两者都var functionOnefunction functionTwo某种程度上被提升 - 只是 functionOne 被设置为未定义(你可以称之为半提升,变量总是只被提升到那个程度)而函数 functionTwo 被完全提升,因为它被定义并且宣布。调用未定义的东西当然会抛出一个类型错误。
2021-02-24 21:58:05

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

在这种情况下,xyzabc都是同一个对象的别名:

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 在当前范围内的任何地方都定义了此处:

// 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()- 答案是:这取决于浏览器的情况下。

@EugeneLazutkin 您正在定义一个函数并立即调用(调用)它,也称为 IIFE(立即调用的函数表达式),这是实现词法范围的一种方法(IIFE 内部的任何内容都无法在其外部访问)。所以 的值abc不是函数本身,而是函数的返回值。abc.name 为空是有意义的,因为 abc 返回一个未命名的函数。@ikirachen 提到删除 ,()因为那是调用该函数的原因。没有它,它只是用多余的括号括起来。
2021-02-11 21:58:05
@EugeneLazutkin 您正在执行函数并尝试读取结果的名称。去除那个 '();' 部分和你的例子将是正确的;)
2021-02-18 21:58:05
@ikirachen 我不明白你指的是什么。您能否为您的建议提供一些背景信息,以便我了解您在说什么?
2021-02-18 21:58:05
显然,JS 运行时变得更智能了。然而把它包起来: var abc = (() => function(){})(); 控制台日志(abc.name);// 没有
2021-02-27 21:58:05
var abc = function(){}; 控制台日志(abc.name);// "abc" // 从 2021 年开始
2021-03-05 21:58:05

这是创建函数的标准形式的纲要:(最初是为另一个问题编写的,但在移入规范问题后进行了调整。)

条款:

快速清单:

  • 函数声明

  • “匿名”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引擎应该做的事情tryifswitchwhile,等等,是这样的:

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)

“匿名”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 与网络代码一起使用仍然存在问题。)

访问器函数初始化器 (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()通话中的那个东西了吗?那是一个功能。

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

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

没有的版本{ ... }称为带有表达式 body简洁 body的箭头函数(另外:一个简洁的箭头函数。){ ... }定义主体的箭头函数是一个带有函数 body的箭头函数(另外:一个冗长的箭头函数。)

对象初始化器中的方法声明(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;
    }
}

上面有两个函数声明:一个用于构造函数,它获取 name Person,另一个 for getFullName,它是分配给 的函数Person.prototype

说到全局上下文,最后的var语句和 aFunctionDeclaration都会在全局对象上创建一个不可删除的属性,但是两者的值都可以被覆盖

两种方式之间的细微差别在于,当变量实例化过程运行时(在实际代码执行之前),所有声明为 with 的标识符var都将使用 初始化undefined,而FunctionDeclaration's使用的标识符从那一刻起可用,例如:

 alert(typeof foo); // 'function', it's already available
 alert(typeof bar); // 'undefined'
 function foo () {}
 var bar = function () {};
 alert(typeof bar); // 'function'

分配bar FunctionExpression一直发生到运行时。

由 a 创建的全局属性FunctionDeclaration可以像变量值一样被覆盖而不会出现任何问题,例如:

 function test () {}
 test = null;

您的两个示例之间的另一个明显区别是第一个函数没有名称,但第二个函数有名称,这在调试(即检查调用堆栈)时非常有用。

关于您编辑的第一个示例 ( foo = function() { alert('hello!'); };),这是一个未声明的作业,我强烈建议您始终使用var关键字。

有赋值,​​没有var语句,如果在作用域链中找不到被引用的标识符,就会成为全局对象可删除属性。

此外,未声明的赋值会ReferenceError严格模式下在 ECMAScript 5 上抛出一个

必读:

注意:此答案已从另一个问题合并而来,其中 OP 的主要疑问和误解是用 a 声明的标识符FunctionDeclaration不能被覆盖,但事实并非如此。

您在那里发布的两个代码片段几乎对于所有目的都具有相同的行为方式。

但是,行为的不同之处在于,对于第一个变体 ( var functionOne = function() {}),只能在代码中的那个点之后调用该函数。

对于第二个变体 ( function functionTwo()),该函数可用于在声明该函数的位置上方运行的代码。

这是因为对于第一个变体,函数foo在运行时分配给变量在第二个中,函数foo在解析时分配给该标识符

更多技术信息

JavaScript 有三种定义函数的方式。

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