协慌网

登录 贡献 社区

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

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

以前的开发人员使用两种声明函数的方法,如果背后有原因,我就无法解决。

这两种方式是:

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

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

答案

不同之处在于functionOne是一个函数表达式,因此只在达到该行时定义,而functionTwo是一个函数声明,并且只要执行其周围的函数或脚本(由于提升 )就会定义。

例如,一个函数表达式:

// TypeError: functionOne is not a function
functionOne();

var functionOne = function() {
  console.log("Hello!");
};

并且,一个函数声明:

// Outputs: "Hello!"
functionTwo();

function functionTwo() {
  console.log("Hello!");
}

这也意味着您无法使用函数声明有条件地定义函数:

if (test) {
   // Error or misbehavior
   function functionThree() { doSomething(); }
}

上面实际上定义了functionThree而不管test的值 - 除非use strict有效,在这种情况下它只会引发错误。

首先,我想纠正 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 here 在当前范围内的任何位置定义:

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

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

条款:

快速清单:

  • 功能声明

  • “匿名” 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 和 Web 代码一般仍然存在问题。)

存取器功能初始化器(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示例是它们的一种形式。如果您有多个参数来传递函数,则使用 parens:

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

构造和方法声明在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的函数。