JavaScript函数创建的细节

时间: 2017-12-07阅读: 330标签: 函数

如果你曾经了解或编写过JavaScript,你可能已经注意到定义函数的方法有两种。即便是对编程语言有更多经验的人也很难理解这些差异。在这篇博客的第一部分,我们将深入探讨函数声明和函数表达式之间的差异。这篇文章将不包括不同类型的函数之间的差异(箭头函数,async函数,普通函数等等),而是关注我们定义它的方式。 如果你理解挪威语,在这链接上还有一个视频包括了相同的内容.

这个系列的下一篇文章见: 关于变量定义的细节.


两种方法

在Javascript中我们去定义函数的两种方法分别是声明和表达式:

function myDeclaredFunction () {
  console.log('This is a function declaration');
}

const myFunctionExpression = function () {
  console.log('This is a function expression');
};

我们看到区分它们的一个地方是变量声明和缺少标识符名称。但是我们要深入挖掘,看看语法和用法上有什么区别。


函数声明

在JavaScript中,定义函数的一种方法是在顶层或块中,以关键字“Function”开始声明。这种定义是作为JavaScript语法的一部分。在语言中, 语法即定义什么词在什么的位置是合法的。

函数声明是一种函数式声明(类似于“词性”标注,如在自然语言中的名词,动词,等等),在语法中允许作为声明列表选项的子集,这是声明列表的一部分,也是语句块的一部分等等。这些都是技术性和罗嗦的术语。但你可以这样理解;如果一行在一个块“{ }”中或者程序顶部以关键字function开头,你可能有一个函数声明。

在语法上,我们说函数式声明必须有一个名字,或者在文法上将叫标识符。 有一个例外是当函数被export default 导出的时候。但是在其他地方,你必须为你的函数声明命名。

通过这些规则,我们可以这样举例:

// 这是一个函数声明
function myDeclaredFunction () { }
 // 这是一个函数声明
function myDeclaredFunction () { // block
   // 这是一个函数声明
   function myDeclaredInnerFunction () { }
}
// 这是一个函数声明 (无标识符)
export default function () { }
// 这不是一个有效的函数声明和Javascript
function () { }
// 这不是一个函数声明
let func = function () { }

用语法说明看起来比较复杂,但是相信我,大部分情况下你直接扫视代码,能够知道什么是声明。在大部分情况,如果以后以function 开头,并且没有包裹在圆括号里面,它就是一个函数声明。


函数表达式

如果我们了解函数声明是函数以function关键字开头定义在声明列别选项中,那么我们应该知道所有的其他函数都是表达式。函数被let,var和const定义的都是表达式。函数被定义为其他函数的参数是表达式,函数定义为一个对象属性中的值是表达式,事实上,函数定义在类语法糖中也是表达式。

我们看到函数声明大部分情况都需要标识符(default export 例外)。对于表达式来说也是一样的吗?不,函数表达式可以是匿名的(参考表达式的命名和匿名)。我们在上一个部分已经看到一个函数表达式的例子,不过我们可以有更多的例子:

// 一个函数表达式
let foo = function () {};
// 和上面一样,不过有命名的
const bar = function name () {};
// 箭头函数也是表达式
const baz = () => {};
// 这是一个他被括号包裹了的情况,使它成了函数表达式
(function () { });
// 在上面我们可以看到,在声明列表选项里面没有一个以 function关键字开头
// 但是有一个例外,那就是作为setTimeout的参数时候。
setTimeout(
  // This is a function expression
  function () { }
);
const obj = {
  // 函数表达式.
  foo () { }
  // 我们把它写的的简明一点也许更清楚:
  bar: function () { }
};
class MyClass {
  // 也是函数表达式.
  foo () { }
}
// 类只不过是语法糖,但是我们把它精简下会更容易理解
function MyClass () { }
// 现在我们可以更清楚的看到这就是表达式
Foo.prototype.foo = function foo () { };

总结起来,我们可以说,在可以拥有像字符串和数字这样值的地方,你拥有一个函数表达式。 我们已经看到语法指定我们是否有声明或表达式。但这有什么不同吗?这真的有关系吗?事实证明,在某些情况下,这很重要。让我们进一步挖掘。


声明与提升

最显著的区别,影响最大的是提升。函数声明被提升到顶层,或者在函数中有一个函数声明,提升到函数的顶端。在任何情况下,函数声明都可以在声明的地方使用它们。

// 完全合法和有效的javascript
console.log(add(40, 2));
function add(a, b) {
  return a + b;
}

这实际上是一样的:

// 运行时提升到顶部
function add(a, b) {
  return a + b;
}
console.log(add(40, 2));

在JavaScript中,提升是一个常见的事情,所有变量声明都会被提升,我们很快就会看到。你可能会问自己,为什么有人会这样做?可能有多种原因。多数情况下,像在编程中一样,是主观的。在JavaScript中,很长一段时间以来,人们一直在按优先级对代码进行优先排序。代码越重要,它就排的越高。这样做的一个效果是,在打开文件时,描述模块的函数最容易看到。这在很大程度上依赖于提升,因为所有辅助函数都会在文件中进一步定义。这种做法正在逐渐消失,但仍有开发人员喜欢这种做法。

在使用函数表达式进行变量声明时也会发生提升。我们不会详细讨论这里的内容,但是JavaScript中的所有变量声明都被提升了,但是不同的是,变量没有赋值。因此,与前一个例子相比,这是行不通的:

console.log(add(40, 2));
// 会引起一个ReferenceError。我们声明了add,但它是 undefined
// 额外知识: 在顶部之间得区域和我们赋值给变量的范围,我们称作暂时性死区。
const add = function add(a, b) {
  return a + b;
};


表达式的匿名和命名

正如我们已经看到的,函数声明在语法上不能匿名。但是表达式可以。有些时候可以方便的使用它们作为一个参数,比如“map”或“filter”之类的函数:

// 匿名函数表达式
const events = [1, 2, 3, 4].filter(function (i) {
  return i % 2 === 0;
});
// 或者有些人偏向于箭头函数
const events = [1, 2, 3, 4].filter(i => i % 2 === 0);

但和其他便利性一样,也可能有太多的东西。匿名函数可能导致难以调试,因为它混淆我们的堆栈跟踪。嵌套的匿名函数很难浏览。

我们可以通过在匿名函数中抛出错误来查看没有名称的堆栈跟踪的输出:

try {
  (function() {
    throw new Error('Error');
  })();
} catch (e) {
  console.log(e.stack);
}

会导致类似的事情:

@file:///test.js:3:11
@file:///test.js:2:4
test.js:6:3

如你所见。它还具有行和列的信息,但我们知道source-maps和转换,源码位置并不总是值得信赖。看下命名的函数::

try {
  (function myFunction () {
    throw new Error('Error');
  })();
} catch (e) {
  console.log(e.stack);
}

…我们得到:

myFunction@file:///test.js:3:11
@file:///test.js:2:13

现在,在以后的JavaScript版本中,函数可以从变量名中推断名称:

let myInferredFunction = function () { };
console.log(myInferredFunction.name);
// => myInferredFunction
// 也可以是箭头函数
let myInferredFunction = () => { };
console.log(myInferredFunction.name);
// => myInferredFunction
// Actual function name take precedence
const myInferredFunction = function namedFunction () { };
console.log(myInferredFunction.name);
// => namedFunction

在示例中要注意的几件事。我们看到JavaScript中的函数就是我们所说的“一等公民”。我们通过函数表达式的例子看到了这一点。我们把函数作为为值。在JavaScript函数中是对象(但称为可调用对象的特殊类型),因此它们具有属性。这些属性之一是‘名称’,一个getter获取器对应的函数名称。引擎使用“堆栈中的名称”踪迹错误,但我们可以直接访问它。

我们还看到函数标识符优先于推断名称。虽然推断命名是完全有效的,但是,当传递匿名函数作为参数时,我们无法推断出名称。这意味着我们仍然需要考虑匿名和调试的问题。在最近的趋势中,使用箭头函数随处可见。但箭头函数总是匿名的,除非名称被推断出来了。


递归和引用推断函数

虽然调试可以通过推断函数名获得帮助,但是推断和正确命名之间是有区别的。它可能更抽象,但在某些情况下可以创建实际的bug,并且很难清除。当使用递归函数时。我们可以把递归函数概括为调用它们自身的函数。

递归在编程工具链中是一个有价值的工具。把一个问题分解成子集,然后在一个可能无限的范围内解决一个问题。但是函数表达式上的推断名称可能会导致问题:变量可以被函数自身的外部重写:

let fibonacci = function (num) {
  if (num <= 1) return 1;
  return fibonacci(num - 1) + fibonacci(num - 2);
}
let fibCopy = fibonacci;
fibonacci = function () {
  throw new Error('No, way!');
}
console.log(fibCopy(3));
// => Error: No, way!

但是,如果我们写了一个命名的函数表达式(或函数声明),这个方法仍然能像预期的那样工作:

let fibonacci = function fibonacci (num) {
  if (num <= 1) return 1;
  return fibonacci(num - 1) + fibonacci(num - 2);
}
let fibCopy = fibonacci;
fibonacci = function () {
  throw new Error('No, way!');
}
console.log(fibCopy(3));
// => 3
// (如预期的那样)

正如最后一个示例所示,在斐波那契表达式的第二个调用递归调用中,函数指向到了实际上的命名函数表达式,而不是我们后面分配的那个会引起破坏的新函数。这意味着即使我们在外部作用域中重新赋值,当我们再次调用斐波那契函数,它仍指向正确的值。

这是因为当我们把fibonacci设置为函数的标识符时,我们覆盖了(重写,优先)前面的变量。并且不再使用实际的变量,即使我们在外部作用域中重新分配了变量也无关紧要。我们只是重新分配变量,而不是指向在fibonacci函数里面。(译者注:即函数声明,在递归调用时引擎能够正确识别本身的函数名,而匿名函数可能会被错误地推断为外部定义的变量)

这似乎是人为有意的。不过当使用递归,这实际上是一个合法的bug。如果发生了,可能很难排查。


总结

我们现在已经看到分配函数的方法有两种。对于函数声明,会把内容一起提升,对于函数表达式,不会把内容提升,但是如果我们把它赋值为变量,它的引用可能会被提升(译者注:undefined)。表达式可能是匿名的,可以命名,可以隐式的从变量中活得命名。未命名的函数会容易导致难以调试,他会混淆堆栈追踪。

我们还看到,使用对函数表达式使用变量名与使用正真命名函数不同。在递归函数中,我们最终可以引用错误的值。

可以同时使用函数声明和表达式,这取决于风格和偏好。函数声明带来的提升能力,会让你在顶部有更多的重要方法(当你第一次打开文件时可见),辅助函数在后面进一步定义,但是其他人喜欢自上而下地顺序阅读。这是个人的和主观的,但现在你知道了差异和取舍,并且能够做出一个最适合你的决定。(译者注:一般而言,应该尽量先定义后使用)

(完)


原文中发现有些不错的评论:
Amarpreet Singh:

有很多关于函数提升的讹传。并不是函数声明移动到顶部,因此可以定义之前访问。在创建阶段,编译器把函数定义储备在堆内,因此函数可以在定义前访问。函数表达式在另一方面就像变量赋值,在代码执行前,它们是未定义的。原文链接翻译链接

js中函数声明、函数表达式、匿名函数的理解和区别?

js和其他编程语言相对比较随意,首先来理解下函数基本概念:函数声明、函数表达式、匿名函数。函数声明和函数表达式区别有哪些?

js中的立即执行函数的写法,立即执行函数作用是什么?

这篇文章主要讲解:js立即执行函数是什么?js使用立即执行函数有什么作用呢?js立即执行函数的写法有哪些?

js在excel的编写_excel支持使用JavaScript自定义函数编写

微软 称excel就实现面向开发者的功能,也就是说我们不仅可以全新定义的公式,还可以重新定义excel的内置函数,现在Excel自定义函数增加了使用 JavaScript 编写的支持,下面就简单介绍下如何使用js来编写excel自定义函数。

JavaScript中函数的三种定义方法

函数的三种定义方法分别是:函数定义语句、函数直接量表达式和Function()构造函数的方法,下面依次介绍这几种方法具体怎么实现,在实际编程中,Function()构造函数很少用到,前两中定义方法使用比较普遍。

js调用函数的几种方法_ES5/ES6的函数调用方式

这篇文章主要介绍ES5中函数的4种调用,在ES5中函数内容的this指向和调用方法有关。以及ES6中函数的调用,使用箭头函数,其中箭头函数的this是和定义时有关和调用无关。

javascript回调函数的理解和使用方法(callback)

在js开发中,程序代码是从上而下一条线执行的,但有时候我们需要等待一个操作结束后,再进行下一步操作,这个时候就需要用到回调函数。 在js中,函数也是对象,确切地说:函数是用Function()构造函数创建的Function对象。

编写小而美函数的艺术

随着软件应用的复杂度不断上升,为了确保应用稳定且易拓展,代码质量就变的越来越重要。不幸的是,包括我在内的几乎每个开发者在职业生涯中都会面对质量很差的代码。这些代码通常有以下特征:

在严格模式或ES6中,如何在函数内部拿到函数对象本身?

在函数中方法函数对象本身,我们以前可以这样实现,但是在严格模式或ES6下,使用callee/caller会报错,由于不能使用arguments.callee,在不使用函数名本身的情况,有什么方法可以实现呢?

js中箭头函数的编码规范,如何更好的使用箭头函数

当您必须使用匿名函数,请使用箭头函数表示法,它创建了一个在 this 上下文中执行的函数的版本,这通常是你想要的,而且这样的写法更为简洁。如果你有一个相当复杂的函数,你或许可以把逻辑部分转移到一个声明函数上。

让我们来创建一个JavaScript Wait函数

Async/await以及它底层promises的应用正在猛烈地冲击着JS的世界。在大多数客户端和JS服务端平台的支持下,回调编程已经成为过去的事情。当然,基于回调的编程很丑陋的。