目的

  果然,知识还是总结为好,以前看过的闭包(Closure)过一段时间再去看还是有点概念不清,趁着最近在整理知识,就来总结一下闭包的原理,顺带解决一下之前我所不懂的问题。

什么是闭包

感觉闭包这个概念只可意会,不可言传,但总结来说,闭包有三个特性:

  • 函数嵌套函数
  • 函数内部可以应用外部的参数和变量
  • 参数和变量不会被垃圾回收机制回收

在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
–阮一峰(学习Javascript闭包(Closure)

理解闭包之前,我们需要理解JavaScript特殊的变量作用域。
变量的作用域无非就是两种:全局变量局部变量
Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量,在函数外部自然无法读取函数内的局部变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var t1=22;
var fun1 = function(){
var t2 = 33;
var fun2 = function (){
//
}

console.log(t2); //33
console.log(t1); //22
console.log(fun2); //function
console.log(fun1); //function
};

console.log(t1); //22
console.log(fun1); //function
fun1();
console.log(t2); //error: not defined
console.log(fun2); //error: not defined

这里有一个地方需要注意,函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明了一个全局变量!

1
2
3
4
5
6
function fun2(){
  n=44;
  alert(n);
}
fun2();
alert(n); // 44

对于函数的作用域,还要理解一下作用域链

当某个函数第一次被调用时,会创建一个执行环境以及相应的作用域链,并把作用域链赋值给一个特殊的内部属性(即[[Scope]]).然后,使用this、arguments和其他命名参数的值来初始化函数的活动对象。
但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,······直至作为作用域链终点的全局执行环境。
-《JavaScript高级程序设计》

上面所说的作用域链很重要,闭包的特性就是依赖于作用域来实现的。
通过作用域链,可以决定变量的访问方向。
作用域链就是函数在定义的时候创建的,用于寻找使用到的变量的值的一个索引。
变量的寻找是向上爬寻的。上面的例子可以理解为:

1
2
3
4
window(全局作用域,包含t1,fun1)
^
|
fun1(fun1函数内部作用域,包含t2,fun2)

t1,fun1是全局变量,在全局中声明;b,fun2是局部变量,在局部声明。
无论在fun1内部调用变量,还是在外部调用变量,调用时会先在内部作用域中寻找变量,如果找不到,再向上一层活动对象中寻找,直到找到或者到了全局执行环境下还没有找到的话,则解释器返回undefined.

所以说,怎样才能在外部访问到函数内部的变量呢,闭包可以做到。

回到开始我所讲到的闭包的三个特性,可以思考一下下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
function f1(){
    var n=999;
    nAdd=function(){n+=1}
    function f2(){
      alert(n);
    }
    return f2;
  }
  var result=f1();
  result(); // 999
  nAdd();
  result(); // 1000

在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。

f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制GC(garbage collection)回收。

这段代码中另一个值得注意的地方,就是”nAdd=function(){n+=1}”这一行,首先在nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。
–阮一峰(学习Javascript闭包(Closure)

还有一个典型的作用域问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
function f1() {
var res = new Array();
for(var i=0;i<10;i++){
res[i] = function() {
alert(i);
};
}
return res;
}
var f2 = f1();
f2[0]();//alert 10
f2[1]();//alert 10
//并不会返回一次弹出0-9的函数数组,而是弹出10个10的函数数组,因为res中每个函数的作用域中都保存着f1()的活动对象,引用的是同一个变量i,当f1()返回后i的值为10

解决办法为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function f1() {
var res = new Array();
for(var i=0;i<10;i++){
res[i] = (function(num) {
return function (){
alert(num);
}
})(i);//函数参数按值传递
}
return res;
}
var f2 = f1();
f2[0]();//alert 0
f2[1]();//alert 1

内存泄露

上面我们谈到了“垃圾回收机制”,其实涉及到内存泄露的问题,开始我也不懂什么是内存泄露,现在就来给大家科普一下,方便理解闭包。

概念

首先我们明确下内存泄露的概念:内存里不能被回收也不能被利用的空间即为内存泄露。为什么不能被回收呢?不符合内存回收的算法;为什么不能被利用呢?在栈上没有指向他的指针。
-(再议 js闭包和ie内存泄露原理)

垃圾回收方式有两种:

  1. 标记清除
  2. 引用计数

标记清除:垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记,然后,它会去掉环境中的变量的标记和被环境中的变量引用的变量的标记,此后,如果变量再被标记则表示此变量准备被删除。 2008年为止,IE,Firefox,opera,chrome,Safari的javascript都用使用了该方式;
引用计数:跟踪记录每个值被引用的次数,当声明一个变量并将一个引用类型的值赋给该变量时,这个值的引用次数就是1,如果这个值再被赋值给另一个变量,则引用次数加1。相反,如果一个变量脱离了该值的引用,则该值引用次数减1,当次数为0时,就会等待垃圾收集器的回收。
-js闭包,垃圾回收,内存泄漏

垃圾回收机制现在很成熟了,但早期的IE版本里(ie4-ie6),对宿主对象(也就是document对象)采用是计数的垃圾回收机制,闭包导致内存泄露的一个原因就是这个算法的一个缺陷。循环引用会导致没法回收,这个循环引用只限定于有宿主对象(BOM和DOM)参与的循环引用,而js对象之间即时形成循环引用,也不会产生内存泄露,因为对js对象的回收算法不是计数的方式。

低版本IE中采用的就是引用计数的方式,所以会经常出现内存泄露问题。
引用计数,可以用两个例子说明。

1
2
3
4
function fa() {
var o = new Object();
}
fa();

1
2
栈             堆
var o------>new object

栈上只是存了一个指针,指针就是堆上对象的的地址;这个是时候我们的程序通过这个指针句可以操作堆上的对象。栈上的这个指针是自动管理的,但函数退出后,就销毁了;主要程序就在没办法访问到堆上的这个对象了,而堆上的这个对象这个时候就会被我们的GC自动回收了;如果回收不了,就是内存泄露了。

另一个例子就是上面的闭包例子。

1
2
3
4
5
6
7
8
9
10
11
12
function f1(){
    var n=999;
    nAdd=function(){n+=1}
    function f2(){
      alert(n);
    }
    return f2;
  }
  var result=f1();
  result(); // 999
  nAdd();
  result(); // 1000

1
2
3
栈             堆
var n--------->o1
var result---->o2

当执行

1
var n=999;

的时候,o1的计数为1。
在执行f2的时候,o2的计数为1,为了保持函数对n这个变量的引用,在这个f2的作用域链上加了一个对o1的引用,
这样o1的计数值加1变为2,在f1函数执行完后,o1计数减1变成1,所以o1不会被内存回收。
那么o1什么时候会被回收呢?o2被回收的时候;o2什么时候被回收呢?当指向o2的全局变量var result从栈上消失的时候。

循环引用

我觉得要理解为什么会造成循环引用还是比较难的,而且现实中很容易就会造成循环引用问题。
浅析闭包和内存泄露的问题
循环引用简单来说就是对象A引用了对象B,而对象B又引用了对象A

1
2
A ---------> B ------------> C
^、_ _ _ _ _ _ _|

当A释放时,仍然有来自C的指针指向B,这样B就不能被释放掉,需要JavaScript的特殊处理。

我主要想提一下DOM与JavaScript的循环,通过查阅资料,可以知道,IE中有一部分对象并不是原生额javascript对象,例如,BOM和DOM中的对象就是以COM对象的形式实现的,而COM对象的垃圾回收机制采用的就是引用计数。因此,虽然IE的javascript引擎采用的是标记清除策略,但是访问COM对象依然是基于引用计数的,因此只要在IE中设计COM对象就会存在循环引用的问题!。

当一个循环中同时包含DOM元素和常规JavaScript对象时,IE无法释放任何一个对象——因为这两类对象是由不同的内存管理程序负责管理的。
除非关闭浏览器,否则这种循环在IE中永远得不到释放。为此,随着时间的推移,这可能会导致大量内存被无效地占用。

导致这种循环的一个常见原因是简单的事件处理(这里引用上面博客的例子):

1
2
3
4
5
6
7
$(document).ready(function() {
var button = document.getElementById('button-1');
button.onclick = function() {
console.log('hello');
return false;
};
});

当指定单击事件处理程序时,就创建了一个在其封闭的函数中包含button变量的闭包。而且,现在的button也包含一个指向闭包(onclick属性自身)的引用。这样,就导致了在IE中即使离开当前页面也不会释放这个循环。

如要释放内存,需要断开循环引用。方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1.关闭浏览器
2.删除onclick属性
$(document).ready(function() {
var button = document.getElementById('button-1');
button.onclick = function() {
console.log('hello');
button.onclick = null;
return false;
};
});
3.断开循环引用
function hello() {
console.log('hello');
return false;
}
$(document).ready(function() {
var button = document.getElementById('button-1');
button.onclick = hello;
});
因为hello()函数不再包含 button,引用就成了单向的(从button到hello),不存在循环,所以就不会造成内存泄漏了。
第三种方法也是谷歌js编码规范里的。

其实上面的循环引用也可以用计数方式来解释:

1
2
3
4
5
执行 var button的时候O1为1
执行button.onclick的时候匿名函数O2为1,O1加1。
当结束函数时O1的计数为1,o2的计数为1,都不能被内存回收。
(有解释说:其实fa里面的宿主对象只是真正对象一个副本,当执行e.event这句指令的时候做了两件事,
一个是副本的对象指向O2,这时O2的计数加1,真正的宿主对象又指向这个O2,这个O2的计数再加1 变为了2。[这里我还没弄懂个中意思])

总结

  感觉自己说了一堆关于闭包所涉及的知识,如果你对我所说的概念都有一个比较清晰的理解的话,相信你对闭包也有了深刻的认识。现在就来总结一下闭包的优缺点吧。

优点:

  • 当需要一个变量常驻内存时,闭包可以实现一个变量常驻内存 (如果多了就占用内存了)
  • 避免全局变量的污染
  • 私有化变量,保护函数内的变量安全,加强了封装性

缺点:

  • 因为闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存
  • 引起内存泄露

当然,闭包的优缺点可能不止我所说的,当你真正去用的时候可能你才会真正去了解它。

思考题

想了想,还是附上阮一峰里面的思考题吧,对理解闭包会有帮助:
代码片段一:

1
2
3
4
5
6
7
8
9
10
var name = "The Window";
  var object = {
    name : "My Object",
    getNameFunc : function(){
      return function(){
        return this.name;
      };
    }
  };
  alert(object.getNameFunc()());

代码片段二:

1
2
3
4
5
6
7
8
9
10
11
var name = "The Window";
  var object = {
    name : "My Object",
    getNameFunc : function(){
      var that = this;
      return function(){
        return that.name;
      };
    }
  };
  alert(object.getNameFunc()());

解释

对于代码片段一
object.getnameFunc() 返回的匿名闭包函数被全局变量所引用,其中的this指向全局变量,当执行时打印The Window 。
对于代码片段二
object.getnameFunc() 在返回闭包函数前,将this赋给that,此时getnameFunc是由object调用的,故而this指向object,当内部函数被返回时,由于闭包的特性,仍然能访问到外部函数中的值,当执行打印My Object 。

背景

  积累久了的知识还是要拿出来顺一顺,碰巧这几天研究瀑布流,就顺带理清了瀑布流的实现思路,也用ajax实现了下拉从数据库加载数据的效果,并且对实现过程中遇到的问题作出了解决。代码也已经放上了github,有需要者自行下载。

  瀑布流相信大家都不会陌生,据说最早是Pinterest网站使用的网页布局,后来流行于各大网站。
  瀑布流的实现有几种方式,详情可见:淘宝网UED官方博客
  而我所用的方法是使用JavaScript来实现,其他方法在这里就不多描述了。

静态瀑布流实现过程

下载地址

准备工作

  1. html的基本页面布局为

    1
    2
    3
    <div id="container" class="container">
    <div class="imgShow"><img src=""/></div>
    </div>
  2. css的设置为

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    .container {
    position: absolute;
    top: 20px;
    width:100%;
    }
    .imgShow {
    position: absolute;
    border: solid 1px #ccc;
    padding: 10px;
    width: 200px;
    top: 0px;
    left: 0px;
    }
    img { width: 100%; }

核心过程

实现瀑布流的过程中使用了函数的prototype属性,当创建一个函数实例的时候,实例可以共享原型对象包含的属性和方法。

我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,
而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
使用原型对象的好处是可以让所有的对象实例共享它包含的属性和方法。
–《JavaScript高级程序设计》

  1. 首先定义需要的属性。
    可以把瀑布流拆成三个部分来看:容器、列、格子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
     function WaterFall(containerId){
    this.container = document.getElementById(containerId);
    this.conWidth = 0;//container容器宽度
    this.boxWidth = 0;//每一个格子的宽度
    this.columnNum = 1; //分为多少列
    this.columnHeight = [];//存储每列的高度
    this.boxTagName = "div";
    this.boxList = []; //所有的格子的对象
    this.init();
    }
  2. 初始化数据,并且计算出每行可以放下的格子的数量n = 屏幕可见区域宽度/(格子宽度+间距)。

    1
    2
    3
    4
    5
    6
    7
    8
    WaterFall.prototype.init = function(){
    this.boxList = this.container.getElementsByTagName(this.boxTagName);
    this.boxWidth = this.boxList[0].offsetWidth + 10;
    this.conWidth = this.container.offsetWidth;
    // 计算每行可以放下的格子数量
    var n = parseInt(this.conWidth/this.boxWidth);
    this.columnNum = (n > 0) ? n : 1;
    }
  3. 进行排序:在上一步中得到每行可以放下的格子的数量n后,把前n个格子放在第一行每一列中;然后每次寻找高度最小的一列,依次把格子放进去,并通过设置left(容器离左边的宽度+间距+格子的宽度*最低列索引)和top(间距+最低列高度)来实现定位,最后刷新新列的高度,遍历所有格子直到格子全都被排序。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    WaterFall.prototype.show = function(){
    var len = this.boxList.length;
    var conLeft = this.container.offsetLeft;
    var conTop = this.container.offsetTop;
    var marginTop = 10;
    var marginLeft = 10;
    for(var i=0; i<len; i++){
    if(i<this.columnNum){
    this.boxList[i].style.left = conLeft + marginLeft + i*this.boxWidth + 'px';
    this.boxList[i].style.top = conTop + 'px';
    this.columnHeight[i] = conTop + this.boxList[i].offsetHeight;
    }else{
    var minColum = this.getMinHeightCol();
    var boxHeight = this.boxList[i].offsetHeight + marginTop;
    this.boxList[i].style.left = conLeft + marginLeft + minColum*(this.boxWidth ) + 'px';
    this.boxList[i].style.top = this.columnHeight[minColum] + marginTop + 'px';
    this.columnHeight[minColum] += boxHeight;
    }
    }
    }
  4. 加载函数,实例化对象。

    1
    2
    3
    4
    window.onload = function(){
    var water = new WaterFull("container");
    water.show();
    }
  5. 当浏览器窗口被缩放时,进行页面重排渲染。

    1
    2
    3
    4
    window.onresize = function(){
    var water = new WaterFull("container");
    water.show();
    }

  考虑到当窗口大小在进行改变但未终止时都要对瀑布流进行重排渲染的话,网页性能会大大降低,所以我采用setTimeout的方法来适当延时窗口大小改变后瀑布流的重排渲染。上面的代码改为:

1
2
3
4
5
6
7
8
 var timer;
window.onresize = function(){
clearTimeout(timer);
timer = setTimeout(function(){
var water = new WaterFall("container");
water.show()
},300);
}

进行到这里,静态的瀑布流效果就完成了。但是现在很多网页不会一下子把格子里面的内容从数据库中导出,而是通过ajax来实现分页请求数据。

动态瀑布流实现过程

下载地址

  1. html部分在上面的基础上加上

    1
    <input type="hidden" name="currentPage" id="currentPage" value="1">
  2. 核心部分跟静态的一样,通过ajax请求数据,每次请求15条数据(其实实现下拉ajax分页请求数据与后台php分页类似)。前提是自己用php写了一个上传图片的功能,并将图片暂存到了uoloads文件夹中,详情请看github

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    function getImage(){
    var html = "";
    var currentPage = parseInt($("#currentPage").val());
    $.ajax({
    url:"getAjax.php",
    type:'post',
    data:"currentPage="+currentPage,
    dataType:'json',
    success:function(result){
    var data = result;
    $.each(data,function(index,value){
    html = " <div class='imgShow'><img src='uploads/"+ value.photo+"'/> </div> ";
    $(".container").append(html);
    var water = new WaterFall("container");
    water.show();
    });
    $("#currentPage").val(currentPage+1);
    }
    })
    }
  3. 下拉加载判断:当滚动条的高度+窗口可视高度==整个文档的高度时,再请求多一页数据,这里的判断条件比较随意,有兴趣者可以进行改进。

    1
    2
    3
    4
    5
    $(window).scroll(function(event){
    if($(window).scrollTop() + $(window).height() == $(document).height()){
    getImage();
    }
    });

  测试了一下,咦?为什么有些图片重叠了,但是在下拉的过程中又正常排列了,一开始我以为是函数的加载顺序搞错了,调试了之后是没问题的,后来看到一种现象,如下图:
waterfall

  • 有问题的代码测试为waterfallAjax2
      经过分析,觉悟到是前一张图片还没有加载完,后一张图片就贴上来的,所以会出现重叠的现象。于是我进行了图片预加载工作。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function preLoad(src){
    var img = new Image();
    img.src = src;
    var html= "";
    img.onload = function(){
    html = " <div class='imgShow'><img src='"+ src+"'/> </div> ";
    $(".container").append(html);
    // 瀑布流显示图片
    var water = new WaterFall("container");
    water.show();
    }
    }

经过这一步后,瀑布流正常显示,瞬间感觉自己棒棒哒~

结尾

  经过整个项目的实现,自己对ajax以及页面布局又有了更好的了解,并且学会在项目中运用自己所学的知识,以达到巩固的效果。

初衷

  在学习中,发现对很多东西都是一知半解,知识结构比较混乱,虽然也有在笔记本上记录自己的学习过程,但终究达不到总结的作用。固搭建此博客,意在记录自身的学习过程,与志同道合之人聊聊技术,分享所学心得。

  以前有了解过用wordpress搭建博客,但最近从朋友中了解到用github也可以,于是心血来潮,建成此博客。

  此博客的搭建快速简单,容易上手(对于程序员来说),但其中也遇到一些坑爹问题。希望这篇博客对大家搭建自己的博客和解决遇到的问题会有所帮助。

  下面开始博客搭建之旅吧~~

搭建

博客搭建的原理是用静态页面生成静态博客,使用github pages来搭建,不过github pages只有300M免费空间,所以尽量省着点用吧。但一般生成的html都不会很大,主要是图片占空间。为了节省空间,图片可以使用图床保存然后接网址过来。。
  
准备工作可以参考以下网址:

在上面的链接中,如果你没有购买域名,没有关系,可以直接跳到安装准备软件步骤。首先,你要准备好以下软件(注意安装路径不要有中文字符,安装位置随意):

  • Node.js
  • Git

然后按照教程注册一个github并且配置SSH keys。成功后,如果没有购买域名,忽略将独立域名与 GitHub Pages 的空间绑定这一步以及后面的步骤,因为其实没有什么用处。
接下来可以查看这个链接继续:

前面有些重复的步骤可以不用理,跳到安装hexo这里,用命令行安装,这步需要按照hexo官网上的安装命令来安装,因为有可能更新了安装命令。后续步骤按照这篇博客进行安装即可。
(如果在hexo init这步在git bash中没有反应就在命令行中进入到文件夹位置再输入命令行)

注意:到部署这一步时,根目录下_config.yml文件的最下面deploy的配置应该为

1
2
3
4
5
6
# Deployment
## Docs: http://hexo.io/docs/deployment.html
deploy:
type: git
repo: git@github.com:xurna/xurna.github.io.git
branch: master

其中每个属性后面都要加一个空格!!!并且type的属性值为gitrepository改为repo,repo的属性值为你刚刚在github所建的**.github.io仓库的SSH地址,复制下来即可。

最后的hexo deploy命令需要在git bash中进行,在执行 hexo deploy 后,如果出现 error deployer not found:github 的错误,解决办法是输入命令npm install hexo-deployer-git --save,然后在部署,问题解决。

tips
hexo现在支持更加简单的命令格式了,比如:

  • hexo g == hexo generate
  • hexo d == hexo deploy
  • hexo s == hexo server
  • hexo n == hexo new

这样,博客就部署到网上了,试试在新的网页中打开你刚建立的**.github.io网址,如果页面正常显示,那就大功告成了,恭喜你拥有了属于你的个人博客。

完成

  进行到这步,你的搭建博客之旅算是到了尾声。但是,挑剔的人会觉得博客的主题并不好看,其实还有很多hexo主题的,我用的是jacman主题,在github中搜索“jacman”,找到“wuchong/jacman”就可以查看相关文档。
在git bash中在你的博客文件夹目录下输入:

1
$ git clone https://github.com/wuchong/jacman.git themes/jacman

就可以把主题克隆下来,然后修改配置文件_config.yml:

1
2
3
4
# Extensions
## Plugins: https://hexo.io/plugins/
## Themes: https://hexo.io/themes/
theme: jacman

theme的属性值改为主题的名字jacman

1
language: zh-CN

如果一开始出现繁体字,可以加上语言设置,一般情况下最好设置一下,更多样式修改请查看配置文件。

这样就可以使用

1
hexo new "blog name"

开始建立自己的第一篇博客了,博客语法为markdown语法,这里不详细说了,可以查看文档http://www.markdown.cn/.

建成了个人博客,是不是有点小激动和成就感?