JavaScript函数创建的细节

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

如果你曾经了解或编写过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:

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

ES6 Array.find()和findIndex()函数用法

ES6为Array增加了find(),findIndex函数。find()函数用来查找目标元素,找到就返回该元素,找不到返回undefined,而findIndex()函数也是查找目标元素,找到就返回元素的位置,找不到就返回-1。

Vue 中的 computed 和 methods 的使用

computed:在computed中的函数,是在dom加载后马上执行的;methods:在methods中的函数,必须要有一定的触发条件,才会执行 ,Vue.js 将绑定表达式限制为一个表达式,如果想要实现绑定多于一个表达式的逻辑

JS函数提升和变量提升

函数声明(function declaration),通过function 关键字,functionName函数名,arg参数(可选)定义的函数。函数表达式: 将函数声明赋值给一个变量,这个表达式叫做函数表达式

ES6箭头函数

传统的javascript函数语法并没有提供任何的灵活性,每一次你需要定义一个函数时,你都必须输入function () {},这至少会出现两个问题,ES6箭头函数都圆满解决了它,箭头函数内的this值继承自外围作用域。运行时它会首先到它的父作用域找,如果父作用域还是箭头函数

Jquery的toggle()函数

toggle()函数用于切换元素的显示/隐藏 jQuery还有一个同名的事件函数,toggle(),用于绑定click事件并在触发时轮流切换执行不同的事件处理函数。

Js中document.execCommand()函数的使用

document.execCommand()方法处理Html数据时常用语法格式如下:document.execCommand(sCommand[,交互方式, 动态参数])其中:sCommand为指令参数(如下例中的”2D-Position”),交互方式参数如果是true的话将显示对话框,如果为false的话,则不显示对话框

Js函数表达和闭包

函数声明提升:函数可以先用,声明在下面自动给提到上面来,函数表达式=后面的是匿名函数,又叫拉姆达函数,他一般可以被用来当成值使用(可以用来return),函数自己调用自己就叫递归,没啥好说的。当函数赋值给另一个函数时会导致重新调用函数名称不同而调用失败

js中的toString和valueOf

基本上,所有JS数据类型都拥有valueOf和toString这两个方法,null除外。它们俩解决javascript值运算与显示的问题。所有对象继承了两个转换方法:每个JavaScript固有对象的 valueOf 方法定义不同。

javascript封装函数

使用函数有两步:1、定义函数,又叫声明函数, 封装函数。2、调用函数var 变量 = 函数名(实参);对函数的参数和返回值的理解

什么时候不该使用es6箭头函数?

箭头函数带来了很多便利。恰当的使用箭头函数可以让我们避免使用早期的.bind()函数或者需要固定上下文的地方并且让代码更加简洁。箭头函数也有一些不便利的地方。我们在需要动态上下文的地方不能使用箭头函数:定义需要动态上下文的函数

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

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

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