从这篇开始,我们会用很长的章节来讨论函数,这个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()被“看到”)。

  最后强调一下函数的两个特征:它们都是对象,它们提供局部作用域。

  好了,这篇有关函数的基本情况和定义大家都了解了。下一篇我们继续。

JavaScript 模式》读书笔记(4)— 函数1的更多相关文章

  1. JavaScript模式读书笔记 第4章 函数

    2014年11月10日 1.JavaScript函数具有两个特点: 函数是第一类对象    函数能够提供作用域         函数即对象,表现为:         -1,函数能够在执行时动态创建,也 ...

  2. JavaScript模式读书笔记 文章3章 文字和构造

    1.对象字面量     -1.Javascript中所创建的自己定义对象在任务时候都是可变的.能够从一个空对象開始,依据须要添加函数.对象字面量模式能够使我们在创建对象的时候向其加入函数.       ...

  3. 《你不知道的javascript》读书笔记2

    概述 放假读完了<你不知道的javascript>上篇,学到了很多东西,记录下来,供以后开发时参考,相信对其他人也有用. 这篇笔记是这本书的下半部分,上半部分请见<你不知道的java ...

  4. 《编写可维护的javascript》读书笔记(中)——编程实践

    上篇读书笔记系列之:<编写可维护的javascript>读书笔记(上) 上篇说的是编程风格,记录的都是最重要的点,不讲废话,写的比较简洁,而本篇将加入一些实例,因为那样比较容易说明问题. ...

  5. C语言深度解剖读书笔记(6.函数的核心)

    对于本节的函数内容其实就没什么难点了,但是对于函数这节又涉及到了顺序点的问题,我觉得可以还是忽略吧. 本节知识点: 1.函数中的顺序点:f(k,k++);  这样的问题大多跟编译器有关,不要去刻意追求 ...

  6. Javascript & JQuery读书笔记

    Hi All, 分享一下我学JS & JQuery的读书笔记: JS的3个不足:复杂的文档对象模型(DOM),不一致的浏览器的实现和便捷的开发,调试工具的缺乏. Jquery的选择器 a. 基 ...

  7. 《你不知道的javascript》读书笔记1

    概述 放假读完了<你不知道的javascript>上篇,学到了很多东西,记录下来,供以后开发时参考,相信对其他人也有用. js的工作原理 引擎:从头到尾负责整个js的编译和运行.(很大一部 ...

  8. JavaScript中的this—你不知道的JavaScript上卷读书笔记(三)

    this是什么? this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件.this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式.当一个函数被调用时,会 ...

  9. JavaScript词法作用域—你不知道的JavaScript上卷读书笔记(一)

    前段时间在每天往返的地铁上抽空将 <你不知道的JavaScript(上卷)>读了一遍,这本书很多部分写的很是精妙,对于接触前端时间不太久的人来说,就好像是叩开了JavaScript的另一扇 ...

  10. SQL反模式读书笔记思维导图

    在写SQL过程以及设计数据表的过程中,我们经常会走一些弯路,会做一些错误的设计.<SQL反模式>这本书针对这些经常容易出错的设计模式进行分析,解释了错误的理由.允许错误的场景,并给出更好的 ...

随机推荐

  1. Docker学习笔记_04 Rancher的部署安装(编排选用K8S)

    原文地址:http://dbase.cc/2018/01/12/docker/04_rancher的部署安装/ 为什么要使用Rancher Rancher是一个开源的企业级容器管理平台.通过Ranch ...

  2. HashMap底层分析

    以下基于 JDK1.7 分析. 如图所示,HashMap 底层是基于数组和链表实现的.其中有两个重要的参数: 容量 负载因子 容量的默认大小是 16,负载因子是 0.75,当 HashMap 的 si ...

  3. HTTP接口抓包工具之Fiddler

    Fiddler的基本功能介绍: Fiddler是最强大最好用的Web调试工具之一,它能记录所有客户端和服务器的http和https请求,允许你监视,设置断点,甚至修改输入输出数据,Fiddler包含了 ...

  4. 初入 Ubuntu 的一些操作 · Lei's blog

    查看系统版本 cat /etc/os-release 修改 root 密码 passwd 新建用户 新建用户: adduser username 将新用户加入 sudo 组,这样就可以用 sudo 命 ...

  5. hexo-next-travis-ci 构建自动化部署博客

    构建效果如上面视频所示,如果浏览器不支持请戳一下链接: 自动化部署构建效果 .只要将编辑的 .md 文件推送到 github 上,博客网站就可以更新这篇文章. 其实差不多半年前也构建过一次,由于安装 ...

  6. xshell6使用的命令

    我们进入Xshell的界面之后连接上Linux服务器 常用命令: (1)命令ls——列出文件 ls -la 给出当前目录下所有文件的一个长列表,包括以句点开头的“隐藏”文件 ls a* 列出当前目录下 ...

  7. MySQL之单表多表查询

    #1.单表查询 #单表查询语法 select <字段1,字段2....> from <表名> where <表达式> group by field 分组 havin ...

  8. Java设计模式之结构模式

    一.外观模式 分析:外观模式是为子系统的一组接口提供一个统一的界面,数据库JDBC连接应用就是外观模式的一个典型例子,特点:降低系统的复杂度,增加灵活性.结果:代码示例: public class D ...

  9. 从头认识js-基本概念(关键字,保留字,数据类型)

    语法 ECMAScript的语法大量借鉴了C及其他类C语言(如Java和Perl)的语法.因此,熟悉这些语言的开发人员在接受ECMSAScript更加宽松的语法时,一定会有一种轻松自在的感觉. 区分大 ...

  10. JavaScript 工作原理之十三-CSS 和 JS 动画底层原理及如何优化其性能

    原文请查阅这里,本文采用知识共享署名 4.0 国际许可协议共享,BY Troland. 本系列持续更新中,Github 地址请查阅这里. 这是 JavaScript 工作原理的第十三章. 概述 正如你 ...