前言
要知道什么是作用域链,我们首先得知道什么是作用域,简而言之,作用域就是变量起作用的范围,在es6之前作用域分为函数作用域(局部作用域)以及全局作用域。
全局作用域:即在全局中定义的变量,代码中的任何地方都能访问到的变量即为全局变量。
函数作用域:变量在函数内部声明后即拥有了函数作用域,对外部来说是无法访问的,但是如果变量在函数内部未声明直接赋值,则该变量为全局变量拥有全局作用域。
什么是作用域链
在对作用域有了一定的了解后,让我们来看看什么是作用域链。
当函数在执行的时候会生成一个[scope](作用域)对象,里面储存了函数执行时执行环境中内部和外部所有的函数和变量,根据js代码的执行顺序:
1.对基础语法进行检测,是否有语法错误。
2.全局预编译:产生一个全局的[scope]对象(简写GO),储存变量及函数声明,预编译分析变量声明及函数声明,变量会提升,即先声明,等代码执行时再赋值。(函数声明会覆盖原有的赋值)
3.函数预编译:产生一个自身的[scope](简写AO)对象,同样储存函数以及变量声明,函数的参数成为该对象的属性,如果未传递则为undefined。
4.解释执行,预编译过程已经分析过了变量及函数声明不需要再声明,只需要赋值。
之后会将预编译过程中产生的[scope]对象按照由外到里的顺序存入一个环境栈中,如下表:
2.自身的AO |
1.外部函数的AO |
0.全局下的GO |
然后函数会对需要调用的变量按照由上到下的顺序进行查找,这种查找的方式呈链式结构,也就是作用域链。
举个栗子:
<script>
var name = 'wuyanzhu'
function fn1() {
var name = 'pengyuyan'
function fn2() {
var name = 'liudehua'
console.log(name);
}
function fn3() {
console.log(name);
}
fn2();
fn3();
}
fn1();
</script>
当执行fn2,fn3时,环境栈中的[scope]对象如下表:
fn2 | fn3 |
2.自身的AO name = liudehua | 2.自身的AO 未声明name |
1.外部函数fn1的AO name = pengyuyan | 1.外部函数fn1的AO name = pengyuyan |
0.全局的GO name = wuyanzhu | 0.全局的GO name = wuyanzhu |
当fn2执行时,会从上往下从自身的AO开始查找name属性,故输出liudehua;
当fn3执行时,同样从自身AO中开始查找name属性,没有就往下一个继续查找,故输出pengyuyan;
一般来说,当函数执行完毕之后,所创建的[scope]对象会自动销毁,其中所保存变量及函数也会随之销毁,这也是外部函数无法访问内部函数变量的原因,但在某些情况下,我们又需要访问内部函数变量,这也就引入了闭包的概念,换句话说闭包就是可以访问其他函数内部变量的函数。
闭包
一.闭包的概念
可以访问其他函数内部变量的函数。
在大致了解了闭包的概念之后,让我们来看看闭包的作用具体用法。
再举个栗子:
<script>
function outer() {
var scope = "outer";
function inner() {
console.log(scope);//输出outer
}
inner();
}
var fn = outer();
console.log(fn);//输出undefined
</script>
如上:正常情况下,内部函数可以访问外部函数的变量,但是外部函数却无法访问内部函数变量,inner正常访问scope输出outer,外部函数outer却无法访问inner中的变量,输出undefined;
那有什么办法呢,我们可以将内部函数作为返回值返回,这样外部函数就可以访问inner中的内部变量了,如下:
<script>
function outer() {
var scope = "outer";
function inner() {
console.log(scope);;
}
return inner;//将内部函数返回到外部
}
var fn = outer();
fn();
</script>
这样就可以将内部函数的变量保存到外部,上述代码中的inner就是闭包。
再再举一个栗子:
<script>
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
})
}
</script>
猜猜输出什么0~4?1~5?nonono,最后输出结果是5个5,这是因为js是单线程的,会先执行完循环再执行计时器,等到执行计时器的时候i已经变成了5,所有打印5个5。
但是要怎么样才能依次打印呢,我们可以使用自调用函数,将i作为参数传递,如下:
<script>
for (var i = 0; i < 5; i++) {
(function (i) {
setTimeout(function () {
console.log(i);
})
})(i)
}
</script>
这里用了闭包来保存变量i,尽管循环结束后i的值变成了5,但是函数每次执行时i的值被记录了下来,所以依次打印出0,1,2,3,4。
二.闭包的优缺点
优点:调用函数内的局部变量,保护函数内变量的安全,防止与其他同名变量形成冲突造成全局污染。
缺点:闭包使用后,会使得引用变量不能被销毁,一直存在内存中,造成内存泄露,我们可以在只用完之后手动为变量赋值为null。