从这篇开始, 我们会用很长的章节来讨论函数, 这个 JavaScript 中最重要, 也是最基本的技能. 本章中, 我们会区分函数表达式与函数声明, 并且还会学习到局部作用域和变量声明提升的工作原理. 以及大量对 API, 代码初始化, 程序性能等有帮助的模式.
我们首先, 要来回顾一些基础知识以明确一些概念和定义.
一, 背景
JavaScript 中的函数有两个主要特点显使其显得比较特殊. 第一个特点在于函数是第一类对象(first-class object), 第二个特点在于它们可以提供作用域. 函数就是对象:
函数可以在运行时动态创建, 还可以在程序执行过程中创建.
函数可以分配给变量, 可以将它们的引用复制到其他变量, 可以被扩展, 此外, 除少数特殊情况, 函数还可以被删除.
可以作为参数传递给其他函数, 并且还可以由其他函数返回.
函数可以由自己的属性和方法.
因此, 对于函数 A 来说, 他可能是一个对象, 并且具有自己的属性和方法, 而且其中的方法之一可能恰好又是另一个函数 B. 此外, 函数 B 可以接受函数 C 作为参数, 并且在执行时可以返回另外的函数 D.
- function A(){};
- A.name = "aName";
- function D(){console.log(1234)};
- function C(){
- return D();
- };
- A.B = function (callback){
- callback()
- };
- A.B(C);
代码看起来就像上面这样. 乍看之下, 有许多的函数要记录. 但是适应各种函数应用后, 将开始欣赏函数所提供的能力, 灵活性, 以及表现力. 一般来说, 当考虑 JavaScript 中的函数, 对象时, 其唯一的特性在于该对象 (即函数) 是可调用的, 这意味着它是可执行的.
事实上, 当看到 new Function()构造函数运行时, 函数就是对象的意义就变得非常明确了:
- // 反模式
- // 仅用于演示目的
- var add = new Function('a','b','return a + b');
- add(1,2); // returns 3
在以上这段代码中, 毫无疑问, add()是一个对象, 毕竟它是由一个构造函数所创建. 然而, 使用 Function()构造函数并不是一个好主意, 如同使用 eval()一样不好. 这是由于代码是以字符串方式传递并重新计算. 同样, 这也不便于编写和阅读, 这是因为必须使用引号隔开代码, 并且如果出于可读性的目的而希望在函数中正确地缩进代码, 那么还需要格外注意.
第二个重要的特征在于函数提供了作用域. 在 JavaScript 中并没有块级作用域 (当然, let 出现之后, 已经有了块级作用域, 这里我们不讨论). 函数内部以 var 关键词定义的任何变量都是局部变量, 对于函数外部是不可见的. 考虑到花括号{} 并不提供作用域(这句话是没问题的, 哪怕是在现在的 ES6 出现之后, 因为提供作用域的并不是花括号, 而是花括号内使用 let 声明), 因此如果在 if 条件语句或在 for 以及 while 循环中, 使用 var 关键词定义一个变量, 这并不意味着定义了一个局部变量. 它仅对于包装函数来说是局部变量, 并且如果没有包装函数, 它将成为一个全局变量.
消除术语的歧义
让我嗯话费一点时间讨论用于定义函数的相关代码的术语, 因为在谈论到模式时, 使用准确, 约定的名称与代码是同等重要的.
- // 命名函数表达式
- var add = function add(a,b) {
- return a + b;
- };
上面的代码显示了一个函数, 它使用了命名函数表达式(named function expression). 如果跳过函数表达式中的名称(例子中的第二个 add), 将会得到一个未命名函数表达式, 也简称为函数表达式, 或者最常见的是将之称为匿名函数.
- // 函数表达式, 又名匿名函数, 未命名函数表达式
- var add = function(a,b) {
- return a + b;
- };
因此, 广义上称为函数表达式, 并且命名函数表达式是一个函数表达式的一种特殊情况, 通常发生在定义可选的命名时.
当省略了第二个 add 并且以一个未命名函数表达式作为结束, 这并不会影响该函数的定义以及后续的调用. 唯一的区别在于该函数对象的 name 属性将会变成一个空字符串. name 属性是 JavaScript 语言的一个扩展(它并不是 ECMA 标准的一部分), 但是在许多环境中得到了广泛的应用. 如果保留了第二个 add, 那么 add.name 属性将会包含字符串 "add". 当使用调试器时, 或者当从自身递归调用同一个函数时, name 属性时非常有用的, 否则, 可以跳过该属性.
最后, 获得了函数声明(function declaration). 这些声明看起来与其他语言中所使用的函数极为相似:
- function foo() {
- // 此处为函数主体
- }
就语法而言, 命名函数表达式与函数的声明看起来很相似, 尤其是如果不将函数表达式的结果分配给变量 (后面的回调模式中会看到) 的时候. 有时候, 没有其他方法可以区分出函数声明和命名函数表达式的差异, 除非查看函数出现的上, 下文预警, 正如将在下一节中所看到的.
在尾随的分号中, 这两者之间在语法上存在差异. 函数声明中并不需要分号结尾, 但在函数表达式中需要分号, 并且应该总是使用分号, 及时编辑其中分号自动插入机制可能帮您完成了这个工作.
函数字面量, 这个术语也经常被使用, 它可能表示一个函数表达式或命名函数表达式. 由于这种模糊性含义, 并不推荐使用该术语.
声明 vs 表达式: 名称和变量声明提升
因此, 应该使用哪种方法? 函数声明还是函数表达式? 在不能使用声明的情况下, 下面将为您解决这种困境.
- // 这是一个函数表达式
- // 它作为参数传递给函数 "callMe"
- callMe(function (){
- // 这里, 即该函数是一个匿名函数表达式
- // 也被称为匿名函数
- });
- // 这是一个命名函数表达式
- callMe(function me() {
- // 这里, 即 me, 是命名函数表达式
- // 名称是 me
- });
- // 另一个函数表达式
- var myobj = {
- say:function() {
- // 这里是函数表达式
- }
- };
上面的代码, 展示了将函数对象作为参数传递, 或者在对象字面量中定义方法.
注意了: 函数声明只能出现在 "程序代码" 中, 这表示它仅能在其它函数体内部或全局空间中. 它们的定义不能分配给变量或者属性, 也不能以参数形式出现在函数调用中.
- // 全局作用域
- function foo() {}
- function local() {
- // 局部作用域
- function bar() {}
- return bar;
- }
上面的代码, foo(),bar(),local()都是以函数声明模式进行定义的.
函数的命名属性
当选择函数定义模式的时候, 另一个需要考虑的事情是有关制度 name 属性的可用性. 同样, 这个属性并不是标准, 但在许多环境中都可以使用它. 在函数声明和命名函数表达式中, 已经定义了 name 属性. 在匿名函数表达式中, 他依赖于其实现方式. 其 name 可能是为定义的, 也可能是空字符串来定义 name 属性.
- function foo(){
- } // 声明
- var bar = function (){
- }; // 表达式
- var baz = function baz() {
- }; // 命名表达式
- console.log(foo.name); // 输出 "foo"
- console.log(bar.name); // 输出 "bar"
- console.log(baz.name); // 输出 "baz"
注意, 这里的一个区别, 就是在现代浏览器中, 若把一个匿名函数表达式赋值给一个变量, 那么此时, 匿名函数表达式的 name 属性即该变量的名字. 因版本迭代原因, 这与书中描述有些出入.
name 属性在调试 bug 和递归调用自身时很有用. 其他场景可选择使用匿名函数表达式即可.
- var foo = function bar() {
- };
- console.log(foo.name)
这样做也是可以的, 打印出得结果是 bar. 这在技术上是没问题的, 但是会存在一些兼容问题, 所以不建议这样使用.
函数的提升
从前面的讨论中, 可能会得出函数声明的行为几乎等同于命名函数表达式的行为. 然而这并不是完全正确, 其区别在于提升 (hoisting) 行为.
对于所有变量, 无论在函数体的何处进行声明, 都会在后台被提升到函数顶部. 而这对于函数同样适用, 其原因在于函数只是分配给变量的对象."明白" 的地方在于当使用函数声明时, 函数定义也被提升, 而不仅仅是函数声明被提升.
- // 反模式
- // 全局函数
- function foo() {
- console.log("global foo");
- }
- function bar() {
- console.log("global bar");
- }
- function hoistMe() {
- // 在这里是为了判断提升的内容到底是什么, 仅仅是变量名? 还是连带函数体一起?
- console.log(typeof foo);
- console.log(typeof bar);
- // 执行
- foo();
- bar();
- // 函数声明
- // 变量 "foo" 以及其实现者被提升
- function foo() {
- console.log('local foo');
- }
- // 函数表达式
- // 仅变量'bar'被提升
- // 函数实现未被提升
- var bar = function (){
- console.log('local bar');
- };
- }
- hoistMe();
在这个例子中我们可以看到, 如同正常的变量一样, 仅存在与 hoistMe()函数中的 foo 和 bar 移动到了顶部, 从而覆盖了全局 foo 和 bar 函数. 两者之间的区别在于局部 foo()的定义被提升到顶部且能正常运行, 即使在后面才定义它. bar()的定义并没有被提升, 仅有他的声明被提升. 这就是为什么代码执行到达 bar()的定义时, 其显示结果是 undefined 且并没有作为函数来调用 (然而, 在作用域链中, 仍然防止全局 bar() 被 "看到").
最后强调一下函数的两个特征: 它们都是对象, 它们提供局部作用域.
好了, 这篇有关函数的基本情况和定义大家都了解了. 下一篇我们继续.
来源: https://www.cnblogs.com/zaking/p/12572842.html