开发者问题收集

var 函数名称 = function() {} vs function 函数名称() {}

2008-12-03
1223264

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

之前的开发人员使用了两种声明函数的方式,我不知道这背后是否有原因。

这两种方式是:

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

以及

function functionTwo() {
    // Some code
}

使用这两种不同方法的原因是什么,每种方法的优缺点是什么?是否有一种方法可以做而另一种方法不能做的事情?

3个回答

区别在于 functionOne 是一个 函数表达式 ,因此仅在到达该行时才定义,而 functionTwo 是一个 函数声明 ,并且在其周围的函数或脚本执行时立即定义(由于 提升 )。

例如,一个函数表达式:

// 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
2008-12-03

首先我想纠正一下 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"

使用组合样式的一个令人信服的理由是函数对象的“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() 的情况下 — 答案是:它取决于浏览器。

Eugene Lazutkin
2008-12-03

以下是创建函数的标准形式的概述: (最初是为另一个问题编写的,但在移入规范问题后进行了改编。)

术语:

快速列表:

  • 函数声明

  • “匿名” 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,规范才涵盖如果将函数声明放在控制结构(如 tryifswitchwhile 等)中,JavaScript 引擎应该做什么,如下所示:

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 中,此函数没有名称(它是匿名的)。在 ES2015 中,如果可能,会通过从上下文推断来为函数分配一个名称。在上面的示例中,名称将是 y 。当函数是属性初始化器的值时,也会执行类似操作。 (有关发生这种情况的时间和规则的详细信息,请在 规范 中搜索 SetFunctionName - 它出现在 各个地方 。)

命名 函数 表达式

第三种形式是 命名函数表达式 (“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.definePropertiesObject.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));

没有 { ... } 的版本称为带有 表达式主体 简洁主体 的箭头函数。(也称为: 简洁 箭头函数。)带有 { ... } 定义主体的版本是带有 函数主体 的箭头函数。 (另见: 详细 箭头函数。)

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

T.J. Crowder
2014-03-04