作用域
作用域,我的理解是变量和函数能够作用的区域。
在JS中,作用域分为两种:全局作用域和函数作用域。
全局作用域
就是指在全局声明的变量和函数。他们会一直存储在内存中,直到页面关闭。所以你可以在任何位置使用他们,他们一直存在。
函数作用域
在函数中声明的变量或者函数。他们只存在于当前函数中。通常情况下,函数执行完之后就会被销毁,对外部是不可见的。
作用域链
由于函数是可以嵌套的,所以导致作用域也是有嵌套层级关系的,这样就会形成一条从下到上的作用域链。而且下层作用域是可以访问到上层作用域的变量和函数的。我们JS解析引擎就是通过这条作用域链来查找变量的。
作用域链的example:
function a ()
{
function b ()//函数b的作用域链是从b作用域指向a作用域指向全局作用域
{}
}
查找变量的example:
function fun()
{
console.log(b)
}
var b = 1;
fun();
这里的输出结果是1,虽然在fun函数作用域中没有变量b的定义,那么他会去上一级作用域,也就是全局作用域寻找他的声明,如果没有找到就报错。
自由变量
在当前作用域中存在但是没有在当前作用域中声明的变量称为自由变量。他的值就是通过作用域链从上级作用域中获取的。
遮蔽效应
由于同一条作用域链中不同作用域中可能会会出现同名的变量或函数,那么则会出现遮蔽效应。
var a =1 ;
function fun()
{
function a(){}
console.log(a);
}
fun()
输出结果是函数a,原因是fun函数作用域中定义的函数a遮蔽了全局作用域中定义的同名变量a。
变量声明提升
在任何作用域下,在js预解释的过程中,使用var关键字声明的变量都会存在变量声明提升的问题。
console.log(a);//输出undefined,而不是报错。
var a = 1;
原因是经过预解释之后,代码实际上变成了这样:
var a;
console.log(a)
a = 1
函数声明提升
在任何作用域下,在js预解释过程中,函数声明也会提升。但是函数表达式是不会提升的。
console.log(funa);//函数声明提升,所以这里输出的是一个函数
function funa ()
{}
原因是经过预解释之后,代码实际上变成了这样:
function funa()
{}
console.log(funa);
函数表达式实际上是变量的声明提升:
console.log(funb)//undefined
var funb = function (){}
注意事项
- 变量声明提升比函数声明提升提升程度更大,从而导致同名变量和函数声明会导致函数声明会覆盖变量声明。
example:
console.log(a);//输出a函数
var a = 1;
function a (){}
预编译后的代码应该是:
var a ;
function a (){}
console.log(a);
a = 1;
- 同名函数声明,后面会覆盖前面的。
function a (){}
function a (){console.log(1)};
执行环境
这个概念和我们之前的作用域密不可分,得一起食用。我个人的理解就是每个作用域都对应一个执行环境。但是作用域和执行环境又有点不同,作用域应该是一种静态的存在,就是说当我的代码结构确定下来之后,我的作用域的层级结构已经确定好了,作用域链也随之确定,但是执行环境却未必。随着代码的进行,当前所处的执行环境是在动态改变的。
一个作用域对应一个执行环境,而一个执行环境又对应一个对象,在当前环境中声明的变量和函数都储存在其中。同时还包含一个this指针,对于函数还有一个arguments对象。
//1.进入全局环境,此时环境对象是obj1={a:undefined,fun:function(){},this:window}
var a = 1;//2.执行过后,环境对象应该是obj1={a:1,fun:function(){},this.window}
function fun()
{
var b= 1;//执行这一行,fun函数执行环境变成了obj2={b:2,this:window,arguments:[0]}
}//4.执行这一行之后就离开函数环境,进入全局环境
fun();//3.随后执行这一行,执行这一行之后,我们就进入了fun函数执行环境,此时fun函数的执行环境对象为obj2={b:undefined,this:window,arguments:[0]}
执行环境栈
其实我们代码的进行过程就是执行环境对象,不断进栈出栈的过程,一开始我们位于全局环境中,所以执行环境只有上文的obj1,但是当我们执行 fun()之后,我们就进入fun函数环境,相当于把fun环境对象obj2压入到了栈中,当函数执行完之后,我们又退回到全局环境中,fun环境对象就会出栈被销毁回收掉。
执行环境的注意事项
每一次函数调用都会产生一个函数执行环境,他们并不是相关联的。
example:
function sum()
{
var n = 1;
return function sum (){
n++;
console.log(n)
}
}
var a = sum();
var b = sum();
a();//2
a();//3
b();//2
当然这个例子也涉及到了闭包的相关知识.这是什么原因导致的呢?**这是因为两个sum()执行后返回的函数根本就不是一个函数,换言之,a != b。**为什么会导致这样的结果呢??是因为这两个函数根本就储存在不同的空间里。两个sum()产生了两个不同的函数环境对象。当然里面装的内容是一样的,就像是在另一个平行世界里你的孪生兄弟一样的存在。(当然他们都不会被垃圾回收机制回收。这就是闭包。)所以第一个第二个a()操作的是第一个sum环境对象里面的n,1->2->3,而第三个a()是操作的第二个环境对象的n,1->2
闭包
关于闭包的概念,前文已经有所涉及,实际上理解了作用域和执行环境的概念之后,闭包的概念就豁然开朗了。关于闭包的概念有很多,都大同小异。最普遍的一种说法是闭包是定义在函数内部的函数。那下面我来解释一下,为什么被定义在函数内部的函数被称为闭包。
function fun1()
{
var b = 1;
function fun2 ()
{
b++;
}
return fun2;
}
var a = fun1();
这个fun2函数就称之为闭包,其实道理很简单,当我们执行var a = fun1();时,我们实际上是从全局环境进入到了fun1函数环境。fun1的执行环境对象是obj= {b:1,fun2:function(){b++},arguments:[0],window:this},正常情况下,我们执行完fun1函数后,fun1环境对象会被垃圾回收机制清除,为什么呢?道理显而易见,只有在fun2执行过程中才用到了这些定义的变量和函数,离开fun2作用域后,他们的存在就完全没有什么意义了,所以他们会被清除干掉。但是这里我们把fun2return出去赋值给了一个全局变量a,这时候你就不能把这个环境对象干掉了,那么环境对象里的fun2也就从内存中消失了,但是我的全局变量又指向了他,如果你将其干掉,那么我的a就没有值了。所以此次fun1函数的执行环境会被保留下来,不会被回收。我认为这就是闭包的关键所在。
闭包的关键
正如我上文说的,闭包的关键是有没有形成一个没有被垃圾回收机制回收的函数执行环境。(当然这仅仅是我自己的小见解,可能是错误的欢迎指正)
闭包的特点
- js的特性是沿着作用域链下级作用域可以访问上级作用域的变量或函数,而外部作用域没办法访问其子级作用域的变量或函数,这时候闭包的作用就体现出来了,他本身是个子函数,所以他能访问到他父级函数里面的变量和函数。那么我们在外部调用闭包就能在外部获取到函数内部的变量或函数,闭包是沟通外部和内部的桥梁。
- 第二个他能记住诞生闭包的那个函数环境,例如上文所说的这个fun1函数的执行环境。使他不被清除,实际上你每次使用闭包都是基于这个函数环境的。这点大概就是闭包的灵魂。
闭包的用途
闭包的用途其实很广泛。这里列举几个例子有助于我们深入理解闭包。
计数器
- 错误做法
function a ()
{
var start = 0;
start++;
console.log(start);
}
a();
a();
a();
永远输出的是1.为什么呢?因为每次执行a()的时候,你都开辟了一个全新的函数执行环境。之前被操作改变的那个n没有得到保留。这时候闭包的作用就体现出来了。他能记住自己诞生的环境,并且每次使用他都是基于这个环境。所以n的改变可以得到延续。 2. 正确做法
function a ()
{
var start = 0;
return function ()
{
start++;
}
}
var b = a();
b();
b();
b();
注意使用闭包之后一定要记住释放内存,防止内存的泄漏:b = null
闭包和循环
一道经典的面试题
var arr= new Array(5);
for (var i = 0;i<arr.length;i++)
{
arr[i]=function ()
{
console.log(i);
}
}
很明显他要达到的效果就是arri==>输出i,但是实际上他全是输出的5。为什么呢?还是能从函数的执行环境方向分析,这里全是在全局执行环境下的代码。定义的这些函数自身都没有i,所以他会从全局环境中寻找i,当你执行函数的时候,i已然全部变成了5,这时候输出的当然全是5了。
解决方案:
var arr= new Array(5);
for (var i = 0;i<arr.length;i++)
{
arr[i]=(function(n){
return function()
{
console.log(n)
}
})(i)
}
利用闭包,怎么实现的呢?其实要实现效果,主要是要保存好i的值,这里立即执行函数就是完成这个功能,当你立即执行的时候,执行环境进入了立即执行函数的函数环境。函数环境对象存储了obj={n:i(每次不一样),function(){console.log(n)}},然后arr[i]指向这个函数function(){console.log(n)}。一共会产生五个不同的立即执行函数环境对象,他们的n都是不一样的,跟i一致。并且这五个环境都不会被回收,都存在内存里面,当执行对应的arri的时候,实际上就是进入了对应的执行环境。然后就找到了对应的n,从而达到目的。
闭包的注意事项
闭包会使函数环境对象也就是在其中定义的变量或函数始终储存在内存中不会被清除,所以不要滥用闭包否则会对页面性能造成很大影响