整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

JavaScript: 零基础轻松学闭包,实战

JavaScript: 零基础轻松学闭包,实战

文面向初学者,大神轻喷。

好了,开始吧。

上一节 JavaScript: 零基础轻松学闭包(1)中,我们对闭包的原理进行了讲解,这一节会说很多实战性的东西了,可能会有点难度,你准备好了吗?

1. 如何将私有数据暴露出去

还记得在上一节中,有这样一个例子么?

var test=function(){
 var i=10;
}
function test2(){
 alert(i);
}
test2();

函数 test 和 test2 各自形成一个闭包,两个闭包之间无法访问对方的私有数据。比如,在 test 中定义的变量,在 test2 里面是无法直接访问到的。

那么问题来了, 当然,这边和挖掘机没关系。这里的问题是,有没有什么办法让 test2 可以访问到其他闭包中的私有变量呢?

办法当然是有的,最直接的想法就是,大不了我定义一个全局变量,在 test 中将私有数据赋给全局变量,然后在 test2 里面就能访问到了。

是的,因为两个函数共同享有一个全局作用域,所以这个办法确实可行。我在很多项目里也的确看到很多人就是这么做的。

那么,有没有一种更好的方法呢?要知道,全局作用域是一个比较敏感的地方,一不小心就会出现变量名重复的问题。顺便说一句,在全局作用域中,尽量不要使用诸如 temp , a , b , c 这一类的大众化变量。

于是,这就牵扯到返回值的相关知识了,你在C语言的教材中肯定见惯了类似于这样的代码

int sum(int a,int b)
{
 return a + b;
}
int all=sum(3,5);

这是一个简单的求和函数,很多人慢慢地养成了这样一个观念,就是函数的返回值就是一个字面值,要么是数字类型,要么是布尔类型,或者是字符串。

在很多强类型的语言,诸如 Java,C,C++, 确实如此。但是 return 在 JavaScript 中却大有来头。

在上一节已经说明了,js 的函数也是一种数据类型,你可以把函数看成是和int , float , double 一样的东西。

那么,既然int可以当做函数的参数或者返回值,函数当然也可以!

请看下面两句话:

在js中

  • 如果函数被当做参数传进去了,它就是所谓的回调函数。
  • 如果函数被当做返回值return出去了,它就是把一个闭包return出去了。

这一章不讲回调函数,如果你不清楚啥叫回调函数,可以去看看这个小例子:

(浅谈js回调函数)

还是上面的那个例子,我们希望在 test2 中可以访问到 test 里面的变量,可以这样做:

var test=function(){
 var i=10;
 /* 定义一个函数将变量i暴露出去 */
 var get=function(){
 return i ;
 }
 return get; //将获得i的函数暴露出去
}
function test2(){
 
 var fn=test();//接收test暴露出来的函数
 alert(fn()); //获得test中的私有数据
}
test2();

test 函数中的 get 方法是一个内部函数,它自己也形成了一个闭包, test 是他的父级作用域,因此它可以获取i的值。

i 进入 get 方法的闭包,被包了起来,然后最终被返回了出去。

而对于 test2 来说,是可以访问到 test函数的,因此可以调用并执行 test 函数,从而获取其返回值。

你可能会说,我直接在test中把i给return出去就好了嘛,干嘛这么麻烦。

是的,言之有道理。

可是,如果我要访问 test 中多个私有数据咋办捏?

这下你可明白了吧!

现在,我们给出关于闭包的第二个注解:

(第一个注解在上一节)

从应用的角度来看,闭包可以将函数或者对象的私有数据暴露出去,而不影响全局作用域。

通过这张图,是不是好理解一些了呢?我们这一节单说函数里的私有数据。

2. 将私有数据包装成json对象

刚才的例子说明,在js中,return出去的可以是基本数据类型,也可以是函数类型。

其实,JavaScript是一种基于对象的语言,也有对象的概念,所以,我们可以把你需要的东西包裹成一个对象返回出去!

上代码:

var test=function(){
 var apple='苹果';
 var pear='梨子';
 /* 定义一个函数将水果暴露出去 */
 var getFruit={
 apple : apple ,
 pear : pear
 }
 
 return getFruit; //将获得i的函数暴露出去
}
function test2(){
 
 var getFruit=test();//接收test暴露出来的函数
 
 console.log(getFruit);
}
test2();

像这样用 { } 括起来的东西就是一个js对象,也就是所谓json。你可能经常会听到json这个词,觉得还挺高大上的。其实它就是一个用 { } 包起来的数据而已。

里面是键值对的形式,非常类似于Java里面的HashMap。

在这个例子中,我们可以直接把需要暴露的私有数据用一个 { } 包起来,构成一个json对象return出去就可以啦。

因为是 js 对象,alert 不能看到里面的具体内容,所以我们使用 console.log() ,结果如下:

展开后:

Paste_Image.png

这样是不是也可以了?多出来的 proto 是原型链,以后会讲到。

3. 我们来做一个紫金葫芦

大家都还记得西游记里孙悟空用遮天的把戏骗来的紫金葫芦吗,只要你拿着这个葫芦,叫一声别人的名字,如果答应了,别人就会被吸进去。

OK,这个紫金葫芦里面不正如一个闭包吗?

对不对嘛,所以,我们用闭包的知识来做一个好玩的东西吧。

<body>
 <div id='box' style='width:50px;height:50px;background:#333;color:#fff;text-align:center;line-height:50px'>小妖</div>
</body>

紫金葫芦里面的源码大概是这样的:

var 紫金葫芦=function(id){
 var domElement=document.getElementById(id);
 
 var returnObject={
 domElement : domElement ,
 backgroundColor : function(color){
 domElement.style.backgroundColor=color;
 },
 click : function(fn){
 domElement.onclick=fn;
 }
 };
 
 return returnObject;
}

注:我纯粹是为了看起来方便而采用中文定义变量,在实际开发中,千万不要使用中文变量。

我们在返回出去的对象上加了三个东西:

1.domElement

你传进来一个id,我就用 document.getElementById 来包一下,得到一个dom元素,最终要操作的也就是这个dom元素。也就是说:

var box1=紫金葫芦('box').domElement;
var box2=document.getElementById('box');
alert(box1===box2);

他们是一个东西,一样的。

紫金葫芦('box');

这行代码一旦执行,紫金葫芦就会返回 returnObject 对象,也就是说。我们喊一声 “box”,那个id为box的小妖一答应,就被装进来了,然后我们可以对它为所欲为!

比如,给它换一个背景色:

2.backgroundColor 给元素添加背景色的方法

var box=紫金葫芦('box');
box.backgroundColor('red');

3.click 给元素添加点击事件,需要传入一个回调函数

var box=紫金葫芦('box');
box.click(function(){
 alert('就没人吐槽这个无聊的作者么,小妖也有尊严的好么,啊喂!!');
});

结果:

也许你已经发现了,这些方法是不是和jQuery有点类似呢?

闭包有关的两个概念:

(1)变量的作用域 不带有关键字var的变量会成为全局变量; 在函数中使用关键字var声明的变量是局部变量。 局部变量只有在函数内部才能访问到,在函数外面是访问不到的。但在函数内部可以通过作用域链一直向上搜索直到全局对象,也就是说,函数内部可以访问函数外部的变量。

(2)变量的生存周期 对于全局变量,其生存周期是永久的,除非主动销毁这个全局变量; 而对于在函数内用关键字var声明的局部变量,当退出函数时,这些局部变量会随着函数调用结束而被销毁。

例外情况:闭包

从闭包的一个经典应用谈起

问题:无论单击哪个div,都会弹出5。 原因:onclick事件是异步触发的,当事件被触发时,for循环早已结束,此时变量i的值早已经是5。 解决:在闭包的帮助下,把每次循环的i值都封闭起来。当事件函数顺着作用域链从内到外查找变量i时,会先找到被封闭在闭包环境的i,单击div时,会分别输出0,1,2,3,4。

类似实例:闭包直接赋给数组

结果:result的每个元素都返回10。 说明:闭包的作用域链有明显的副作用——闭包总是获得外部函数变量的最终值。上面代码中,外部函数产生一个函数数组result并返回。函数数组中的每个元素都是一个函数,每个函数都返回 i变量。似乎每个函数应该返回每次循环的i值,即依次返回0到9,但事实是,每个函数的返回结果都是10。这是因为每个内部函数返回的是变量i,而不是i在某个时刻的特定值,而i的作用域是整个外部函数,当外部函数执行完成后,i的值是10。 解决:在每个内部函数的内部,再产生一个匿名函数并返回。

结果:result依次返回0到9。 说明:(i)使得该层匿名函数立即执行。 ###3、闭包

有时候需要得到函数内的局部变量。如何从外部读取局部变量?那就是在函数的内部,再定义一个函数。 闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量,利用闭包可以突破作用链域,将函数内部的变量和方法传递到外部。 ① 闭包的原理

  • 后台执行环境中,闭包的作用域链包含着自己的作用域、函数的作用域和全局作用域。
  • 通常,函数的作用域和变量会在函数执行结束后销毁。
  • 但是,当函数返回一个闭包时,这个函数的作用域将会一直在内存中保存到闭包不存在为止。 ② 闭包的特性
  • 函数内再嵌套函数。
  • 内部函数可以引用外层的参数和变量。
  • 参数和变量不会被垃圾回收机制回收。 ③ 闭包的用途
  • 读取函数内部的变量。

在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,就可以在f1外部读取它的内部变量了。

闭包就是能够读取其他函数内部变量的函数。由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制回收。 (2)让函数内部的变量的值始终保持在内存中(延长局部变量的寿命)

由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。也就是说,闭包会引用外部函数作用域,会占用更多的内存,过度使用闭包,会导致性能问题。所以,仅当必要时才使用闭包。对产生闭包的函数,使用后应该解除引用。 (3)自执行函数+闭包减少全局变量污染(封装私有变量)

var person=(function() {
 var_name="Alice";
 var _id=16;
 return {
 getUserInfo: function() {
 return _name + ": " + _id;
 }
 }
})();//帮助突破技术瓶颈,提升思维能力

使用下划线来约定私有变量_name和_age,它们被封装在闭包产生的作用域中,外部是访问不到这两个变量的,这就避免了对全局的命令污染。 ④ 闭包的缺点: (1) 需要维护额外的作用域。 (2) 过渡使用闭包会占用大量内存。 4、this对象 在闭包内使用this对象将产生一些复杂的行为。this对象的值基于函数所在的执行环境在运行时决定:在全局函数中使用时,this等于window(非严格模式)或undefined(严格模式);而当作为对象的方法调用时,this等于这个对象。

每个函数一旦被调用,它将自动获得this和arguments两个变量。一个内部函数是不能直接从外部函数访问到这两个变量的。可以通过将this对象存储在另一个变量中来解决这个问题。把外部作用域中的this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了。

var name="The window";
var object={
 name: "My object",
 getName: function() {
 var that=this;
 return function() {
 return that.name;
 };
 }
};
alert(object.getName()());//输出:"My object"

要让闭包访问外部函数的this和arguments对象,可以通过将它们的引用存储在另一个变量中来完成。 5、内存泄漏 使用闭包的时候很容易造成循环引用,若闭包的作用域包含着一些DOM节点,这时候就有可能造成内存泄漏,但其实,这本身不是闭包的问题,而是由于:BOM和DOM中的对象是使用C++以COM对象的方式实现的,而COM对象的垃圾收集机制采用的是引用计数策略,在基于引用计数策略的垃圾回收机制中,若两个对象之间形成了循环引用,则这两个对象都无法被回收。

匿名函数保存了一个对element的引用,只要匿名函数存在,element的引用数至少为1,它所占用的内存就永远不会被回收。

通过把element.id的一个副本保存在变量中,并且在闭包中引用该变量消除了循环引用,但是仅仅做到这一步还不能解决内存泄漏的问题,闭包会引用包含函数的所有活动对象,包含element,即使闭包不直接引用element,包含函数的活动对象中也仍然会保存一个引用,因此有必要把element元素设置为null,这样就能解除对DOM对象的引用,确保正常回收其占用的内存。 6、模仿块级作用域 JavaScript中没有直接的块级作用域。

使用闭包可以模仿块级作用域——创建并立即调用一个函数,这样既可以执行其中的代码,又不会在内存中留下对该函数的引用。结果是内部函数的所有变量都会立即被销毁,除非将某些变量赋值给了包含作用域(即外部作用域)中的变量。 用作块级作用域的匿名函数:将函数声明包含在一对圆括号中,表示它实际上是一个函数表达式,紧随其后的另一对圆括号会立即调用这个函数。

(function() {
 //这里是块级作用域;
}) ();

可以使用匿名函数表达式来模拟块级作用域,任何定义在匿名函数中的变量在匿名函数执行完之后都将被销毁,在匿名函数外访问这些变量将会产生错误。

喜欢的小伙伴,转发+点个关注吧!

包是什么

闭包是指那些能够访问自由变量的函数。

《JavaScript高级程序设计第三版》:闭包是指有权访问另一个函数作用域中的变量的函数,创建闭包的常见方式,就是在一个函数内部创建另一个函数。

《你不知道的JavaScript(上卷)》:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

function foo() {
 var a=2;
 function bar() {
 console.log( a ); // 2
 }
 bar();
}
foo();

按照第一种定义,这个就是闭包了,因为在一个函数foo内部创建另一个函数bar()。其实,我们仔细看下定义就会发现:在一个函数内部创建另一个函数是创建闭包的常见方式,并不是闭包的定义。确切的说,上述代码中bar() 对a 的引用的方法是词法作用域的查找规则,而这些规则只是闭包的一部分。

var a=2;
(function IIFE() {
 console.log( a );//2
})();

这个是闭包吗?按照前面的定义,并不是,因为IIFE这个函数并不是在它本身的词法作用域以外执行的,a 是通过普通的词法作用域查找而非闭包被发现的。

function foo() {
 var a=2;
 function bar() {
 console.log( a );
 }
 return bar;
}
var baz=foo();
baz(); // 2 

在上面例子中,bar()在自己定义的词法作用域以外的地方被执行,这就是闭包。

一般情况下,由于有垃圾回收机制,在foo() 执行后,foo() 的整个内部作用域都被销毁。而闭包的“神奇”之处在于可以阻止这件事情的发生。事实上,bar()在使用foo() 的内部作用域,所以这个内部作用域依然存在,拜bar() 所声明的位置所赐,它拥有涵盖foo() 内部作用域的闭包,使得该作用域能够一直存活,使得bar() 在之后任何时间进行引用。bar() 对foo()的作用域的引用,就叫作闭包。

function foo() {
 var a=2;
 function baz() {
 console.log( a ); // 2
 }
 bar( baz );
}
function bar(fn) {
 fn(); 
}
var fn;
function foo() {
 var a=2;
 function baz() {
 console.log( a );
 }
 fn=baz; // 将baz 分配给全局变量
}
function bar() {
 fn(); // 这就是闭包!
}
foo();
bar(); // 2

上述两段代码的区别在于,函数值的传递方式不同,但其运行结果一样,而且都产生了闭包。因此,无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。

我们再来分析闭包中经典的for循环问题

for(var i=0;i<5;i++){
 setTimeout(function timer(){
 console.log( i );
 },i*1000)
}

如果你认为这段代码的运行结果为分五次输出0,1,2,3,4,每次间隔为1秒,那就错了。正确的结果是,五次输出都为5,那么,这个5 是从哪里来的呢?我们发现这个循环的终止条件是i >=5。条件首次成立时i 的值是5。因此,输出显示的是循环结束时i 的最终值。

作用域链

function compare(value1, value2){
 if (value1 < value2){
 return -1;
 } else if (value1 > value2){
 return 1;
 } else {
 return 0;
 }
}
var result=compare(5, 10);

以上代码先定义了compare()函数,然后又在全局作用域中调用了它。当调用compare()时,会创建一个包含arguments、value1 和value2 的活动对象。全局执行环境的变量对象(包含result和compare)在compare()执行环境的作用域链中则处于第二位。下图展示了包含上述关系的compare()函数执行时的作用域链。

作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。

无论什么时候在函数中访问一个变量时,就会从作用域链中搜索具有相应名字的变量。一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。但是,闭包的情况又有所不同。

function createComparisonFunction(propertyName) {
 return function(object1, object2){
 var value1=object1[propertyName];
 var value2=object2[propertyName];
 if (value1 < value2){
 return -1;
 } else if (value1 > value2){
 return 1;
 } else {
 return 0;
 }
 };
}

在另一个函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到它的作用域链中。因此,在createComparisonFunction()函数内部定义的匿名函数的作用域链中,实际上将会包含外部函数createComparisonFunction()的活动对象。这段代码的作用域链如下所示

在匿名函数从createComparisonFunction()中被返回后,它的作用域链被初始化为包含createComparisonFunction()函数的活动对象和全局变量对象。这样,匿名函数就可以访问在createComparisonFunction()中定义的所有变量。更为重要的是,createComparisonFunction()函数在执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。换句话说,当createComparisonFunction()函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中;直到匿名函数被销毁后,createComparisonFunction()的活动对象才会被销毁

//创建函数
var compareNames=createComparisonFunction("name");
//调用函数
var result=compareNames({ name: "Nicholas" }, { name: "Greg" });
//解除对匿名函数的引用(以便释放内存)
compareNames=null;

首先,创建的比较函数被保存在变量compareNames 中。而通过将compareNames 设置为等于null解除该函数的引用,就等于通知垃圾回收例程将其清除。随着匿名函数的作用域链被销毁,其他作用域(除了全局作用域)也都可以安全地销毁了。

内存管理

在闭包中调用局部变量,会导致这个局部变量无法及时被销毁,相当于全局变量一样会一直占用着内存。如果需要回收这些变量占用的内存,可以手动将变量设置为null。

然而在使用闭包的过程中,比较容易形成 JavaScript 对象和 DOM 对象的循环引用,就有可能造成内存泄露。这是因为浏览器的垃圾回收机制中,如果两个对象之间形成了循环引用,那么它们都无法被回收。

function func() {
 var test=document.getElementById('test');
 test.onclick=function () {
 console.log('hello world');
 }
}

在上面例子中,func 函数中用匿名函数创建了一个闭包。变量 test 是 JavaScript 对象,引用了 id 为 test 的 DOM 对象,DOM 对象的 onclick 属性又引用了闭包,而闭包又可以调用 test(test.onclick函数中的this就是对象test) ,因而形成了循环引用,导致两个对象都无法被回收。要解决这个问题,只需要把循环引用中的变量设为 null 即可。

function func() {
 var test=document.getElementById('test');
 test.onclick=function () {
 console.log('hello world');
 }
 test=null;
}

如果在 func 函数中不使用匿名函数创建闭包,而是通过引用一个外部函数,也不会出现循环引用的问题。

function func() {
 var test=document.getElementById('test');
 test.onclick=funcTest;
}
function funcTest(){
 console.log('hello world');
}

应用场景

函数防抖和节流

防抖和节流的作用都是防止函数多次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于wait,防抖的情况下只会调用一次,而节流的 情况会每隔一定时间(参数wait)调用函数。

设计单例模式

  • 组件化思想,使用简单,扩展简单
  • 避免全局污染
class CreateUser {
 constructor(name) {
 this.name=name;
 this.getName();
 }
 getName() {
 return this.name;
 }
}
// 代理实现单例模式
var ProxyMode=(function() {
 var instance=null;
 return function(name) {
 if(!instance) {
 instance=new CreateUser(name);
 }
 return instance;
 }
})();
// 测试单体模式的实例
var a=ProxyMode("aaa");
var b=ProxyMode("bbb");
// 因为单体模式是只实例化一次,所以下面的实例是相等的
console.log(a===b); //true

设置私有变量

//赋值到闭包里
const Squery=(function () {
 const _width=Symbol();
 class Squery {
 constructor(s) {
 this[_width]=s
 }
 foo() {
 console.log(this[_width])
 }
 }
 return Squery
})();
const ss=new Squery(20);
ss.foo();
console.log(ss[_width])

为节点循环绑定click事件

<p id="info">123</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
<script>
 function showContent(content){
 document.getElementById('info').innerHTML=content;
 };
 function setContent(){
 var infoArr=[
 {'id':'email','content':'your email address'},
 {'id':'name','content':'your name'},
 {'id':'age','content':'your age'}
 ];
 for (var i=0; i < infoArr.length; i++) {
 var item=infoArr[i];
 //循环中创建了三个闭包,他们使用了相同的词法环境item,item.content是变化的变量
 //当onfocus执行时,item.content才确定,此时循环已经结束,三个闭包共享的item已经指向数组最后一项。
 document.getElementById(item.id).onfocus=function(){
 showContent(item.content)
 }
 }
 }
 setContent()
 /**
 * 解决方法1
 * 通过函数工厂,则函数为每一个回调都创建一个新的词法环境
 */
 function showContent(content){
 document.getElementById('info').innerHTML=content;
 };
 function callBack(content){
 return function(){
 showContent(content)
 }
 };
 function setContent(){
 var infoArr=[
 {'id':'email','content':'your email address'},
 {'id':'name','content':'your name'},
 {'id':'age','content':'your age'}
 ];
 for (var i=0; i < infoArr.length; i++) {
 var item=infoArr[i];
 document.getElementById(item.id).onfocus=callBack(item.content)
 }
 }
 setContent()
 /**
 * 解决方法2
 * 绑定事件放在立即执行函数中
 */
 function showContent(content){
 document.getElementById('info').innerHTML=content;
 };
 function setContent(){
 var infoArr=[
 {'id':'email','content':'your email address'},
 {'id':'name','content':'your name'},
 {'id':'age','content':'your age'}
 ];
 for (var i=0; i < infoArr.length; i++) {
 (function(){
 var item=infoArr[i];
 document.getElementById(item.id).onfocus=function(){
 showContent(item.content)
 }
 })()//放立即执行函数,立即绑定,用每次的值绑定到事件上,而不是循环结束的值
 }
 }
 setContent()
 /**
 * 解决方案3
 * 用ES6声明,避免声明提前,作用域只在当前块内
 */
 function showContent(content){
 document.getElementById('info').innerHTML=content;
 };
 function setContent(){
 var infoArr=[
 {'id':'email','content':'your email address'},
 {'id':'name','content':'your name'},
 {'id':'age','content':'your age'}
 ];
 for (var i=0; i < infoArr.length; i++) {
 let item=infoArr[i]; //限制作用域只在当前块内
 document.getElementById(item.id).onfocus=function(){
 showContent(item.content)
 }
 }
 }
 setContent()

参考文献:

  • 你不知道的JavaScript之闭包