JavaScript函数创建的细节

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

如果你曾经了解或编写过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实现KB、MB、GB、TB单位转换

当函数参数值小于等于1000时,参数除以1000,即可得到最小单位kb,赋值给变量_integer;当_integer值大于1000时,kb值除以1000,即可得到mb,赋值给变量_integer;以此类推。

js函数

在JavaScript中,函数其实就是对象。使函数不同于其他对象的决定性特点是函数存在一个被称为[[Call]]的内部属性。内部属性无法通过代码访问而是定义了代码执行时的行为。ECMAScript为JavaScript的对象定义了多种内部属性

Vue源码中用到的工具函数

以下摘取的函数,在 shared 目录下公用的工具方法。提取了一些常用通用的函数进行剖析,主要包含以下内容创建一个被冻结的空对象:判断是否是 undefined 或 null,判断是否不是 undefined 和 null

JS 自执行函数

由于自己js基础知识薄弱,很多js的知识还没有掌握,所以接下来会经常写一些关于js基础知识的博客,也算给自己提个醒吧。js自执行函数,听到这个名字,首先会联想到函数

高阶函数 - Higher Order Function

一个函数 如果输入参数包含函数 或 返回值包含函数,就称为高阶函数。按fn与fn功能是否一致【即相同输入是否始终对应相同输出】,把这类高阶函数的作用分为两类:

浅析js的工厂函数、构造函数

首先,说下工厂函数。顾名思义,就好比一个工厂一样,可以批量制造某种类型的东西。其实说白了就是封装了个方法减少重复工作,相信稍微有点码龄的人都懂。上代码:

用原生Js实现Jquery函数方法

在本文中我将把自己最常用的 jQuery 函数转换为原生 JavaScript。有时我需要创建一个简单的静态 HTML 或登录页面,而且不想引入任何库或其它依赖。对这种情况,我只使用普通的 JavaScript 来完成工作

Generator函数的语法和应用

状态机,封装了多个内部状态;返回一个遍历器对象,通过改对象可以一次遍历Generator函数内部的每一个状态;带*号,yeild表达式定义不同的内部状态;调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果

CSS calc()函数的用法

CSS3 的 calc() 函数允许我们在属性值中执行数学操作。例如,我们可以使用 calc() 指定一个元素宽的固定像素值为多个数值的和。如果你使用过 CSS 预处理器,比如 SASS

fly63.com版权所有,内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权,请与小编联系!情况属实本人将予以删除!

广告赞助文章投稿关于web前端网站点搜索站长推荐网站地图站长QQ:522607023

小程序专栏: 土味情话心理测试脑筋急转弯幽默笑话段子句子语录成语大全