一、作用域
ES5的作用域用两种:全局作用域和函数(局部)作用域;
ES6新增作用域:在{}中用let、const定义变量会形成块级作用域。
javascript是“先解释,后运行”的语言,在全局作用域和函数作用域中会有将使用var声明的变量提前的问题,如果一个变量a在它声明之前使用,它的值会是undefined,如果这个变量a在作用域内没有声明就使用,会报错,避免出错可以这样使用window.a,因为全局作用域的变量都属于window对象的属性,使用对象未定义的属性的值是undefined。变量声明提前是js的一个重大缺陷,是不规范的,ES6改变了这种缺陷,可以使用let、const声明变量,使用let、const声明的变量必须先声明后使用,也不能重复定义变量,否则报错。
注意:if判断里面使用var声明的变量也会提前:
if(false){
var c = 'c';
}; console.log(c); // undefined
二、作用链和自由变量
var x = 10; // 全局作用域A中的变量x
function fn1() { // 函数作用域fn1
var y = 20;
console.log(x);
var fn2 = function() { // 函数作用域fn2
console.log(y);
}
}
作用域链:全局作用域A ==> 函数作用域fn1 ==> 函数作用域fn2
我们称在函数作用域中通过作用域链使用的变量为自由变量,函数作用域fn1中的x是自由变量,函数作用域fn2中的y是自由变量。函数作用域中自由变量的指向在函数定义的时候就确定了,与函数在哪里调用被谁调用无关,这和函数中的this不同,this的指向只有在函数被调用的时候才确定。
三、函数声明和函数表达式
函数声明: function fn() {},函数声明和变量声明一样,会被提前,但是有区别,函数声明提前fn的值不是undefined;
函数表达式: function() {},也叫匿名函数,函数表达式要么被赋值给变量,要么立即执行,如
// 函数表达式的赋值 var fn = function() {}; // 立即执行函数 var obj = (function(){ return { a: 'a' } })()
四、闭包
var x = 10; // 全局作用域A中的变量x function fn1() { // 函数作用域fn1 var x = 20; var fn2 = function() { // 函数作用域fn2 console.log(x); } return fn2; } var fn3 = fn1(); fn3(); // 20
函数作用域中x是自由变量,在函数fn2定义的那一刻起它的指向就指定了,根据这一特点我们可以知道结果为20,其实我们将这种在函数调用时return一个使用了该函数中的局部变量的函数到函数外部赋值给一个变量的方式叫闭包。闭包就是将函数内部和函数外部连接起来的一座桥梁。
五、哪些方式可以形成闭包呢?
很明显,由(四)可以知道,通过在函数调用时return一个使用了该函数中的局部变量的函数并且赋值给一个变量的方式形成闭包,这种方式是将函数当做返回值。还有别的方式吗?
另外一种方式就是返回一个对象并赋值到另外一个变量,对象中有函数方法(即函数):
function returnObj() { var num = 800; return { objFn: function() { console.log(num++); } } }
var returnObj1 = returnObj();// 闭包是只有在return的对象被赋值时才形成 returnObj1.objFn();// 800 returnObj1.objFn();// 801 var returnObj2 = returnObj(); // 闭包是只有在return的对象被赋值时才形成 returnObj2.objFn();// 800 returnObj2.objFn();// 801 returnObj2.objFn();// 802
// 或者 new + 构造函数的隐性return返回 function Persion() { var num = 600; this.say = function() { console.log(num++); } } var p1 = new Persion(); p1.say(); //600 p1.say();//601 p1.say();//602 var p2 = new Persion(); p2.say(); //600 p2.say(); //601 p2.say(); //602
形成闭包的两种方式和条件:
1、函数调用时函数作为返回值、函数调用时将含有方法的对象作为返回值(第二种在构造函数中得到充分的运用)。
2、返回值必须赋值;(不赋值就是普通函数的调用,不形成闭包)
六、闭包的用途
闭包作用:
1、将函数作用域中的局部变量像全局作用域中的全局变量一样保存在内存中不被销毁;
2、闭包的每一次形成函数作用域中的局部变量都被重新声明定义了一次,与之前声明定义的局部变量是不一样的,是独立的(即使变量名一样),就好像new+构造函数一样造了很多示例。
var x = 10; // 全局作用域A中的变量x function fn1() { // 函数作用域fn1 var x = 20; xAdd = function() { x++; }; var fn2 = function() { // 函数作用域fn2 console.log(x); } return fn2; } var fn3 = fn1(); fn3(); // 20 xAdd(); fn3(); // 21 xAdd(); fn3(); // 22
由结果可以知道函数f1中的局部变量x一直保存在内存中,并没有在f1调用后被自动清除。
为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。
这段代码中另一个值得注意的地方,就是"xAdd=function(){x++;}"这一行,首先在xAdd前面没有使用var关键字,因此xAdd是一个全局变量,而不是局部变量。其次,xAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以xAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作,也相当于隐形return。
特别注意:不要在函数作用域中使用不通过var声明的变量,函数声明最好也不要使用。
七、使用闭包的注意点
1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
2)闭包会在父函数外部,改变父函数内部变量的值。
var x = 10; // 全局作用域A中的变量x function fn1() { // 函数作用域fn1 var x = 20; function fn2() { // 函数作用域fn2 console.log(x); x++; } return fn2; } var fn3 = fn1(); fn3(); // 20 fn3(); // 21 fn3(); // 22
如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
var name = "The Window"; var object = { name : "My Object", getNameFunc : function(){ return function(){ return this.name; }; } }; console.log(object.getNameFunc()());
结果:The Window。ruturn的匿名函数中的this执行window。
var name = "The Window"; var object = { name : "My Object", getNameFunc : function(){ var that = this; return function(){ return that.name; }; } }; console.log(object.getNameFunc()());
结果:My Object。ruturn的匿名函数中的that执行object。
八、总结
1、自由变量
函数作用域中通过作用域链使用的变量为自由变量。
在函数作用域形成(或者说定义)时,函数作用域中自由变量的指向已经确定,与在哪里调用执行无关。
2、闭包的概念(个人观点):
一个函数将在它里面定义的函数或者一个含有函数的局部对象作为返回值return到函数外部被赋值的行为称为闭包。
3、闭包作用:
1)、将函数作用域中的局部变量像全局作用域中的全局变量一样保存在内存中不被销毁;
2)、闭包的每一次形成函数作用域中的局部变量都被重新声明定义了一次,与之前声明定义的局部变量是不一样的,是独立的(即使变量名一样),就好像new+构造函数一样造了很多示例。
4、使用闭包的注意点
1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
参考链接:
1、http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html