第12章函数进阶node
12-1当即执行函数表达式程序员
当即执行的函数表达式的英文全称为Immediately Invoked Function Expression,简称就为IIFE。这是一个如它名字所示的那样,在定义后就会被当即调用的函数。ajax
咱们在调用函数的时候须要加上一对括号,IIFE一样如此。除此以外,咱们还须要将函数变为一 个表达式,只须要将整个函数的声明放进括号里面就能够实现。具体的语法以下:编程
(function(){ //函数体 })() 接下来咱们来看一个具体的示例: (function(){ console.log("Hello"); })() // Hello
IIFE能够在执行一个任务的同时,将全部的变量都封装到函数的做用域里面,从而保证了全局的 命名空间不会被不少变量名污染。
数组
这里我能够举一个简单的例子,在之前咱们要交换两个数的时候,每每须要声明第三个临时变量浏览器
temp缓存
|注:从ES6开始已经不须要这么作了,直接使用结构就能够交换两个数了安全
let a = 3,b = 5; let temp = a; a = b; b = temp; console.log(a);//5 console.log(b);//3 console.log(temp);//3
这样虽然咱们的两个变量被交换了,可是存在一个问题,那就是咱们在全局环境下也存在了一个 temp变量,这就能够被称之为污染了全局环境。因此咱们可使用IIFE来解决该问题,以下:微信
let a = 3,b = 5; (function(a,b){ let temp = a; a = b; b = temp; })(a,b) console.log(a);//3 console.log(b);//5 console.log(temp);//报错
这是一个很是方便的功能,特别是有些时候咱们在初始化一些信息时须要一些变量的帮助,可是 这些变量除了初始化以后就不再会用了,那么这个时候咱们就能够考虑使用IIFE来进行初始 化,这样不会污染到全局环境。闭包
( function(){ let days =["星期天",”星期一","星期二",”星期三",”星期四",”星期五",”星期六"]; let date = new Date(); let today = [date.toLocaleDateString(),days[date.getDay()]]; console.log('今天是${today[0]}, ${today[1]},欢迎你回来! '); })() //今天是2017-12-20,星期三,欢迎你回来!
这里咱们只是想要输出一条欢迎信息,附上当天的日期和星期几,可是有一个很尴尬的地方在于 上面定义的这些变量咱们都只使用一次,后面就不会再用了,因此这个时候咱们也是能够考虑使 用IIFE来避免这些无用的变量声明。
经过IIFE,咱们能够对咱们的代码进行分块。而且块与块之间不会互相影响,哪怕有同名的变量 也没问题,由于IIFE也是函数,在函数内部声明的变量是一个局部变量,示例以下:
(function(){ //block A let name = "xiejie"; console.log('my name is ${name}'); })(); ( function(){ //block B let name = "song"; console.log('my name is ${name}'); })(); // my name is xiejie // my name is song
在var流行的时代,JS是没有块做用域的。什么叫作块做用域呢?目前咱们所知的做用域大概 有两种:全局做用域和函数做用域。其中,全局做用域是指声明的变量可在当前环境的任何地方 使用。函数做用域则只能在当前函数所创造的环境中使用。块级做用域是指每一个代码块也能够有 本身的做用域,好比在if块中声明一个变量,就只能在当前代码块中使用,外面没法使用。而 用var声明的变量是不存在块级做用域的,因此即便在if块中用var声明变量,它也能在外 部的函数或者全局做用域中使用。
function show(valid){ if(valid){ var a = 100; } console.log('a:',a); } show(true); // 输出a的值为100
这个例子中,a变量是在if块中声明,可是它的外部仍然能输出它的结果。
解决这个问题有两种方法,第一:使用ES6中的let关键字声明变量,这样它就有块级做用域。 第二:使用IIFE,示例以下:
function show(valid){ if(valid){ ( function(){ var a = 100; })(); } console.log('a:',a); } show(true); // 报错:a is not defined
固然,只要浏览器支持,创建尽可能使用let的方式来声明变量。
12-2变量初始化
12-2-1执行上下文
在ECMAScript中代码的运行环境分为如下三种:
・全局级别的代码:这是默认的代码运行环境,一旦代码被载入,JS引擎最早进入的就是这个 环境
・函数级别的代码:当执行一个函数时,运行函数体中的代码。
・EvaI级别的代码:在EvaI函数内运行的代码。
为了便于理解,咱们能够将"执行上下文"粗略的看作是当前代码的运行环境或者说是做用域。下 面咱们来看一个例子,其中包括了全局以及函数级别的执行上下文,以下:
let one = "Hello"; let test = function(){ let two = "Lucy",three = "Bill"; let test2 = function(){ console.log(one,two); } let test3 = function(){ console.log(one,three); } test2(); test3(); } test();
上面这段代码,自己是没有什么意义的,咱们主要是要使用这段代码来分析一下里面存在多少个 上下文。在上面的代码中,一共存在4个上下文。一个全局上下文,一个test函数上下文,一个 test2函数上下文和一个test3函数上下文。
经过上面的例子,咱们就能够得出下面的结论:
・无论什么状况下,只存在一个全局的上下文,该上下文能被任何其它的上下文所访问到。也 就是说,咱们能够在test的上下文中访问到全局上下文中的o ne变量,固然在函数test2或者 test3中一样能够访问到该变量。
・至于函数上下文的个数是没有任何限制的,每到调用执行一个函数时,引擎就会自动新建出 —个函数上下文,换句话说,就是新建一个局部做用域,能够在该局部做用域中声明私有变 量等,在外部的上下文中是没法直接访问到该局部做用域内的元素的。
在上述例子中,内部的函数能够访问到外部上下文中的声明的变量,反之则行不通。那么,这到 底是什么缘由呢?引擎内部是如何处理的呢?这须要了解执行上下文堆栈。
执行上下文堆栈
JS引擎的工做方式是单线程的。也就是说,某一个时候只有惟一的一个事件是处于被激活的,其 他的事件都是被放入队列中,等待被处理的。下面的示例图就描述了这样一个堆栈,以下:
咱们已经知道,当JS代码文件被JS引擎载入后,默认最早进入的是一个全局的执行上下文。当 在全局上下文中调用执行一个函数时,程序流就进入该被调用函数内,此时引擎就会为该函数创 建一个新的执行上下文,而且将其压入到执行上下文堆栈的顶部。
JS引擎老是执行当前在堆栈顶部的上下文,一旦执行完毕,该上下文就会从堆栈顶部被弹出,然 后,进入其下的上下文执行代码。这样,堆栈中的上下文就会被依次执行而且弹出堆栈,直到回 到全局的上下文。
来看下面这段代码,分析其一共有多少个上下文:
(function foo(i){
if(i==3){ return; }else{ console.log(i); foo(++i); } })(0); //全局上下文 //函数上下文0 //函数上下文1 //函数上下文2 //函数上下文
上述foo被声明后,经过0运算符强制直接运行了。函数代码就是调用了其自身4次,每次是局部 变量i增长1。每次foo函数被自身调用时,就会有一个新的执行上下文被建立。每当一个上下文执 行完毕,该上下文就被弹出堆栈,回到上一个上下文,直到再次回到全局上下文。因此在本段代 码中一共存在了 5个不一样的执行上下文。
因而可知,对于执行上下文这个抽象的概念,能够概括为如下几点:
・单线程
•同步执行
・惟一的一个全局上下文
・函数的执行上下文的个数没有限制
・每次某个函数被调用,就会有个新的执行上下文为其建立,即便是调用的自身函数,也是如此。
12-2-2函数上下文的创建与激活
咱们如今已经知道,每当咱们调用一个函数时,一个新的执行上下文就会被建立出来。然而,在 js引擎的内部,这个上下文的建立过程具体分为两个阶段,分别是创建阶段和代码执行阶段。这 两个阶段要作的事儿也不同。
创建阶段:发生在当调用一个函数,可是在执行函数体内的具体代码以前
•创建变量对象(arguments对象,形式参数,函数和局部变量)
•初始化做用域链
・肯定上下文中this的指向对象
代码执行阶段:发生在具体开始执行函数体内的代码的时候
・执行函数体内的每一句代码
咱们将创建阶段称之为函数上下文的创建,将代码执行阶段称之为函数上下文的激活。
变量对象
在上面介绍函数两个阶段中的创建阶段时,提到了一个词,叫作变量对象。这实际上是将整个上下 文看作是一个对象之后获得的一个词语。具体来说,咱们能够将整个函数上下文看作是一个对 象,那么既然是对象,对象就应该有相应的属性。对于咱们的执行上下文来讲,有以下的三个属 性:
executionContextObj = {
variableObject : {}, //变量对象,里面包含arguments对象,形式参数,函数和局部变量 scopeChain : {},//做用域链,包含内部上下文全部变量对象的列表 this : {}//上下文中this的指向对象
}
能够看到,这里咱们的执行上下文对象有3个属性,分别是变量对象,做用域链以及this,这里我 们重点来看一下变量对象里面所拥有的东西。
在函数的创建阶段,首先会创建arguments对象。而后肯定形式参数,检查固然上下文中的函数 声明,每找到一个函数声明,就在variableObject下面用函数名创建一个属性,属性值就指向该 函数在内存中的地址的一个引用。若是上述函数名已经存在于variableObject(简称V0)下面,那 么对应的属性值会被新的引用给覆盖。最后,是肯定当前上下文中的局部变量,若是遇到和函数 名同名的变量,则会忽略该变量。
好,接下来咱们来经过一个实际的例子来演示函数的这两个阶段以及变量对象是如何变化的。
let foo = function(i){ var a = "Hello"; var b = function privateB(){}; function c(){} } foo(10); 首先在创建阶段的变量对象以下: fooExecutionContext = { variavleObject : { ar guments : {0 : 10,length : 1}, // 肯定arguments 对象 i : \0,//肯定形式参数 c : pointe r to function c(),//肯定函数引用 a : undefined,//局部变量 初始值为undefined b : undefined //局部变量 初始值为undefined }, scopeChain : {}, this : {} }
因而可知,在创建阶段,除了arguments,函数的声明,以及形式参数被赋予了具体的属性值 外,其它的变量属性默认的都是undefinedo而且普通形式声明的函数的提高是在变量的上面 的。
—旦上述创建阶段结束,引擎就会进入代码执行阶段,这个阶段完成后,上述执行上下文对象如
下,变量会被赋上具体的值。
接下来咱们再经过一段代码来加深对函数这两个阶段的过程的理解,代码以下:
(function(){ console.log(typeof foo); console.log(typeof bar); var foo = "Hello"; var bar = function(){ return "World"; } function foo(){ return "good"; } console.log(foo,typeof foo); })()
这里,咱们定义了一个IIFE,该函数在创建阶段的变量对象以下:
fooExecutionContext = { variavleObject : { arguments : {length : 0}, foo : pointer to function foo(), bar : undefined
}, scopeChain : {}, this : {}
首先肯定arguments对象,接下来是形式参数,因为本例中不存在形式参数,因此接下来开始确 定函数的引用,找到foo函数后,建立foo标识符来指向这个foo函数,以后同名的foo变量不会再 被建立,会直接被忽略。而后建立bar变量,不过初始值为undefined。
创建阶段完成以后,接下来进入代码执行阶段,开始一句一句的执行代码,结果以下:
(function(){ console.log(typeof foo);//function console.log(typeof bar);//undefined var foo = "Hello";//foo被从新赋值变成了一个字符串 var bar = function(){ return "World"; } function foo() { return "good"; } console.log(foo,typeof foo);//Hello string })()
12-2-3做用域链
前面在讲解函数上下文时,咱们将上下文看作了是一个对象,这个对象有3个属性,分别是变量 对象,做用域链以及this指向。关于this指向咱们以前已经介绍过了,变量对象也在上面作了相关 的介绍,最后咱们就一块儿来看一下这个做用域链。
所谓做用域链,就是内部上下文全部变量对象(包括父变量对象)的列表。此链主要是用于变量查 询。
关于做用域链,有一^公式做用域链(ScopeChain) = AO + [[scope]]
其中A0,简单来讲就是VO, AO全称为active object(活动对象),对于当前的上下文来说,通常 将其称之为A0,对于不是当前的上下文,通常被称为V0
[[scope]]:全部父级变量对象的层级列表(也被称之为层级链) 举个例子:
var x = 10; function foo() { var y = 20; console.log(x + y); } foo();//30 这里,咱们来分析一下VO和AO //全局 VO : x : 10 foo : pointer to foo() //foo函数上下文 AO : y : 20 ScopeChain : AO(y) + [[scope]](VO:x)
这里,在全局上下文下的VO就是一个x变量,而在foo函数上下文下面,AO有一个变量y,接下来 是做用域链。做用域链等于当前上下文的AO加上父级的VO,因此在函数内部虽然没有变量x,但 是经过做用域链咱们找到了父级上下文下面有一个变量x,而后拿来使用。
关于[[scope]],有一个一个很是重要的特性,那就是[[scope]]是在函数建立的时候,就已经被存 储了,是静态的。所谓静态,就是说永远不会变,函数能够永远不被调用,可是[[scope]]在建立 的时候就已经被写入了,而且存储在函数做用域链对象里面。咱们来举一个例子说明,以下:
let food = "rice"; let eat = function(){ console.log('eat ${food}'); }; ( function(){ let food = "noodle"; eat();//eat rice })();
这里的结果为eat rice,缘由很是简单。由于对于eat()函数来说,建立的时候它的父级是全局 上下文,因此[[scope]]里面就存储了全局上下文的VO,因此food的值为rice。若是咱们将代码稍 做修改,改为以下:
let food = "rice"; ( function(){ let food = "noodle"; let eat = function(){ console.log('eat ${food}'); }; eat();//eat noodle })();
那么这个时候打印出来的值就为eat noodle。由于对于eat()函数来说,这个时候它的父级为
IIFE,因此[[scope]]里面存储的是IIFE这个函数上下文的VO, food的值为noodle。
最后,咱们用一个稍微复杂一些的例子来贯穿上面所介绍的做用域链。
var x = 10; function foo() { var y = 20; function bar() { var z = 30; console.log(x + y + z); } bar(); } foo();// 60
在这里,咱们来分析一下变量对象,函数的[[scope]]属性以及上下文做用域链的变化。 首先,刚开始的时候是全局上下文的变量对象:
globalContext.VO = {
x : 10,
foo : pointer to foo()
}
在foo()函数被建立时,此时foo()函数的[[scope]]属性为:
foo.[[scope]] = [ globalContext.VO
];
以后,foo()函数会被激活,肯定foo()函数里面的活动对象:
fooContext.AO = { y : 20, bar : pointer to bar()
}
此时foo()函数上下文里面的做用域链的结构为:
fooContext.Scope = fooContext.AO + foo.[[scope]] fooContext.Scope = [
fooContext.AO,
globalContext.VO
当内部函数bar ()函数被建立时,其[[scope]]为:
bar.[[scope]] = [
fooContext.AO, globalContext.VO
];
当bar ()函数被激活,拥有活动对象时,bar()函数的活动对象以下:
barContext.AO = {
z : 30
}
此时bar ()函数上下文的做用域链的结构为:
barContext.Scope = barContext.AO + bar.[[scope]] barContext.Scope = [ barContext.AO, fooContext.AO, globalContext.VO
]
最后总结一下:函数的[[scope]]属性是在函数建立时就肯定了,而变量对象则是在函数激活时, 也就是说调用函数时才会肯定。
12-3闭包
对于JavaScript程序员来讲,闭包(closure)是一个难懂又必须征服的概念。接下来我将从四个方 面来描述闭包的概念:
・为何要使用闭包
•什么是闭包
•闭包的原理
•闭包的做用和使用
12-3-1闭包基本介绍
首先咱们来看看为何要使用闭包,先看下面这个例子:
var eat = function(){ var food ="鸡翅"; console.log(food); } eat();
例子中声明了一个名为eat的函数,并对它进行调用。js引擎会建立一个eat的执行上下文,其中 声明food变量并赋值。当该方法执行完后,上下文被销毁,food变量也会跟着消失。这是由于 food变量属于eat函数的局部变量,它做用于eat函数中,会随着eat的执行上下文建立而建立,销 毁而销毁。
再看下面这个例子:
var eat = function(){ var food ="鸡翅"; return function(){ console.log(food); } } var look = eat(); look(); look();
在这个例子中,eat函数返回一个函数,并在这个内部函数中访问food这个局部变量。调用eat函 数并将结果赋给look变量,这个look指向了 eat函数中的内部函数,而后调用它,最终输出food的 值。按照以前的说法,这个food变量应该当eat函数调用完后就销毁,后续为何还能经过调用
look方法访问到这个变量呢?这是由于闭包起了做用。返回的内部函数和它外部的变量food实际 上就是一个闭包。咱们不由想问,为何它们称为闭包?闭包又能作什么呢?
咱们先来看看闭包的概念:
闭包是指引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即便离开了创造它的环境也不例外。这里提到了自由变量,它又是什么呢?
自由变量能够理解成跨做用域的变量,好比子做用域访问父做用域的变量。
这些概念提及来太过于专业,下面我用一个生活中的例子来解释它。假如小王、小强是某某学校 计算机专业的学生。某一天他们的导师在外面接了一个项目,而后为此成立一个项目组,拉了他 两人加入。他们的这个项目组既在学校中创建,但又能够独立于学校存在。好比,小王和小强从 学校毕业了,他们的项目组仍然能够继续存在。因此,咱们能够认为他们组建的这个项目组就是 闭包。下面我把这个例子写成了代码:
var school = function(){ var si ="小强"; var s2 ="小王"; var team = function(project){ console.log(s1 + s2 + project); } return team; } var team = school(); team(“作电商项且“);//小强、小王作电商项目 team(“作微信项且“);//小强、小王作微信项目
变量si和s2属于school函数的局部变量,并被内部函数team使用,同时该函数也被返回出去。 在外部,经过调用scho ol获得了内部函数的引用,后面屡次调用这个内部函数,仍然可以访问到 si和s2变量。这样si和s2做为自由变量被tea m函数引用,即便创造它们的函数school执行完 了,这些变量依然存在,所以,这就是闭包。
看到这里,咱们须要问本身两个问题:
首先,为何函数内部能够访问外部函数的变量?
这个问题其实很好解释,在变量初始化那节中提到过,当一个函数在建立时其内部会产生一个 scope属性,该属性指向建立该函数的执行上下文中的做用域链对象。这句话提及来比较绕口, 看看下面这张图:
其中做用域链对象包含了该上下文中的VO/AO对象,还有scope对象,好比schoo I上下文中的做 用域链对象就像这样:
school.scopeChain = {
VO:{
si:"小强",
s2:"小王"
},
scope:[[scopeChain]]
}
接下来,我们就来回答这个问题。当内部函数中找不到对应的变量,它就会到scope指向的对象 中找。该对象保存着外部上下文中的做用域链对象,从该做用域链中就能找到对应的变量。这就 是为何函数内部能够访问到外部函数变量的缘由。下面我们咱来看看第二个问题:
为何当外部函数的上下文执行完之后,其中的局部变量仍是能经过闭包访问到呢?
其实用上一个问题的答案再延伸一下,这个问题的答案就出来了。你想一想,外部函数的上下文即 使结束了,但内部的函数只要不销毁(被外部引用了,就不会销毁),它当中的scope就会一直 引用着刚才上下文的做用域链对象,那么包含在做用域链中的变量也就能够一直被访问到。
把这个理解了,闭包的原理也就明白了。
按照这种说法,在JS中每一个函数都有scope对象,而且都会保存外部上下文的做用域链对象。也 就是说,任什么时候候外部上下文销毁了,只要内部函数还在都能访问到外部的变量。那岂不是任何 函数均可以称为闭包了吗?事实上确实是这样的,从广义上讲,JS的函数均可以称为闭包(由于 它们能访问外部变量)。但咱们这里要讲的是狭义上的闭包,这样闭包对于实际应用来说才会有意
义。
狭义的闭包必须知足两个条件:
・造成闭包环境的函数可以被外部变量引用,这样就算它外部上下文销毁,它依然存在。
・在内部函数中要访问外部函数的局部变量。
后面我提到的闭包都是指要知足这两个条件。下面咱们来看看闭包有哪些优缺点,先来看看优 点:
・经过闭包可让外部环境访问到函数内部的局部变量。
・经过闭包可让局部变量持续保存下来,不随着它的上下文环境一块儿销毁。看下面这个例 子:
let count = 0; //全局变量 let compute = function(){ //将计数器加 1 count++; console.log(count); } for( let i = 0 ;i < 100;i++){ compute(); // 循环100 次 } 这个例子是对一个全局变量进行加1的操做,一共加100次,获得值为100的结果。下面用闭包的 方式重构它: var compute = function(){ var count = 0; //局部变量 return function(){ count++; //内部函数访问外部变量 console.log(count); } } var func = compute(); //引用了内部函数,造成闭包 for( var i = 0 ;i < 100;i++){ func(); }
这个例子就再也不使用全局变量,其中count这个局部变量依然能够被保存下来。
下面来看看闭包的缺点: 其实闭包自己并无什么明显的缺点。但每每人们对闭包有种误解:说闭包会将局部变量保存下
来,若是大量使用闭包,而其中的变量又未获得清理,可能会形成内存泄漏。因此要尽可能减小闭 包的使用。
局部变量原本应该在函数退出时被解除引用,但若是局部变量被封闭在闭包造成的环境中,那么 这个局部变量就能一直生存下去。从这个角度来看,闭包的确会使一些数据没法被及时销毁。使 用闭包的一部分缘由是咱们选择主动把一些变量封闭在闭包中,由于可能在之后还须要使用这些 变量。把这些变量放在闭包中和放在全局做用域中,对内存方面的影响是同样的,因此这里并不 能说成是内存泄漏。若是在未来须要回收这些变量,咱们能够手动把这些变量设置为n ull。
若是非要说闭包和内存泄漏有关系的地方,那就是使用闭包的同时比较容易造成循环引用,若是 闭包的做用域中保存着一些DOM节点,这个时候就有可能形成内存泄漏。但这自己并不是闭包的 问题,也并不是JavaScript的问题。在IE浏览器中,因为BOM和DOM中的对象是使用C++以COM 对象的方式实现的,而COM对象的垃圾收集机制采用的是引用计数策略。在基于引用计数策略 的垃圾回收机制中,若是两个对象之间造成了循环引用,那么这两个对象都没法被回收,但循环 引用形成的内存泄漏在本质上也不是闭包形成的。
一样,若是要解决循环引用带来的内存泄漏问题,咱们只须要把循环引用中的变量设为n ull即 可。将变量设置为n ull意味着切断变量与它此前引用的值之间的链接。当垃圾收集器下次运行 时,就会删除这些值并回收它们占用的内存。
接下来咱们看看到底何时会用到闭包。好比咱们常常会使用时间函数对某一个变量进行操 做,看这个例子:
let a = 100; setTimeout(function(){ console.log(++a); },1000);
这个例子用到了时间函数setTimeout,并在等待1秒钟后对变量a进行加1的操做。这是一个闭 包,由于setTimeout中的匿名函数对外部变量进行访问,而后该函数又被setTimeout方法引用。 知足了造成闭包的两个条件。因此你看,即便外部上下文结束了,1秒后仍然能对变量a进行加1 操做。
在DOM的事件操做中,也常常用到闭包,好比下面这个例子:
<input id="count" type="button" value="计数"〉 <script> (function(){ var cnt = 0; var count = document.getElementById("count"); count.onclick = function(){ console.log(++cnt); })() </script>
onclick指向的函数中访问了外部变量ent,同时该函数又被o nclick事件引用了,知足两个条件, 是闭包。因此当外部上下文结束后,你继续点击按钮,在触发的事件处理方法中仍然能访问到变 量 ent。
在有些时候闭包还会引发一些奇怪的问题,好比下面这个例子:
for(var i = 1;i <= 3;i++){ setTimeout(function(){ console.log(i); },1000); }
过1秒后分别输出i变量的值为1,2,3。可是,执行的结果是:4,4,4。实际上,问题就出在闭包身 上。你看,循环中的setTimeout访问了它的外部变量i,造成闭包。而i变量只有1个,因此循环3 次的setTimeout中都访问的是同一个变量。循环到第4次,i变量增长到4,不知足循环条件,循 环结束,代码执行完后上下文结束。可是,那3个setTimeout等1秒钟后才执行,因为闭包的原 因,因此它们仍然能访问到变量i,不过此时i变量值已是4了。
既然是闭包引发的问题,那么解决的方法就是去掉闭包。咱们知道造成闭包有两个条件,只要不 知足其一,那就再也不是闭包。条件之一,内部函数被外部引用,这个咱们没办法去掉。条件二, 内部函数访问外部变量。这个条件咱们有办法去掉,好比:
for(var i = 1;i <= 3;i++){ (function(index){ setTimeout(function(){ console.log(index); },1000); })(i) }
这样setTimeout中就能够不用访问for循环声明的变量i了。而是采用调用函数传参的方式把变量i 的值传给了 setTimeout,这样它们就再也不造成闭包。也就是说setTimeout中访问的已经不是外部 的变量i,因此即便i的值增加到4,跟它内部也不要紧,最后达到了咱们想要的效果。
固然,解决这个问题还有个更简单的方法,就是使用ES6中的let关键字。它声明的变量有块做用 域,若是将它放在循环中,那么每次循环都会有一个新的变量i,这样即便有闭包也没问题,由于 每一个闭包保存的都是不一样的i变量,那么刚才的问题也就迎刃而解。
for(let i = 1;i <= 3;i++){ setTimeout(function(){ console.log(i); },1000); }
12-3-2闭包的更多做用
闭包能够帮助把一些不须要暴露在全局的变量封装成"私有变量"。假设有一个计算乘积的简单函 数:
//计算传入的数字的乘积
let mult = function(){ let a = 1; for( let i=0;i<arguments.length;i++) { a *= arguments[i]; } return a; } console.log(mult(1,2,3)); // 6
mult函数接受一些number类型的参数,并返回这些参数的乘积。如今咱们以为对于那些相同的 参数来讲,每次都进行计算是一种浪费,咱们能够加入缓存机制来提升这个函数的性能,以下:
//缓存对象,用于对计算结果进行缓存
let cache = {}; let mult = function(){ //将传入的数字组成字符串做为缓存对象的键 let args = Array.prototype.join.call(arguments,','); if(cache[args]) { return cache[args]; } let a = 1; for( let i=0;i<arguments.length;i++) { a *= arguments[i]; } return cache[args] = a; } console.log(mult(1,2,3)); // 6 console.log(mult(1,2,3)); // 6
咱们看到,cache这个变量仅仅是在mult函数中被使用,与其让cache变量跟mult函数一块儿平行地 暴露在全局做用域下,不如把它封闭在mult函数内部,这样能够减小页面中的全局变量,以免 这个变量在其余对方被不当心修改而引起错误。代码以下:
let mult = (function(){ //缓存对象,用于结果进行缓存 let cache = {}; return function(){ let args = Array.prototype.join.call(arguments,','); if(cache[args]) { return cache[args]; } let a = 1; for( let i=0;i<arguments.length;i++) { a *= arguments[i]; } return cache[args] = a; } })(); console.log(mult(1,2,3)); // 6 console.log(mult(1,2,3)); // 6
提炼函数是代码重构中的一种常见技巧。若是在一个大函数中有一些代码块可以独立出来,那么 咱们经常把这些代码块封装在独立的小函数里面。独立出来的小函数有助于代码的复用,若是这 些小函数有一个良好的命名,它们自己也起到了注释的做用。若是这些小函数不须要在程序的其 他地方使用,那么最好是把它们用闭包封闭起来。重构后的代码以下:
let mult = (function(){ //缓存对象,用于结果进行缓存 let cache = {}; //计算乘积函数和cache—样,该函数一样被闭包封闭了起来 let calc = function(){ let a = 1; for( let i=0;i<arguments.length;i++) { a *= arguments[i]; } return a; } return function(){ let args = Array.prototype.join.call(arguments,','); if(cache[args]) { return cache[args]; } return cache[args] = calc.apply(null,arguments); } })(); console.log(mult(1,2,3)); // 6 console.log(mult(1,2,3)); // 6
img对象常常用于进行数据上报,以下所示:
let report = function(src){ let img = new Image(); img.src = src; } report('http://xxx.com/getUserInfo');
可是经过查询后台的记录咱们得知,由于一些低版本的浏览器的实现存在bug,在这些浏览器下 使用report函数进行数据上报时会丟失30%左右的数据,也就是说‘report函数并非每一次都 成功发起了 HTTP请求。丟失数据的缘由是img是report函数中的局部变量,当report函数在调用 结束后,img局部变量随即被销毁,而此时或许还没来得及发出HTTP请求,因此这次请求就会丟 失掉。
如今咱们把img变量用闭包封闭起来,便能解决请求丟失的问题,以下:
let report = (function(){ let imgs = []; return function(src){ let img = new Image(); imgs.push(img); img.src = src; } })();
12-3-3闭包和面向对象设计
过程与数据的结合是形容面向对象中的"对象"时常用的表达。对象以属性的形式包含了数 据,以方法的形式包含了过程。而闭包则是在过程当中以环境的形式包含了数据。一般用面向对象 思想能实现的功能,用闭包也可以实现,反之亦然。
在JavaScript语言的祖先Scheme语言中,甚至都没有提供面向对象的原生设计,但却可使用 闭包来实现一个完整的面向对象的系统。下面咱们来看看这段跟闭包相关的代码:
let Test = function(){ let value = 0; return { call : function(){ value++; console.log(value); } } } let test = new Test(); test.call();// 1 test.call(); // 2 test.call(); // 3
若是换成面向对象的写法,那就是以下:
let test = { value : 0, call : function(){ this.value++; console.log(this.value); } } test.call(); // 1 test.call(); // 2 test.call(); // 3
或者
let Test = function(){ this.value = 0; } Test.prototype.call = function(){ this.value++; console.log(this.value); } let test = new Test(); test.call(); // 1 test.call(); // 2 test.call(); // 3
12-4递归函数
递归函数是一个一直直接或者间接调用它本身自己,直到知足某个条件才会退出的函数。当须要 设计到迭代过程时,这是一个颇有用的工具。下面咱们以计算阶乘来进行示例:
let numCalc = function(i){ if(i == 1){ return 1; }else{ return i * numCalc(i-1); } } console.log(numCalc(4));//24
这里,咱们要计算4的阶乘,那么咱们能够看做是4乘以3的阶乘。而3的阶乘又能够看做是3乘以 2的阶乘,以此类推。
下面是几个常见的递归函数的例子,经过下面的例子能够帮助咱们加深对递归函数的理解
1•使用递归计算从m加到n
let numCalc = function (m, n) { if (m === n) { return m; }else { return n + numCalc(m, m > n ? n + 1 : n - 1); } } console.log(numCalc(100, 1));//5050
2•使用递归计算出某一位的斐波那契数
let numCalc = function (i) { if (i == 1) { return 0; }else if (i == 2) { return 1; else { return numCalc(i - 1) + numCalc(i - 2); } } console.log(numCalc(8));//13
3•使用递归打印出多维数组里面的每个数字
let arr = [1, 2, [3, 4, [5, 6], 7, 8], 9, 10]; let test = function (arr) { for (let i = 0; i < arr.length; i++) { if (typeof arr[i] == 'object') { test(arr[i]); }else { console.log(arr[i]); } } }; test(arr);
12-5高阶函数
在本小结中,咱们将向你们介绍J avaScript中函数的高阶用法。
12-5-1高阶函数介绍
高阶函数(higher-order-function)指的是操做函数的函数,通常有如下两种状况:
・函数能够做为参数被传递
・函数能够做为返回值输出
JavaScript中的函数显然知足高阶函数的条件,在实际开发中,不管是将函数看成参数传递,还 是让函数的执行结果返回另一个函数,这两种情形都有不少应用场景。下面将对这两种状况进 行详细介绍
12-5-2参数传递
把函数看成参数传递,表明能够抽离出一部分容易变化的业务逻辑,把这部分业务逻辑放在函数 参数中,这样一来能够分离业务代码中变化与不变的部分。其中一个常见的应用场景就是回调函 数。
1.回调函数
前面不管是在介绍函数基础的时候,仍是在介绍异步编程的时候,咱们都有接触过回调函数。回 调函数,就是典型的将函数做为参数来进行传递。
在Ajax异步请求的应用中,回调函数的使用很是频繁。想在Ajax请求返回以后作一些事情,但又 并不知道请求返回的确切时间时,最多见的方案就是把回调函数看成参数传入发起Ajax请求的方 法中,待请求完成以后执行回调函数。俄们将在14章详细介绍Ajax技术)
示例代码以下:
let getUserInfo = function (userId, callback) { $.ajax('http://xx.com/getUserInfo?' + userId, function (data) { if (typeof callback === 'function') { callback(data); } }); } getUserInfo(123, function (data) { alert(data.userName); });
回调函数的应用不只只在异步请求中,当一个函数不适合执行一些请求时,也能够把这些请求封 装成一个函数,并把它做为参数传递给另一个函数,"委托"给另一个函数来执行。好比,想 在页面中建立100个div节点,而后把这些div节点都设置为隐藏。下面是一种编写代码的方式:
let appendDiv = function () { for (var i = 0; i < 100; i++){ var div = document.createElement('div'); div.innerHTML = i; document.body.appendChild(div); div.style.display = 'none'; } }; appendDiv();
把div.style.display = 'none'的逻辑硬编码在appendDiv里显然是不合理
的,appendDiv未免有点个性化,成为了一个难以复用的函数,并非每一个人建立了节点以后 就但愿它们马上被隐藏,因而把div.style.display = 'none'这行代码抽出来,用回调函数 的形式传入appendDiv()方法
let appendDiv = function (callback) { for ( let i = 0; i < 100; i++){ let div = document.createElement('div'); div.innerHTML = i; document.body.appendChild(div); if (typeof callback === 'function'){ callback(div); } } }; appendDiv(function (node) { node.style.display = 'none'; });
能够看到,隐藏节点的请求其实是由客户发起的,可是客户并不知道节点何时会建立好, 因而把隐藏节点的逻辑放在回调函数中,"委托"给appendDiv()方法。appendDiv()方法固然 知道节点何时建立好,因此在节点建立好的时候,appendDiv()会执行以前客户传入的回 调函数。
2.数组排序
前面在介绍数组排序时有介绍过sor t()方法,该方法就接收一个函数做为参数。sort()方法 封装了数组元素的排序方法。从sor t()方法的使用能够看到,咱们的目的是对数组进行排序, 这是不变的部分;而使用什么规则去排序,则是可变的部分。把可变的部分封装在函数参数里, 动态传入sor t()方法,使sor t()方法方法成为了一个很是灵活的方法。咱们这里来复习一 下:
//从小到大排列,输出:[1, 3, 4 ]
[1, 4, 3].sort(function (a, b) { return a - b; }); //从大到小排列,输出:[4, 3, 1 ] [1, 4, 3].sort(function (a, b) { return b - a; });
除了这个sort()方法之外,还有诸如for Each() , map() , eve ry() , some()等函数,也 是常见的回调函数。这些函数在前面都已经介绍过了,这里再也不花篇幅进行介绍。
12-5-3返回值输出
相比把函数看成参数传递,函数看成返回值输出的应用场景也许更多。让函数继续返回一个可执 行的函数,意味着运算过程是可延续的。
1.判断数据的类型
咱们来看下面的例子,判断一个数据是否为数组。在以往的实现中,能够判断这个数据有没 有length属性,有没有sort方法或者slice方法等。可是更好的方法是使
用 Object,prototype.toString 来计算。Object.prototype.toString.call(obj)返回一^个 字符串,好比 Object.p rototype.toSt ri ng.call([1,2,3])老是返回"[object Array]", 而 Object.prototype.toString.call("str")老是返回"[object String]"。下面是使 用Object,prototype.toString.call()方法来判断数据类型的一系列的isType函数。
let isString = function (obj) { return Object.prototype.toString.call(obj) === '[object String]'; }; let isArray = function (obj) { return Object.prototype.toString.call(obj) === '[object Array]'; }; let isNumber = function (obj) { return Object.prototype.toString.call(obj) === '[object Number]'; };
咱们发现,实际上这些函数的大部分实现都是相同的,不一样的只
是Object.p rototype.toSt ring.call(obj)返回的字符串。为了不多余的代码,咱们尝试把 这些字符串做为参数提早传入isType函数。代码以下:
let isType = function (type) { return function (obj) { return Object.prototype.toString.call(obj) === '[object ' + type + ]'; } }; let isString = isType('String'); let isArray = isType('Array'); let isNumber = isType('Number'); console.log(isArray([1, 2,引));// 输出:true
固然,还能够用循环语句,来批量注册这些isType函数:
let Type = {}; for (let i = 0, type;type = ['String', 'Array', 'Number'][i++];){ ( function (type) { Type['is' + type] = function (obj) { return Object.prototype.toString.call(obj) === '[object ' + type + ]'; } })(type) } console.log(Type.isArray([])); // 输出:true console.log(Type.isString("str")); // 输出:true
2. getSingle
下面的例子是一个单例模式的例子。
|注:单例模式的定义是:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
<body> <script> let getSingle = function(fn){ let ret; return function(){ return ret || (ret = fn.apply(this.arguments)); } } let createDiv = getSingle(function(){ return document.createElement('div'); }); let div1 = createDiv(); let div2 = createDiv(); console.log(div1 === div2); // true </script> </body>
在这个高阶函数的例子中,既把函数看成参数传递,又让函数执行后返回了另外一个函数。
12-5-4面向切面编程
AOP(面向切面编程)的主要做用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务 逻辑无关的功能一般包括日志统计、安全控制、异常处理等。把这些功能抽离出来以后,再通 过"动态织入"的方式掺入业务逻辑模块中。这样作的好处首先是能够保持业务逻辑模块的纯净和 高内聚性,其次是能够很方便地复用日志统计等功能模块
在Java语言中,能够经过反射和动态代理机制来实现AOP技术。而在JavaScript这种动态语言 中,AOP的实现更加简单,这是JavaScript与生俱来的能力。一般,在JavaScript中实现AOP, 都是指把一个函数"动态织入"到另一个函数之中。下面经过扩展Function.p rototype来实 现,示例以下:
Function.prototype.before = function (beforefn) { let _self = this; //保存原函数的引用 return function () { //返回包含了原函数和新函数的"代理"函数 beforefn.apply(this, arguments); // 先执行新函数,修正this return _self.apply(this,arguments); // 再执行原函数 } }; Function.prototype.after = function (afterfn) { let _self = this; return function () { let ret = _self.apply(this, ar guments); //先执行原函数 afte rfn.apply(this, ar guments); //再执行新函数return ret; } }; let func = function () { console.log(2); }; func = func.before(function () { console.log(1); }).after(function () { console.log(3); }); func(); // 1 // 2 // 3
把负责打印数字1和打印数字3的两个函数经过AOP的方式动态植入func()函数。经过执行上面 的代码,控制台顺利地返回了执行结果一、二、3。