T1、作用域,作用域链,闭包
作用域
先来谈谈变量的作用域变量的作用域无非就是两种:全局变量和局部变量。
全局作用域:
最外层函数定义的变量拥有全局作用域,即对任何内部函数来说,都是可以访问的。
局部作用域:
和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到,而对于函数外部是无法访问的。
需要注意的是,函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明了一个全局变量!
javascript并没有所谓的块级作用域,javascript的作用域是相对函数而言的,可以称为函数作用域。
作用域链(Scope Chain)
那什么是作用域链?
我的理解就是,根据在内部函数可以访问外部函数变量的这种机制,用链式查找决定哪些数据能被内部函数访问。
想要知道js怎么链式查找,就得先了解js的执行环境
执行环境(execution context)
每个函数运行时都会产生一个执行环境,而这个执行环境怎么表示呢?js为每一个执行环境关联了一个变量对象。环境中定义的所有变量和函数都保存在这个对象中。
全局执行环境是最外围的执行环境,全局执行环境被认为是window对象,因此所有的全局变量和函数都作为window对象的属性和方法创建的。
内部环境可以通过作用域链访问所有的外部环境,但是外部环境不能访问内部环境中的任何变量和函数,这些环境之间的联系是线性,有次序的。每个环境都可以向上搜索作用域链以查询变量和函数名;但任何环境都不能向下搜索作用域链而进入另一个执行环境。
延长作用域链:(1)try-catch语句的catch块 (2)with语句
with语句主要用来临时扩展作用域链,将语句中的对象添加到作用域的头部。
看下面代码
person={name:"yhb",age:22,height:175,wife:{name:"lwy",age:21}}; with(person.wife){ console.log(name); }
with语句将person.wife添加到当前作用域链的头部,所以输出的就是:“lwy".
with语句结束后,作用域链恢复正常。
这两个语句都会在作用域链的前端添加一个变量对象。with语句会将指定的对象添加到作用域链中。对于catch语句来说,回创建一个新的变量对象。
js的执行顺序是根据函数的调用来决定的,当一个函数被调用时,该函数环境的变量对象就被压入一个环境栈中。而在函数执行之后,栈将该函数的变量对象弹出,把控制权交给之前的执行环境变量对象。举个例子:
<script>
var scope = "global";
function fn1(){
return scope;
}
function fn2(){
return scope;
}
fn1();
fn2();
</script>
上面代码执行情况演示:
了解了环境变量,再详细讲讲作用域链。
当某个函数第一次被调用时,就会创建一个执行环境(execution context)以及相应的作用域链,并把作用域链赋值给一个特殊的内部属性([scope])。然后使用this,arguments(arguments在全局环境中不存在)和其他命名参数的值来初始化函数的活动对象(activation object)。当前执行环境的变量对象始终在作用域链的第0位。
以上面的代码为例,当第一次调用fn1()时的作用域链如下图所示:
(因为fn2()还没有被调用,所以没有fn2的执行环境)
可以看到fn1活动对象里并没有scope变量,于是沿着作用域链(scope chain)向后寻找,结果在全局变量对象里找到了scope,所以就返回全局变量对象里的scope值。
标识符解析是沿着作用域链一级一级地搜索标识符地过程。搜索过程始终从作用域链地前端开始,然后逐级向后回溯,直到找到标识符为止(如果找不到标识符,通常会导致错误发生)—-《JavaScript高级程序设计》
那作用域链地作用仅仅只是为了搜索标识符吗?
再来看一段代码:
<script>
function outer(){
var scope = "outer";
function inner(){
return scope;
}
return inner;
}
var fn = outer();
fn();
</script>
outer()内部返回了一个inner函数,当调用outer时,inner函数的作用域链就已经被初始化了(复制父函数的作用域链,再在前端插入自己的活动对象),具体如下图:
一般来说,当某个环境中的所有代码执行完毕后,该环境被销毁(弹出环境栈),保存在其中的所有变量和函数也随之销毁(全局执行环境变量直到应用程序退出,如网页关闭才会被销毁)
但是像上面那种有内部函数的又有所不同,当outer()函数执行结束,执行环境被销毁,但是其关联的活动对象并没有随之销毁,而是一直存在于内存中,因为该活动对象被其内部函数的作用域链所引用。
具体如下图:
outer执行结束,内部函数开始被调用
outer执行环境等待被回收,outer的作用域链对全局变量对象和outer的活动对象引用都断了
像上面这种内部函数的作用域链仍然保持着对父函数活动对象的引用,就是闭包(closure)
闭包:
(1)解释:
由于在JS中,变量的作用域属于函数作用域,在函数执行后作用域就会被清理、内存也随之回收,但是由于闭包是建立在一个函数内部的子函数,子函数的作用域链上存在自身的活动对象,和外部环境的活动对象以及全局活动对象,当外部函数被调用时,内部函数的作用域链就已经被初始化了,当外部函数执行结束,执行环境被销毁,由于该活动对象被其内部函数的作用域链所引用,所以其关联的活动对象并没有随之销毁,而是一直存在于内存中 。所以说内部的子函数也就是闭包,便拥有了访问上级作用域中的变量的权限。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
(2)为什么其他非闭包的函数没有权限访问另一个函数的内部作用域+为什么闭包有这个权限?
答:见上面的解释。
(3)闭包的用途、解决了什么问题?
- 读取其他函数内部的变量
- 让变量的值始终保存在内存中
实点的说法应该是下面这样:
由于闭包可以缓存上级作用域,那么就使得函数外部打破了“函数作用域”的束缚,可以访问函数内部的变量。以平时使用的Ajax成功回调为例,这里其实就是个闭包,由于上述的特性,回调就拥有了整个上级作用域的访问和操作能力,提高了极大的便利。开发者不用去写钩子函数来操作上级函数作用域内部的变量了。
(4)闭包有哪些应用场景
闭包随处可见,一个Ajax请求的成功回调,一个事件绑定的回调方法,一个setTimeout的延时回调,或者一个函数内部返回另一个匿名函数,这些都是闭包。简而言之,无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都有闭包的身影。
(5)闭包有哪些劣势?
原理比较深奥:要想完全掌握闭包,一定要清楚函数作用域、内存回收机制、作用域继承等,然而闭包是随处可见的,很可能开发者在不经意间就写出了一个闭包,理解不够深入的话很可能造成运行结果与预期不符。
代码难以维护:闭包内部是可以缓存上级作用域,而如果闭包又是异步执行的话,一定要清楚上级作用域都发生了什么,而这样就需要对代码的运行逻辑和JS运行机制相当了解才能弄明白究竟发生了什么。
比其他函数占用更多的内存:由于闭包会携带包含它的外部函数的作用域,因此会比其他函数占用更多的内存。过渡的使用闭包可能会导致内存占用过多。
内存泄漏:
在闭包中调用外部函数的局部变量,会导致这个局部变量无法及时被销毁,相当于全局变量一样会一直占用着内存。如果需要回收这些变量占用的内存,可以手动将变量设置为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 ,因而形成了循环引用,导致两个对象都无法被回收。要解决这个问题,只需要把循环引用中的变量设为 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');
}
注意:闭包所保存的是整个变量对象,而不是某个特殊的变量。闭包只能取得包含函数中任何变量的最后一个值。
闭包与变量
闭包只能取得包含函数中任何变量的最后一个值。
function createFunctions(){
var result = new Array();
for(var i=0; i<10;i++){
result[i] == function(){
return i ;
}
}
return result;
}
( 结果返回10 )
解决办法:创建另一个匿名函数强制让闭包的行为达到预期。
function createFunctions(){
var result = new Array();
for(i=0;i<10;i++){
result[i]=function(num){
return function(){
return num;
};
}(i);
}
return result;
}
( 结果返回0~9 )
关于this对象
(1):全局函数中:this等于window;
(2):匿名函数的执行环境具有全局性,因此其this对象通常指向window.
( 返回外部环境中的name ----The Window)
var name = "The Window"; // 创建全局name
var object = { //创建包含name属性的对象
name:"My name";
getNameFunc:function(){ //包含一个方法 getNameFunc()
return function(){ //返回一个匿名函数
return this.name; //匿名函数返回this.name
}
}
};
alert(objext.getNameFunc(){});
原因: 内部函数在搜索this和arguments时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数的这两个变量;
解决方法:把外部作用域的this对象保存在一个闭包能够访问到的变量里。
说明: 定义匿名函数前,我们把this 对象赋值给that变量,在定义了闭包之后,闭包也可以访问之歌变量,因为它是我们在包含函数中特意声明的一个变量,及时函数返回,that仍然引用object,所以返回了“ My name” ( 返回匿名函数中的name ----My name)
var name = "The Window";
var object = {
name:"My name";
getNameFunc:function(){
var that = this;
return function(){
return that.name;
}
}
};
alert(objext.getNameFunc(){});