Javascript闭包篇(Closure)--Javascript学习笔记(五)
作者:永远的冰点 发布时间:July 16, 2011 分类:Javascript
一、闭包的概念
In computer science, a closure is a function that is evaluated in an environment containing one or more
bound variables. When called, the function can access these variables.
闭包是个函数,而它“记住了周围发生了什么”。表现为由“一个函数”体中定义了“另一个函数”
“闭包”是一个表达式(一般是函数),它具有自由变量以及绑定这些变量的环境(该环境“封闭了”这个表达式)。(闭包,就是封闭了外部函数作用域中变量的内部函数。但是,如果外部函数不返回这个内部函数,闭包的特性无法显现。如果外部函数返回这个内部函数,那么返回的内部函数就成了名副其实的闭包。此时,闭包封闭的外部变量就是自由变量,而由于该自由变量存在,外部函数即便返回,其占用的内存也得不到释放。)
闭包就是能够读取其他函数内部变量的函数。
闭包是有权访问另一个函数作用域中的变量的函数。
闭包是javascript中很难理解的部分,很多高级的应用都依靠闭包来实现的。可以先不用仔细理解前面关于闭包的定义。可以先看下面的一些内容,看完后再回头看前面的定义有助于对闭包的理解。
要理解闭包,首先要理解变理的作用域,见Javascript变量作用域篇。
先看下面的一个例子:
function outer() {
var i = 100;
function inner() {
console.log(i);
}
}
上面代码,根据变量的作用域,函数outer中所有的局部变量,对函数inner都是可见的;函数inner中的局部变量,在函数inner外是不可见的,所以在函数inner外是无法读取函数inner的局部变量的。
既然函数inner可以读取函数outer的局部变量,那么只要将inner作为返会值,就可以直接在ouer外部读取inner的局部变量。
function outer() {
var i = 100;
function inner() {
console.log(i);
}
return inner;
}
var rs = outer();
rs();
这个函数有两个特点:
1、函数inner嵌套在函数ouer内部;
2、函数outer返回函数inner。
这样执行完var rs = outer()后,实际rs指向了函数inner。这段代码其实就是一个闭包。也就是说当函数outer内的函数inner被函数outer外的一个变量引用的时候,就创建了一个闭包。
二、闭包的作用
function outer() {
var i = 100;
function inner() {
console.log(i++);
}
return inner;
}
var rs = outer();
rs(); //100
rs(); //101
rs(); //102
上面的代码中,rs是闭包inner函数。rs共运行了三次,第一次100,第二次101,第三次102,这说明在函数outer中的局部变量i一直保存在内存中,并没有在调用自动清除。
闭包的作用就是在outer执行完毕并返回后,闭包使javascript的垃圾回收机制(grabage collection)不会回收outer所占的内存,因为outer的内部函数inner的执行要依赖outer中的变量。(另一种解释:outer是inner的父函数,inner被赋给了一个全局变量,导致inner会一直在内存中,而inner的存在依赖于outer,因些outer也始终于在内存中,不会在调用结束后被垃圾收集回收)。
1、闭包有权访问函数内部的所有变量。
2、当函数返回一个闭包时,这个函数的作用域将会一直在内存中保存到闭包不存在为止。
三、闭包与变量
由于作用域链的机制,闭包只能取得包含函数中任何变量的最后一个值。看下面例子:
function f() {
var rs = [];
for (var i=0; i <10; i++) {
rs[i] = function() {
return i;
};
}
return rs;
}
var fn = f();
for (var i = 0; i < fn.length; i++) {
console.log('函数fn[' + i + ']()返回值:' + fn[i]());
}
函数会返回一个数组,表面上看,似乎每个函数都应该返回自己的索引值,实际上,每个函数都返回10,这是因为第个函数的作用域链上都保存着函数f的活动对象,它们引用的都是同一变量i。当函数f返回后,变量i的值为10,此时每个函数都保存着变量i的同一个变量对象。我们可以通过创建另一个匿名函数来强制让闭包的行为符合预期。
function f() {
var rs = [];
for (var i=0; i <10; i++) {
rs[i] = function(num) {
return function() {
return num;
};
}(i);
}
return rs;
}
var fn = f();
for (var i = 0; i < fn.length; i++) {
console.log('函数fn[' + i + ']()返回值:' + fn[i]());
}
这个版本中,我们没有直接将闭包赋值给数组,而是定义了一个匿名函数,并将立即执行匿名函数的结果赋值给数组。这里匿名函数有一个参数num,在调用每个函数时,我们传入变量i,由于参数是按值传递的,所以就会将变量i复制给参数num。而在这个匿名函数内部,又创建了并返回了一个访问num的闭包,这样,rs数组中每个函数都有自己num变量的一个副本,因此就可以返回不同的数值了。
四、闭包中的this对象
var name = 'Jack';
var o = {
name : 'bingdian',
getName : function() {
return function() {
return this.name;
};
}
}
console.log(o.getName()()); //Jack
var name = 'Jack';
var o = {
name : 'bingdian',
getName : function() {
var self = this;
return function() {
return self.name;
};
}
}
console.log(o.getName()()); //bingdian
五、内存泄露
function assignHandler() {
var el = document.getElementById('demo');
el.onclick = function() {
console.log(el.id);
}
}
assignHandler();
以上代码创建了作为el元素事件处理程序的闭包,而这个闭包又创建了一个循环引用,只要匿名函数存在,el的引用数至少为1,因些它所占用的内存就永完不会被回收。
function assignHandler() {
var el = document.getElementById('demo');
var id = el.id;
el.onclick = function() {
console.log(id);
}
el = null;
}
assignHandler();
把变量el设置null能够解除DOM对象的引用,确保正常回收其占用内存。
五、模仿块级作用域
任何一对花括号({和})中的语句集都属于一个块,在这之中定义的所有变量在代码块外都是不可见的,我们称之为块级作用域。
(function(){
//块级作用域
})();
六、闭包的应用
1、保护函数内的变量安全。如前面的例子,函数outer中i只有函数inner才能访问,而无法通过其他途径访问到,因此保护了i的安全性。
2、在内存中维持一个变量。如前面的例子,由于闭包,函数outer中i的一直存在于内存中,因此每次执行rs(),都会给i加1。
扩展阅读:
- http://jibbering.com/faq/notes/closures/
- http://www.cn-cuckoo.com/2007/08/01/understand-javascript-closures-72.html
- http://www.kryogenix.org/code/browser/secrets-of-javascript-closures/
Javascript变量作用域篇--Javascript学习笔记(四)
作者:永远的冰点 发布时间:July 13, 2011 分类:Javascript
一、变量声明
变量用关键字var来声明,如:
var i;
var num;
var a, b;
var name ='bingdian';
var i = 0, j = 1 , k=6;
var关键字声明的变量是永久的,用delete运算符删除这些变量将会引发错误。
x = 1;
delete x;
console.log(x); //x is not defined
javascript的变量类型是松散类型的,可以用来保存任何数据类型,可以在修改变量值的同时修改变量类型:
var answer = 10;
x = 'The answer is ' + 10
y = 10 + ' is the answer'
console.log(x); // The answer is 10
console.log(y); // 10 is the answer
var a = '17' - 8; //9
var b = '17' + 8; //178
console.log(a);
console.log(b);
使用var关键字多次声明同一个变量是合法的。
使用一个未声明的变量,会引发错误。
二、变量作用域
变量的作用域是程序中定义这个变量的区域。函数内声明的变量只在函数内部起作用(声明局部变量一定要使用var关键字声明)。
在函数内部,局部变量作用域优先级高于同名全局变量,例:
var i = 99;
var foo = function() {
var i = 10;
console.log(i);
}
foo(); //10 使用局部变量
console.log(i); //99 使用全局变量
声明局部变量一定要使用var关键字,使用var关键字声明变量时,变量会自动添加到距离最近的可用环境中。如果没有写var, 变量就会暴露在全局上下文中, 这样很可能会和现有变量冲突. 另外, 如果没有加上, 很难明确该变量的作用域是什么, 变量也很可能像在局部作用域中, 很轻易地泄漏到 Document 或者 Window 中, 所以务必用var去声明变量。例:
var a = 3;
var foo = function() {
a = 10;
b = 22;
console.log(a);
console.log(b);
}
foo(); //10 22
console.log(a); //10
console.log(b); //22
如果变量在未声明的情况下被初始化,该变量会自动添加到全局环境。看下面两个例子:
function add(a ,b) {
var sum = a+b;
return sum;
}
var rs = add(2,3);
console.log(rs); //5
console.log(sum); //sum is not defined
function add(a ,b) {
sum = a+b;
return sum;
}
var rs = add(2,3);
console.log(rs); //5
console.log(sum); //5 sum在被初始化赋值时没用var关键字,调用完add()后,添加到全局变量的sum继续存在。
javascript执行代码时,会创建一个上下文执行环境,全局环境是最外围的环境。每个函数在被调用时都会创建自己的执行环境,当函数执行完,当前执行gg'f环境被销毁。每个执行环境都有一个与之关联的作用域链。在执行代码时,javascript引擎会通过搜索执行环境的作用域链来解析变量和函数名这样的标识符。解析过程从作用域链的前端开始,向上逐级查询与给定名字匹配的标识符,一旦找到标识符,搜索过程就停止,如果没找到该标识符,则沿作用域链继续向上搜索,一直搜索到全局对象,如果没有搜索到,则认为该标识符未定义。标识符在作用域链中位置越深,查找和访问它所需要时间越长,所以尽可能使用局部变量。
全局环境只能访问在全局环境中定义的的变量和函数,不能直接访问局部环境中的任何数据。
三、没有块级作用域
var a = 8;
var foo = function() {
console.log(a); //undefined
var a = 5;
console.log(a); //5
}
f();
因为局部变量在整个函数foo()内都有定义的,整个函数中隐藏了全局变量。虽然局部变量在整个函数体中有定义的,但在var语句之后,所以不会被初始化。所以最好在函数的顶部声明函数中所有用到的变量。
四、未定义的变量和未赋值的变量
没有赋值的变量值为undefined,使用未定义的变量会引起错误。
var a;
console.log(a); //undefined
console.log(b); //b is not defined
五、垃圾收集(grabage collection)
javascrip具有自动垃圾收集机制,javascript解释器可以检测到何时程序不再使用一个对象,就把它所占用的内存释放掉。
Javascript函数篇--Javascript学习笔记(三)
作者:永远的冰点 发布时间:July 3, 2011 分类:Javascript
javascript中最好的特性是它对函数的实现。函数包含一组语句,它们是javascript基础模块单元。每个函数对象在创时会带有一个prototype属性,它的值是一个拥有constructor属性且值为该函数的对象。
一、函数的定义
1)函函数保留字function
2)函数名,可以被省略,可以用它的名字递归的调用自己
3)参数
4)函数主体,函数可以有return语句,也可以没有return语句。return语句可以停止函数的运行,并把表达式的值返给函数调用者。如果不包含return语句,函数只执行函数体中每条语句,然后返回undefined对函数调用者。
function add(num1, num2) {
return num1 + num2;
}
//匿名函数
var add = function(num1, num2) {
return num1 + num2;
}
//递归调用
function factorial(x) {
if (x <= 1) {
return 1;
} else {
return x*factorial(x-1);
}
}
二、函数的调用
1)方法调用模式
当函数被保存为对象的一个属性时,我们称它为一个方法。当一个方法调用时,this被绑定到该对象。
var o = {
value: 0,
increment: function(inc) {
this.value += typeof inc === 'number' ? inc : 1;
}
};
o.increment();
console.log(o.value); //1
o.increment(2);
console.log(o.value); //3
o.increment(5);
console.log(o.value); //8
o.increment('test');
console.log(o.value); //9
方法可以用this去访问该对象,所以它能从对象中取值或修改该对象。通过this取得它们上下文的方法称为公用方法。
2)函数调用模式
var sum = add(2, 4); //sum为6
当函数以该模式调用时,this被绑定到全局对象。解决方法:给该方法定义一个变量并赋值为this,那为内部函数就可以通过该变量访问到this。
//方法调用模式
var o = {
value: 0,
increment: function(inc) {
this.value += typeof inc === 'number' ? inc : 1;
}
};
o.increment();
console.log(o.value); //1
o.increment(2);
console.log(o.value); //3
o.increment(5);
console.log(o.value); //8
o.increment('test');
console.log(o.value); //9
//函数调用模式
var add = function(num1, num2) {
return num1 + num2;
}
o.double = function() {
var _self = this;
var op = function() {
_self.value = add(_self.value, _self.value);
};
op();
}
o.double();
console.log(o.value); //18
3)构造函数调用模式
//创建构造函数
var Person = function(name) {
this.name = name;
}
Person.prototype.get_name = function() {
return this.name;
}
var driver = new Person('jack');
console.log(driver.get_name());
4)apply模式
apply方法构建一个参数数组并去调用函数。apply方法接收两个参数,第一个是绑定给this的值,第二个是参数数组。
var arr = [11, 12];
var sum = add.apply(null, arr);
console.log(sum); //23
var teacher = {
name: lucy
};
var name = Person.prototype.get_name.apply(teacher);
console.log(name); //lucy
Javascript对象篇--Javascript学习笔记(二)
作者:永远的冰点 发布时间:June 27, 2011 分类:Javascript
对象(Object)是属性的无序集合,每个属性都有自己的名字和值,每个属性存放一个原始值、对象或函数。
javascript的原始类型包括数字、字符串、布尔值、null、undefined。其它所有值都是对象。javascript中,数组、函数、正则表达式都是对象。
一、创建对象
1)new操作符后面跟Object构造函数
var person = new object();
2)对象字面量表示法
var empty = {};
var person = {
'first-name':'Nicholas',
'last-name':'Zakas',
'age':29
};
var flight = {
airline: 'MU',
number: 2769,
departure: {
IATA: 'NKG',
time: '2011-06-30 07:45',
city: 'Nanjing'
},
arrival: {
IATA: 'XIY',
time: '2011-06-30 09:45,
city: 'XiAn'
}
};
二、检索
检索方法,优先使用.表示法
person['first-name'] //Nicholas
flight.number //2769
person['job'] //undefined
flight.status //undefined
用||运算符来填充默认值
var job = person['job'] || 'none';
var status = flight.status || 'unkown';
尝试检索一个undefined值会导致异常
flight.equipment //undefined
flight.equipment.model //throw 'TypeError'
flight.equipment && flight.equipment.model //undefined
三、更新
属性存在于对象中,属属性值将被替换
person['age'] = 32;
如果对象没有该属性名,则该属性名将被添加到对象中
person.job = 'Software Engineer';
四、引用
对象通过引用来传值,它们永远不会被拷贝。
var x = person;
x.job = 'Software Engineer';
var job = person.job; //x和person引用同一对象,job为'Software Engineer'
var a = {}, b = {}, c = {}; //a b c 引用不同的空对象
var a = b = c = {}; //a、b、c引用同一空对象
五、反射
typeof操作符确定属性类型
typeof flight.number //'number'
typeof flight.status //'string'
typeof flight.arrival //'Object'
typeof flight.manifest //'undefined'
原型链中任何属性也会产生一个值
typeof flight.toString //function
typeof flight.constructor //'function'
另一个方法,hasOwnProperty方法
flight.hasOwnProperty('number') //true
flight.hasOwnProperty(constructor) //false
五、枚举
for in 语句遍历对象中的所有属性名。hasOwnProperty和typeof排除函数、原型链中的属性和不想要的值。
var name;
for (name in person) {
if (typeof person[name] !== 'function') {
console.log(name+':'+ person[name]);
}
}
上面属性名出现的顺序是不确定的。如果要以特定的顺序出现,可以使用数组:
var i;
var properties = [
'last-name',
'fist-name',
'age'
];
var len = properties.length;
for (i = 0; i < len; i++) {
console.log(properties[i]+':'+person[properties[i]]);
}
六、删除
delete person.age
person.age //undefined
七、通用Object属性和方法
八、减少合局变量污染
var APP = {};
APP.person = {
'first-name':'Nicholas',
'last-name':'Zakas',
'age':29
};
APP.flight = {
airline: 'MU',
number: 2769,
departure: {
IATA: 'NKG',
time: '2011-06-30 07:45',
city: 'Nanjing'
},
arrival: {
IATA: 'XIY',
time: '2011-06-30 09:45,
city: 'XiAn'
}
};
Javascript 加载性能优化
作者:永远的冰点 发布时间:May 29, 2011 分类:Javascript,前端优化
浏览器对javascript的处理主要有2部分:下载和执行
下载在有些浏览器中是并行的,有些浏览器中是串行的,如IE8、Firefox3、Chrome2都是串行下载的
执行在所有浏览器中默认都是阻塞的,当js在执行时不会进行html解析等其它操作
阻塞特性:
javascript有个阻塞特性,当浏览器执行javascript代码时,不能同时做其它任何事情。无论当前javascript代码是内嵌还是在外链文件中,页面的下载和渲染都必须停下来等待脚本执行完成。浏览器在下载和执行脚本是进出现阻塞的原因在于,脚本可能会改变页面或javascript的命名空间,它们对后面页面内容造成影响。
一、脚本位置
浏览器在碰到一个引入外部javascript文件的<script>标签时会停下所有工作来下载并解析执行它,在这个过程中,页面渲染和用户交互完全被阻塞了。例:
<html>
<head>
<title>无标题文档</title>
<link rel="stylesheet" type="text/css" href="styles.css" />
<script type="text/javascript" src="file1.js"></script>
<script type="text/javascript" src="file2.js"></script>
<script type="text/javascript" src="file3.js"></script>
</body>
</head>
<body>
<p>页面的内容。。。</p>
</body>
</html>
由于脚本的阻塞特性,页面会在3个javascript文件全部下载执行完成后,页面才会继续渲染,把脚本放在页面顶部会导致明显延迟,通常表现为显示空白页,用户无法浏览内容,也无法与页面交互。
ie8+、firefox 3.5+、safari4+、chrome2+都允许并行下载javascript文件,但是在下载的过程中仍然会阻塞图片等其它资源的下载。
由于脚本会阻塞页面其它资源的下载,因此推荐将javasrcipt尽量放到body标签的底部,以减少对整个页面下载的影响。
二、组织脚本
由于<script>标签在下载时会阻塞页面的渲染,所以减少<script>标签数量有助于改善这一情况。建议将多个javascript文件合并为一个,这样可以减少性能的消耗。同时也可以减少请求的数量。
(参考:在服务端合并和压缩javascript和CSS文件)
三、无阻塞脚本
1、延迟脚本
HTML4 为<script>标签定义了一个defer 属性,它能使这段代码延迟执行,然而该属性只有IE4+支持,因此它不是一个理想的跨浏览器解决方案。声明了defer 属性的script会在DOM加载完成,window.onload 事件触发前被解析执行:
<html>
<head>
<title>script defer example</title>
</body>
</head>
<body>
<script defer>
alert('defer');
</script>
<script>
alert('script');
</script>
<script>
window.onload = function(){
alert('load');
}
</script>
</body>
</html>
这段代码在支持defer属性的浏览器弹出顺序是:script、defer、load;不支持defer属性的浏览器弹出的顺序是defer、script、load。
2、动态脚本元素
<script type="text/javascript">
function loadScript(url, callback) {
var script = document.createElement('script')
script.type = 'text/javascript';
if (script.readyState) { //for ie
script.onreadystatechange = function() {
if (script.readyState == 'loaded' || script.readyState == 'complete') {
script.onreadystatechange = null;
callback();
}
};
} else { //other browser
script.onload = function() {
callback();
};
}
script.src = url;
document.getElementsByTagName('head')[0].appendChild(script);
}
</script>
loadscript函数用法
<script type="text/javascript">
//单个文件
loadScript('file1.js', function(){
alert('loaded!');
});
//多个文件
loadScript('file1.js', function(){
loadScript('file2.js',function(){
loadScript('file3.js', function(){
alert('all files loaded!');
});
});
});
</script>
这种技术的重点在于:无论何时启动下载,文件的下载和执行过程不会阻塞页面其它进程,你甚至可以将代码放在页面的head区域而不影响页面的其它部分(下载该文件的http链接除外)。
3、XMLHttpRequest 脚本注入
此技术会先创建一个XHR对象,然后用它下载javascript文件,最后创建动态的script元素将代码注入到页面中。
<script type="text/javascript">
var xhr = new XMLHttpRequest();
xhr.open('get', 'file1.js', true);
xhr.onreadystatechange = function() {
if (xhr.status >= 200 && xhr.status <300 || xhr.status == 304) {
var script = document.createElement('script');
script.type = 'text/javascript';
script.text = xhr.responseText;
document.body.appendChild(script);
}
};
xhr.send(null);
</script>
这种方法优点是可以直接下载javascript代码但不立即执行。由于代码是在<script>标签之外返回的,因此下载后不会自动执行,这使得是可以把脚本推迟到你准备好的时候。这种方法的局限性在于javascript文件必须与所请求的页面处于相同的域,这意味着javascript文件不能从cdn下载,因此不适合大型网站或项目。
四、推荐的无阻塞加载方式
1、YUI3的方式
2、LazyLoad(1.5k)
Yahoo!Search工程师Ryan Grove创建的一个通用的延迟加载工具,是loadScript()函数的增强版。
用法示例:
<script type="text/javascript" src="lazyload-min.js"></script>
<script type="text/javascript">
LazyLoad.js('the-reset.js', function(){
Application.init();
});
</script>
LazyLoad同样支持多个javascript文件,并能保证在所有浏览器中都可以按正确的顺序执行。要加载多个javscript文件,只需要给LazyLoad.js()y方法传入一个url数组:
<script type="text/javascript" src="lazyload-min.js"></script>
<script type="text/javascript">
LazyLoad.js(['first.js', 'the-reset.js'], function(){
Application.init();
});
</script>
项目地址:https://github.com/rgrove/lazyload
3、LABjs(4.7k)
LABjs是Kyle Simpson受Steve Sounders的启发实现的无阻塞加载工具。用法示例:
<script type="text/javascript" src="lab.js"></script>
<script type="text/javascript">
$LAB.script('the-reset.js')
.wait(function(){
Application.init();
});
</script>
$LAB.script()方法用来定义需要下载的javascript文件,$LAB.wait()用来指定文件下载并执行完毕后所调用的函数。
要下载多个javscript文件,只需链式调用另一个$LAB.script()方法:
<script type="text/javascript" src="lab.js"></script>
<script type="text/javascript">
$LAB.script('first.js')
.script('the-reset.js')
.wait(function(){
Application.init();
});
</script>
LABjs与众不同的是它管理依赖关系的能力。通常来说,连续的<script>标签意味着文件逐个下载并按顺序执行。
LABjs允许使用wait()方法来指定哪些文件需要等待其它文件。上面的例子中first.js不能保证会在the-reset.js的代码前执行,为了确保这一点,必须在第一个script()方法后调用wait():
<script type="text/javascript" src="lab.js"></script>
<script type="text/javascript">
$LAB.script('first.js').wait()
.script('the-reset.js')
.wait(function(){
Application.init();
});
</script>
项目地址:hhttp://labjs.com/
4、SeaJS(7.5k)
SeaJS 是淘宝玉伯开发的一个遵循 CommonJS 规范的模块加载框架,可用来轻松愉悦地加载任意 javascript 模块。详细请参考:http://seajs.com/docs/
5、do 框架(3.5k)
Do是豆瓣网kejun开发的一个很轻量的Javascript开发框架。目前do.min.js。它的核心功能是对模块进行组织和加载。加载采取并行异步队列的策略,并且可以控制执行时机。Do可以任意置换核心类库,默认是jQuery。
项目地址:https://github.com/kejun/Do
6、RequireJS(13.1k)
项目地址:http://requirejs.org/