函数进阶
递归函数
递归函数是一个一直直接或者间接调用它自己本身,直到满足某个条件才会退出的函数。当需要 设计到迭代过程时,这是一个很有用的工具
使用递归计算从m加到n
使用递归计算出某一位的斐波那契数
使用递归打印出多维数组里面的每一个数字
高阶函数
高阶函数介绍
高阶函数(higher-order-function)指的是操作函数的函数,一般有以下两种情况:
・函数可以作为参数被传递
・函数可以作为返回值输出
JavaScript中的函数显然满足高阶函数的条件,在实际开发中,无论是将函数当作参数传递,还 是让函数的执行结果返回另外一个函数,这两种情形都有很多应用场景。下面将对这两种情况进 行详细介绍
参数传递
回调函数
前面无论是在介绍函数基础的时候,还是在介绍异步编程的时候,我们都有接触过回调函数。回 调函数,就是典型的将函数作为参数来进行传递。
在Ajax异步请求的应用中,回调函数的使用非常频繁。想在Ajax请求返回之后做一些事情,但又 并不知道请求返回的确切时间时,最常见的方案就是把回调函数当作参数传入发起Ajax请求的方 法中,待请求完成之后执行回调函数。俄们将在14章详细介绍Ajax技术)
回调函数的应用不仅只在异步请求中,当一个函数不适合执行一些请求时,也可以把这些请求封 装成一个函数,并把它作为参数传递给另外一个函数,"委托"给另外一个函数来执行。比如,想 在页面中创建100个div节点,然后把这些div节点都设置为隐藏。
把div.style.display = 'none'的逻辑硬编码在appendDiv里显然是不合理
的,appendDiv未免有点个性化,成为了一个难以复用的函数,并不是每个人创建了节点之后 就希望它们立刻被隐藏,于是把div.style.display = 'none'这行代码抽出来,用回调函数 的形式传入appendDiv()方法
可以看到,隐藏节点的请求实际上是由客户发起的,但是客户并不知道节点什么时候会创建好, 于是把隐藏节点的逻辑放在回调函数中,"委托"给appendDiv()方法。appendDiv()方法当然 知道节点什么时候创建好,所以在节点创建好的时候,appendDiv()会执行之前客户传入的回 调函数。
数组排序
前面在介绍数组排序时有介绍过sor t()方法,该方法就接收一个函数作为参数。sort()方法 封装了数组元素的排序方法。从sor t()方法的使用可以看到,我们的目的是对数组进行排序, 这是不变的部分;而使用什么规则去排序,则是可变的部分。把可变的部分封装在函数参数里, 动态传入sor t()方法,使sor t()方法方法成为了一个非常灵活的方法
除了这个sort()方法以外,还有诸如for Each() , map() , eve ry() , some()等函数,也 是常见的回调函数
返回值输出
判断数据的类型
我们来看下面的例子,判断一个数据是否为数组。在以往的实现中,可以判断这个数据有没 有length属性,有没有sort方法或者slice方法等。
面向切面编程
AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务 逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来之后,再通 过"动态织入"的方式掺入业务逻辑模块中。这样做的好处首先是可以保持业务逻辑模块的纯净和 高内聚性,其次是可以很方便地复用日志统计等功能模块
在Java语言中,可以通过反射和动态代理机制来实现AOP技术。而在JavaScript这种动态语言 中,AOP的实现更加简单,这是JavaScript与生俱来的能力。通常,在JavaScript中实现AOP, 都是指把一个函数"动态织入"到另外一个函数之中。下面通过扩展Function.p rototype来实 现,
把负责打印数字1和打印数字3的两个函数通过AOP的方式动态植入func()函数。通过执行上面 的代码,控制台顺利地返回了执行结果1、2、3。
本小结介绍了高阶函数的基础使用,主要包括参数传递和返回值输出两种形式。其中,高阶函数 的一个重要应用是函数柯里化(currying),将在下一小节中进行详细介绍。
闭包
闭包:可以在一个函数的外部可以访问并使用它内部的变量,这就是闭包
闭包基本介绍
首先我们来看看为什么要使用闭包,
例子中声明了一个名为eat的函数,并对它进行调用。js引擎会创建一个eat的执行上下文,其中 声明food变量并赋值。当该方法执行完后,上下文被销毁,food变量也会跟着消失。这是因为 food变量属于eat函数的局部变量,它作用于eat函数中,会随着eat的执行上下文创建而创建,销 毁而销毁。
再看下面这个例子:
自由变量可以理解成跨作用域的变量,比如子作用域访问父作用域的变量。
这些概念说起来太过于专业,下面我用一个生活中的例子来解释它。假如小王、小强是某某学校 计算机专业的学生。某一天他们的导师在外面接了一个项目,然后为此成立一个项目组,拉了他 两人加入。他们的这个项目组既在学校中建立,但又可以独立于学校存在。比如,小王和小强从 学校毕业了,他们的项目组仍然可以继续存在。所以,我们可以认为他们组建的这个项目组就是 闭包。下面我把这个例子写成了代码:
变量si和s2属于school函数的局部变量,并被内部函数team使用,同时该函数也被返回出去。 在外部,通过调用scho ol得到了内部函数的引用,后面多次调用这个内部函数,仍然能够访问到 si和s2变量。这样si和s2作为自由变量被tea m函数引用,即使创造它们的函数school执行完 了,这些变量依然存在,因此,这就是闭包。
狭义的闭包必须满足两个条件
・形成闭包环境的函数能够被外部变量引用,这样就算它外部上下文销毁,它依然存在。
在内部函数中要访问外部函数的局部变量。
后面我提到的闭包都是指要满足这两个条件。下面我们来看看闭包有哪些优缺点,先来看看优 点
・通过闭包可以让外部环境访问到函数内部的局部变量。
・通过闭包可以让局部变量持续保存下来,不随着它的上下文环境一起销毁
这个例子是对一个全局变量进行加1的操作,一共加100次,得到值为100的结果。下面用闭包的 方式重构它:
到底什么时候会用到闭包。比如我们经常会使用时间函数对某一个变量进行操 作
这个例子用到了时间函数setTimeout,并在等待1秒钟后对变量a进行加1的操作。这是一个闭 包,因为setTimeout中的匿名函数对外部变量进行访问,然后该函数又被setTimeout方法引用。 满足了形成闭包的两个条件。所以你看,即使外部上下文结束了,1秒后仍然能对变量a进行加1 操作。
在DOM的事件操作中,也经常用到闭包
onclick指向的函数中访问了外部变量ent,同时该函数又被o nclick事件引用了,满足两个条件, 是闭包。所以当外部上下文结束后,你继续点击按钮,在触发的事件处理方法中仍然能访问到变 量 ent。
在有些时候闭包还会引起一些奇怪的问题,
闭包的更多作用
1.封装变量
闭包可以帮助把一些不需要暴露在全局的变量封装成"私有变量"。假设有一个计算乘积的简单函 数:
mult函数接受一些number类型的参数,并返回这些参数的乘积。现在我们觉得对于那些相同的 参数来说,每次都进行计算是一种浪费,我们可以加入缓存机制来提高这个函数的性能
我们看到,cache这个变量仅仅是在mult函数中被使用,与其让cache变量跟mult函数一起平行地 暴露在全局作用域下,不如把它封闭在mult函数内部,这样可以减少页面中的全局变量,以避免 这个变量在其他对方被不小心修改而引发错误
提炼函数是代码重构中的一种常见技巧。如果在一个大函数中有一些代码块能够独立出来,那么 我们常常把这些代码块封装在独立的小函数里面。独立出来的小函数有助于代码的复用,如果这 些小函数有一个良好的命名,它们本身也起到了注释的作用。如果这些小函数不需要在程序的其 他地方使用,那么最好是把它们用闭包封闭起来
2.延续局部变量的寿命
img对象经常用于进行数据上报
但是通过查询后台的记录我们得知,因为一些低版本的浏览器的实现存在bug,在这些浏览器下 使用report函数进行数据上报时会丟失30%左右的数据,也就是说‘report函数并不是每一次都 成功发起了 HTTP请求。丟失数据的原因是img是report函数中的局部变量,当report函数在调用 结束后,img局部变量随即被销毁,而此时或许还没来得及发出HTTP请求,所以此次请求就会丟 失掉。
现在我们把img变量用闭包封闭起来,便能解决请求丟失的问题
闭包和面向对象设计
过程与数据的结合是形容面向对象中的"对象"时经常使用的表达。对象以属性的形式包含了数 据,以方法的形式包含了过程。而闭包则是在过程中以环境的形式包含了数据。通常用面向对象 思想能实现的功能,用闭包也能够实现,反之亦然。
在JavaScript语言的祖先Scheme语言中,甚至都没有提供面向对象的原生设计,但却可以使用 闭包来实现一个完整的面向对象的系统
如果换成面向对象的写法
或者
变量初始化
执行上下文
在ECMAScript中代码的运行环境分为以下三种:
・全局级别的代码:这是默认的代码运行环境,一旦代码被载入,JS引擎最先进入的就是这个 环境
・函数级别的代码:当执行一个函数时,运行函数体中的代码。
・EvaI级别的代码:在EvaI函数内运行的代码。
为了便于理解,我们可以将"执行上下文"粗略的看做是当前代码的运行环境或者说是作用域。下 面我们来看一个例子,其中包括了全局以及函数级别的执行上下文
上面这段代码,本身是没有什么意义的,我们主要是要使用这段代码来分析一下里面存在多少个 上下文。在上面的代码中,一共存在4个上下文。一个全局上下文,一个test函数上下文,一个 test2函数上下文和一个test3函数上下文。
通过上面的例子,我们就可以得出下面的结论:
・不管什么情况下,只存在一个全局的上下文,该上下文能被任何其它的上下文所访问到。也 就是说,我们可以在test的上下文中访问到全局上下文中的o ne变量,当然在函数test2或者 test3中同样可以访问到该变量。
・至于函数上下文的个数是没有任何限制的,每到调用执行一个函数时,引擎就会自动新建出 —个函数上下文,换句话说,就是新建一个局部作用域,可以在该局部作用域中声明私有变 量等,在外部的上下文中是无法直接访问到该局部作用域内的元素的。
执行上下文堆栈
JS引擎的工作方式是单线程的。也就是说,某一个时候只有唯一的一个事件是处于被激活的,其 他的事件都是被放入队列中,等待被处理的。下面的示例图就描述了这样一个堆栈,
我们已经知道,当JS代码文件被JS引擎载入后,默认最先进入的是一个全局的执行上下文。当 在全局上下文中调用执行一个函数时,程序流就进入该被调用函数内,此时引擎就会为该函数创 建一个新的执行上下文,并且将其压入到执行上下文堆栈的顶部
JS引擎总是执行当前在堆栈顶部的上下文,一旦执行完毕,该上下文就会从堆栈顶部被弹出,然 后,进入其下的上下文执行代码。这样,堆栈中的上下文就会被依次执行并且弹出堆栈,直到回 到全局的上下文。
上述foo被声明后,通过0运算符强制直接运行了。函数代码就是调用了其自身4次,每次是局部 变量i增加1。每次foo函数被自身调用时,就会有一个新的执行上下文被创建。每当一个上下文执 行完毕,该上下文就被弹出堆栈,回到上一个上下文,直到再次回到全局上下文。所以在本段代 码中一共存在了 5个不同的执行上下文
由此可见,对于执行上下文这个抽象的概念,可以归纳为以下几点
・单线程
•同步执行
・唯一的一个全局上下文
・函数的执行上下文的个数没有限制
・每次某个函数被调用,就会有个新的执行上下文为其创建,即使是调用的自身函数,也是如 此。
函数上下文的建立与激活
我们现在已经知道,每当我们调用一个函数时,一个新的执行上下文就会被创建出来。然而,在 js引擎的内部,这个上下文的创建过程具体分为两个阶段,分别是建立阶段和代码执行阶段。这 两个阶段要做的事儿也不一样。
建立阶段:发生在当调用一个函数,但是在执行函数体内的具体代码之前
•建立变量对象(arguments对象,形式参数,函数和局部变量)
•初始化作用域链
・确定上下文中this的指向对象
代码执行阶段:发生在具体开始执行函数体内的代码的时候
・执行函数体内的每一句代码
我们将建立阶段称之为函数上下文的建立,将代码执行阶段称之为函数上下文的激活
变量对象
在上面介绍函数两个阶段中的建立阶段时,提到了一个词,叫做变量对象。这其实是将整个上下 文看做是一个对象以后得到的一个词语。具体来讲,我们可以将整个函数上下文看做是一个对 象,那么既然是对象,对象就应该有相应的属性。对于我们的执行上下文来说,有如下的三个属 性:
可以看到,这里我们的执行上下文对象有3个属性,分别是变量对象,作用域链以及this,这里我 们重点来看一下变量对象里面所拥有的东西。
在函数的建立阶段,首先会建立arguments对象。然后确定形式参数,检查当然上下文中的函数 声明,每找到一个函数声明,就在variableObject下面用函数名建立一个属性,属性值就指向该 函数在内存中的地址的一个引用。如果上述函数名已经存在于variableObject(简称V0)下面,那 么对应的属性值会被新的引用给覆盖。最后,是确定当前上下文中的局部变量,如果遇到和函数 名同名的变量,则会忽略该变量。
好,接下来我们来通过一个实际的例子来演示函数的这两个阶段以及变量对象是如何变化的。
首先在建立阶段的变量对象如下:
由此可见,在建立阶段,除了arguments,函数的声明,以及形式参数被赋予了具体的属性值 外,其它的变量属性默认的都是undefinedo并且普通形式声明的函数的提升是在变量的上面 的。
只有在代码执行阶段,局部变量才会被赋予具体的值。在建立阶段局部变量的值都是 undefined。这其实也就解释了变量提升的原理
作用域链
前面在讲解函数上下文时,我们将上下文看做了是一个对象,这个对象有3个属性,分别是变量 对象,作用域链以及this指向。关于this指向我们之前已经介绍过了,变量对象也在上面做了相关 的介绍,最后我们就一起来看一下这个作用域链。
所谓作用域链,就是内部上下文所有变量对象(包括父变量对象)的列表。此链主要是用于变量查 询。
关于作用域链,有一^公式作用域链(ScopeChain) = AO + [[scope]]
其中A0,简单来说就是VO, AO全称为active object(活动对象),对于当前的上下文来讲,一般 将其称之为A0,对于不是当前的上下文,一般被称为V0
[[scope]]:所有父级变量对象的层级列表(也被称之为层级链)
这里,我们来分析一下VO和AO
这里,在全局上下文下的VO就是一个x变量,而在foo函数上下文下面,AO有一个变量y,接下来 是作用域链。作用域链等于当前上下文的AO加上父级的VO,所以在函数内部虽然没有变量x,但 是通过作用域链我们找到了父级上下文下面有一个变量x,然后拿来使用
关于[[scope]],有一个一个非常重要的特性,那就是[[scope]]是在函数创建的时候,就已经被存 储了,是静态的。所谓静态,就是说永远不会变,函数可以永远不被调用,但是[[scope]]在创建 的时候就已经被写入了,并且存储在函数作用域链对象里面。我们来举一个例子说明
这里的结果为eat rice,原因非常简单。因为对于eat()函数来讲,创建的时候它的父级是全局 上下文,所以[[scope]]里面就存储了全局上下文的VO,所以food的值为rice。如果我们将代码稍 作修改,改成如下:
那么这个时候打印出来的值就为eat noodle。因为对于eat()函数来讲,这个时候它的父级为
IIFE,所以[[scope]]里面存储的是IIFE这个函数上下文的VO, food的值为noodle。
最后,我们用一个稍微复杂一些的例子来贯穿上面所介绍的作用域链。
在这里,我们来分析一下变量对象,函数的[[scope]]属性以及上下文作用域链的变化。 首先,刚开始的时候是全局上下文的变量对象:
在foo()函数被创建时,此时foo()函数的[[scope]]属性为:
之后,foo()函数会被激活,确定foo()函数里面的活动对象:
此时foo()函数上下文里面的作用域链的结构为:
当内部函数bar ()函数被创建时,其[[scope]]为:
当bar ()函数被激活,拥有活动对象时,bar()函数的活动对象如下:
此时bar ()函数上下文的作用域链的结构为:
对x, y , z的标识符解析如下:
最后总结一下:函数的[[scope]]属性是在函数创建时就确定了,而变量对象则是在函数激活时, 也就是说调用函数时才会确定。
立即执行函数表达式
立即执行的函数表达式的英文全称为Immediately Invoked Function Expression,简称就为IIFE。这是一个如它名字所示的那样,在定义后就会被立即调用的函数
我们在调用函数的时候需要加上一对括号,IIFE同样如此。除此之外,我们还需要将函数变为一个表达式,只需要将整个函数的声明放进括号里面就可以实现
IIFE可以执行一个任务的同时,将所有的变量都封装到函数的作用域里面,从而保证了全局的命名空间不会被很多变量名污染
这里我可以举一个简单的例子,在以前我们要交换两个数的时候,往往需要声明第三个临时变量temp
这样虽然我们的两个变量被交换了,但是存在一个问题,那就是我们在全局环境下也存在了一个temp变量,这就可以被称之为污染了全局环境。所以我们可以使用IIFE来解决该问题
这是一个非常方便的功能,特别是有些时候我们在初始化一些信息时需要一些变量的帮助,但是这些变量除了初始化之后就再也不会用了,那么这个时候我们就可以考虑使用IIFE来进行初始化,这样不会无言到全局环境
这里我们只是想要输出一条欢迎信息,附上当天的日期和星期几,但是有一个很尴尬的地方在于 上面定义的这些变量我们都只使用一次,后面就不会再用了,所以这个时候我们也是可以考虑使 用IIFE来避免这些无用的变量声明。
通过IIFE,我们可以对我们的代码进行分块。并且块与块之间不会互相影响,哪怕有同名的变量 也没问题,因为IIFE也是函数,在函数内部声明的变量是一个局部变量
在var流行的时代,JS是没有块作用域的。什么叫做块作用域呢?目前我们所知的作用域大概 有两种:全局作用域和函数作用域。其中,全局作用域是指声明的变量可在当前环境的任何地方 使用。函数作用域则只能在当前函数所创造的环境中使用。块级作用域是指每个代码块也可以有 自己的作用域,比如在if块中声明一个变量,就只能在当前代码块中使用,外面无法使用。而 用var声明的变量是不存在块级作用域的,所以即使在if块中用var声明变量,它也能在外 部的函数或者全局作用域中使用。
这个例子中,a变量是在if块中声明,但是它的外部仍然能输出它的结果。
解决这个问题有两种方法,第一:使用ES6中的let关键字声明变量,这样它就有块级作用域。 第二:使用IIFE,
当然,只要浏览器支持,建立尽量使用let的方式来声明变量。