今天总结的这个章节内容有点多,有点复杂,如果我有说不清或者说错的地方,欢迎大家交流与指导
在前面我已经给大家介绍过词法作用域的概念了(即在编译阶段就被放在固定位置读取的作用域),所以在说闭包之前我们先来说说函数、变量提前与块作用域的概念
一、块作用域
大家可以将块作用域理解为局部变量所在的词法作用域内,形象的给个demo就是↓
function zoom(){
var me='我是zoom作用域下的局部变量me';
console.log(me); //'我是zoom作用域下的局部变量me'
}
console.log(me); //Reference
这个时候zoom函数就是一个块作用域了;
那么for与if呢?是否也算是块作用域呢?
for(var i=0;i<10;i++){
console.log(i); //1 2 3 .....
}
console.log(i); //10
if(true){
var a=5;
}
console.log(a);//5
我们可以看出在ES5的写法下for与if是不算块作用域的,他们的词法作用域都在全局之下,而我们如何可以让他们也变成块作用域呢,这个时候我们有种解决方法;
利用let声明变量,let会将变量绑定在for循环的块中,并且在每次迭代中重新进行赋值:
for(let i=0;i<10;i++){
console.log(i) //1 2 3 .....
}
console.log(i); //Reference
//if同理
if(true){
let a=5;
console.log(a); //5
}
console.log(a); //Reference
{
var a=5;
console.log(a); //5
}
console.log(a); //Reference
当然let只是一种替代方案,可以让你的变量绑定在一个{}中,但实际上for是不会创建块作用域的。
二、函数与变量的提升
1、变量的提升
console.log(a); //undefined
var a=5;
console.log(a); //5
上述代码中,第一个控制台日志中a的报错是undefined而不是未进行声明所报的Reference,这就证明了变量a的声明是在编译阶段被提升至最前了,否则应会被报错为a未定义;
2、函数的提升
top(); //'我被提升了'
function top(){
console.log('我被提升了');
}
这里与1中同理;
3、那么当函数与变量同时出现时,哪个的提升优先呢?show me my code~
console.log(top);//function top(){...}
var top='我是变量top';
function top(){
console.log('我是top函数')
}
由上面的demo我们已经显然得出了结论,当变量与函数相冲时,编译器会优先提前函数而不是变量,如果优先为变量那么此处log的结果应该为undefined,如果先声明top函数再声明top变量,结果是一样的,大家可以试试,所以得出结论:函数提前优先
三、简单的闭包理解
先来一个简单的闭包来方便大家理解:
function foo(){
var a=2;
function bar(){
console.log(a);
}
return bar;
}
var baz=foo();
baz();//2
1、什么时候会产生闭包?
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。(引用至书中)
2、解释其中的机制
在demo中我们先执行了外部函数foo,我们知道引擎具有垃圾回收器的职能,当一个函数执行后,该函数的内部作用域会被销毁,以此来释放内存,当下次再次调用则又不会进行创建对应作用域;
那么在这里我们先执行了foo()后,在foo中return出来的是function bar(){…}并赋值给了baz,紧接着我们执行了baz()输出2,此时我们是在外部执行的bar函数而不是在foo的内部,但是按照回收器的原理,此时foo函数执行完毕后,不再有需要时,内部作用域应该是会被回收的,而此处就体现了出闭包的神奇,foo内部没有被回收,引擎记住了foo的内部作用域,那么我们知道函数一旦不被需要时就会被回收,那么是谁在需要者foo的内部作用域呢?
在此我们分析一下baz=foo(),此时foo()执行后返回了bar并由baz接住了返回值bar方法,此时bar是存在的,并且在其后会在外部的词法作用域中被调用,但是正是由于bar的存在,bar是存在于foo的内部作用域之中的,那么简单的说就是bar需要foo内部作用域的存在,那么引擎这里就知道了:’哦原来bar你还要用啊,那我就先不回收了,你先用着先’,此时foo的内部作用域是一直存在并被记忆在了内存之中,我们反复使用时就不需要再次创建新的foo内部作用域了,因为它从来都没有被回收!这就提高了内部作用域中的执行效率,而其中的var a=2;的变量a就可以说被foo的内部作用域保护了起来,因为我们从来都是在利用闭包内bar在改变变量值;
四、循环和闭包
恩,下面我会套用书上的例子来说明,什么?你问这里怎么不自己写demo说明了,那么我只能说我太菜了,实在想不到比书上还好的例子了……
for(var i=0;i<5;i++){
setTimeout(function(){
console.log(i);
},i*1000)
}
看完这个书中的demo之后,首先一开始其实我也是认为是每隔1s输出一个i(i由0-4)的,但执行后可以发现结果是每秒都是输出5,下次依次解释引擎执行的过程;
1、首先进入循环后确实是执行了一次性定时器,并且根据i的值递增延迟执行,但此时一次性定时器的内部函数只有一个作用域且其中使用的都是全局变量的i,记住由于一次性定时器内的函数是延迟执行的,这里内部函数还没执行;
2、那么问题来了,在一次性定时器中的函数开始执行时,for循环已经循环完毕了,此时i=5,for完毕后一次性定时器中的函数才开始执行,退出循环时i=5,接下来一共执行5次,此时只有一个全局i,且i=5,所以会log出5个5,而不是依次0-4。
所以这里最根本的原因就是一次性定时器内从始至终都共享了 ‘一个’全局变量i,那么我们如何改进它才能使它实现我们想要的结果一次延迟输出0-4呢?
这就回到了我们上面所讲的简单闭包中的知识点了,利用闭包就可以解决了,在for循环中只要能依次生成循环次数的闭包并且在闭包中将i保护起来,那么在执行内部函数时就是不同的闭包,不同的i了,因为有了’循环次数’个作用域,所以在内部可以实现;
那么接下来我们尝试通过5个闭包来创建5个作用域来解决问题:
for(var i=0;i<5;i++){
(function(){
setTimeout(function(){
console.log(i);
},i*1000);
})();
}
我们在这里已经创建了5个作用域了,但是问题解决了吗?你可以试试~
结果你发现:’妈蛋,还是输出了5个5 哈哈哈哈哈哈哈哈哈哈哈哈哈哈’;
为什么不行呢?
哈哈哈哈哈哈哈,因为你即使创建了5个空作用域,但是还是只右查询了一个全局i,虽然在你的5个作用域内都有了i,但是引擎只用了右查询,并没有左查询在各自的作用域中保存下来,哈哈哈哈哈~
所以改造一下,只要不是空作用域,去保存每次的i就行了:
for(var i=0;i<5;i++){
(function(){
var j=i;
setTimeout(function(){
console.log(j)
},j*1000)
})();
}
恩,这次就行了,5个闭包,5个局部变量j来保存每次进入的全局i,然后延迟执行的函数依次有查询j,嘻嘻~
等等我们再来升级一下再结束这段
for(var i=0;i<5;i++){
(function(j){
setTimeout(function(){
console.log(j)
},j*1000)
})(i);
}
恩,立即执行函数这样写就行了,不需要这么麻烦。
最后告诉你一个悲剧的方法
for(let i=0;i<5;i++){
setTimeout(function(){
console.log(i);
},i*1000)
}
这个方法与上面的立即执行函数出来的结果是一样的,依次输出0-4,为什么可以呢,是因为let在for的{…}中创立了类似于块作用域的概念,并且let会使i每次迭代赋值 等价于 {var j=i;i++},每次在块作用域中i都会迭代赋值,即是每次增加1,这里let已经满足了我们开始需要的2个条件~
1、利用5个闭包分别创建5个不同的作用域;
2、将每次的i赋值给局部变量保护起来;
let已经在上面讲过了,这里就不多说了,别想死,我最后看到这个方法心里也是X了狗。
五、模块化与简单的现代模块机制(定义加载器/管理器与依赖注入的简单实现)
首先我们来写一个最基本的模块来说明这个小章节
var modules=(function(name){
function change(){
public.foo=foo1;
}
function foo(){
console.log('我的名字叫:'+name);
}
function foo1(){
console.log('我的名字大写叫:'+name.toUpperCase());
}
var public={
foo:foo,
change:change
};
return public;
})('MyModules');
modules.foo(); //'我的名字叫:MyModules'
modules.change(); //public.foo=>foo1
modules.foo(); //'我的名字大写叫:MYMODULES'
在上面的demo中我们利用模块化的思想,在构造模块时我们将'MyModules' 作为参数传入并构造了2个API,一个是foo一个是change,代码也很简单,相信大家自己就能看懂吧~
接下来我们创建一个加载器或者叫管理器,用于部分需要依赖注入的模块,仿造书中的简单核心逻辑来说明
//首先建立一个管理器
var MyModules=(function(){
var modules={};
function define(name,deps,func){
for(var i=0;i<deps.length;i++){
deps[i]=modules[deps[i]];
}
modules[name]=func.apply(func,deps);//核心
}
function get(name){
return modules[name];
}
return{
define:define,
get:get
}
})();
//注册foo模块
MyModules.define('foo',[],function(){
function add(a,b){
console.log('和为:'+(a+b));
return a+b;
}
return {
add:add
}
//注册bar模块
MyModules.define('bar',['foo'],function(foo){
function sumIsEven(a,b){
console.log(foo.add(a,b)%2 ===0 ?'和为一个偶数':'和为一个奇数');
}
return {
sumIsEven:sumIsEven
}
});
var foo=MyModules.get('foo');
var bar=MyModules.get('bar');
bar.sumIsEven(5,3); //和为8 和为一个偶数
大家也看到了结果,我独立使用了bar 内的sumIsEven来判断和是否为一个偶数,关键点在于我在bar模块内使用了foo模块内的add方法,如果使用angular的童鞋应该知道这样的依赖注入的方式,因为angular的路由就是这样注入的,当然我并没有说完全与其一致,但是思路的核心逻辑是一样的,现在来让我们分析一下这个过程。
1、建立管理器
在其中我们声明了2个函数,一个为定义模块(define),一个为获取模块(get),最后在返回为对象,使其能封装为高内聚的API,正如我上一个小模块demo一样
重点说明:define内的一段循环与其后的赋值操作,其实一般的小伙伴应该看的明白~但是我还是解释一下吧,首先在循环内我们将定义模块的第二个参数(模块名建立的数组)为条件进行的循环,将数组deps内的模块名分别在modules初始化对象内找到对应的模块函数去重新赋值给deps数组,循环结束后则重新将本次定义的模块名利用apply内置API将数组作为参数传给本次模块的声明函数,其中apply并没有改变this的指向,最后将对应赋值给modules对象,在我们的例子中是以下情况:
第一次循环=>数组长度为0=>循环执行0次=>modules[‘foo’]=func.apply(func,[])=>return add;
modules={
foo:{
add:function(a,b){...}
}
}
第二次循环=>数组长度为1=>循环执行一次=>存在依赖foo=>deps[0]=modules[‘foo’]
(注意此时deps[0]为整个foo的函数式)
=>modules[‘bar’]=func.apply(func,deps) ,这里将整个foo函数作为参数传递入了function(bar){…}内执行并赋值给modules[‘bar’],也就是说在bar的作用域内已经可以调用foo内的方法了。但需要注意的是你不适用get是不会返回bar函数的~,所以注册了使用时一定要记得先获取对应模块。注意定义管理器时的get方法。
modules={
foo:{
add:function(a,b){...}
},
bar:{
sumIsEven:function(a,b){...}
}
}
在bar定义的模块内,foo实际上是以这种形式存在的
foo=(function(){
function add(a,b){
console.log('和为:'+(a+b));
return a+b;
}
return {
add:add
}
})()
apply时已经执行了,所以你可以想象成上述的情况方便理解,当然这里只有一个依赖,如果有2个,便有2个这样的函数执行,而且你可以很明显的看出,没错,foo在bar内是一个闭包= =…因为是在sumIsEven作用域内调用的foo作用域中的add方法,这很词法作用域~
其实我说这么多还不如自己多看几遍来的实在~
这次的内容有点多,特别是闭包与模块这里会比较复杂,其实我也分了两天来写这块,如果其中有错误或者不清楚的地方,欢迎大家指导~
本文深入探讨JavaScript中的闭包概念及其应用场景,包括闭包如何帮助处理循环与定时器问题,以及如何通过闭包实现模块化编程。同时,介绍了简单的现代模块机制,包括定义加载器和依赖注入的基本实现。
375

被折叠的 条评论
为什么被折叠?



