JavaScript重难点解析4(作用域与作用域链、闭包详解)
作用域与作用域链
作用域
就是一块"地盘", 一个代码段所在的区域
它是静态的(相对于上下文对象), 在编写代码时就确定了
分类
- 全局作用域
- 函数作用域
- 没有块作用域(ES6有了)
可以隔离变量,不同作用域下同名变量不会有冲突。
var a = 10 //全局作用域
if(true) {
var b = 3 //全局作用域(没有块)
let c = 3 //ES6块作用域
}
function fn(x) {
var a = 100 //函数作用域
}
console.log( a, b) // 10 3
console.log( c) //报错
作用域与执行上下文
区别1
- 全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了。而不是在函数调用时
- 全局执行上下文环境是在全局作用域确定之后, js代码马上执行之前创建
- 函数执行上下文是在调用函数时, 函数体代码执行之前创建
区别2
- 作用域是静态的, 只要函数定义好了就一直存在, 且不会再变化
- 执行上下文是动态的, 调用函数时创建, 函数调用结束时就会自动释放
联系
- 执行上下文(对象)是从属于所在的作用域
- 全局上下文环境==>全局作用域
- 函数上下文环境==>对应的函数使用域
作用域链
理解
- 多个上下级关系的作用域形成的链, 它的方向是从下向上的(从内到外)
- 查找变量时就是沿着作用域链来查找的
查找一个变量的查找规则
- 在当前作用域下的执行上下文中查找对应的属性, 如果有直接返回, 否则进入2
- 在上一级作用域的执行上下文中查找对应的属性, 如果有直接返回, 否则进入3
- 再次执行2的相同操作, 直到全局作用域, 如果还找不到就抛出找不到的异常
闭包
闭包理解
当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(函数)时, 就产生了闭包,闭包是嵌套的内部函数,产生闭包需要两个条件:函数嵌套,内部函数引用了外部函数的数据(变量/函数)。
我们定义这样一段代码:
function fn1 () {
var a = 2
var b = 'abc'
function fn2 () { //执行函数定义就会产生闭包(不用调用内部函数)
console.log(a)
}
fn2()
}
fn1()
用浏览器打开后可以看到
简单来说,当JS代码中发函数嵌套调用时,内函数会有一个Closure对象来保存外函数作用域的变量。
理解了闭包是什么之后我们看两个例子:
将函数作为另一个函数的返回值
function fn1() {
var a = 2
function fn2() {
a++
console.log(a)
}
return fn2
}
var f = fn1()
f() // 3
f() // 4
按照以往的理解局部在函数执行完后会被清除所以会输出3,3,但为什么会输出3,4那?
在代码执行到var f = fn1()时,函数fn1()执行时发生函数嵌套,内函数Closure对象中就保存了引用的外部变量a,它被当作返回值返回,此时f指向的就是包含了变量a的函数fn2(此时的a可以当作fn2的一个局部变量),由于f指向fn2的指针始终未断,所以a的值一直被保留。
将函数作为实参传递给另一个函数调用
function showDelay(msg, time) {
setTimeout(function () {
alert(msg)
}, time)
}
showDelay('atguigu', 2000)
这个相对好理解一些,showDelay函数与setTimeout里面的函数发生了嵌套,并且内部的函数运用了外函数的msg变量,所以发生了闭包。
循环遍历监听问题
这个问题学过js的同学应该非常熟悉,先看代码:
for (var i = 0,length=btns.length; i < length; i++) {
var btn = btns[i]
btn.onclick = function () {
alert('第'+(i+1)+'个')
}
}
这段代码执行后无论点击那一个按钮都会只打印4,原因很简单,当我们点击按钮时会触发onclick函数内容,但因为js中DOM操作是异步执行的,当这段函数执行时全局函数中的for循环已经执行完成,i的值已经为4,所以无论点击那一个按钮都打印4。
解决方法:
1.ES6
for (var i = 0,length=btns.length; i < length; i++) {
let btn = btns[i] //let支持块作用域
btn.onclick = function () {
alert('第'+(i+1)+'个')
}
}
2.添加属性
for (var i = 0,length=btns.length; i < length; i++) {
var btn = btns[i]
//将btn所对应的下标保存在btn上
btn.index = i
btn.onclick = function () {
alert('第'+(this.index+1)+'个')
}
}
3.闭包
for (var i = 0,length=btns.length; i < length; i++) {
(function (j) {
var btn = btns[j]
btn.onclick = function () {
alert('第'+(j+1)+'个')
}
})(i)
}
针对闭包,我们将代码进行改写:
for (var i = 0,length=btns.length; i < length; i++) {
//内存中不存在a,b,这里为了便于理解闭包把立即执行函数拆解一下
function a(j) {
function b(){
alert('第'+(j+1)+'个')
}
var btn = btns[j]
btn.onclick = b
}
a(i)
}
由于变量提升的原因函数定义的语句会在循环之前执行,循环中执行的只有a(i),很明显函数a与函数b发生了闭包,b函数作用域中存在Closure对象保存j的值。所以尽管btn.onclick是异步调用,但由于指向函数b的指针没有丢失,b函数对象一直存在,Closure对象保存j的值也一直存在,所以可以正确弹出i的值。
闭包的生命周期与运用
闭包的生命周期
闭包产生: 在嵌套内部函数定义执行完时就产生了(不是在调用)
闭包死亡: 在嵌套的内部函数成为垃圾对象时
闭包的作用
使用函数内部的变量在函数执行完后, 仍然存活在内存中(延长了局部变量的生命周期)
让函数外部可以操作(读写)到函数内部的数据(变量/函数)
运用
闭包主要运用在自定义JS模块中,可以看我们之前的模块化代码
//module2.js
(function () {
let msg = 'module2';
function foo(){
console.log(msg)
}
window.module2 = {foo}
})()
在使用时:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>02_namespace模式</title>
</head>
<body>
<script type="text/javascript" src="module1.js"></script>
<script type="text/javascript">
module2.foo() //打印‘module2’。
</script>
</body>
</html>
msg为module2.js中立即执行函数的私有变量,只允许外界查询,但由于它属于函数作用域,正常情况在函数执行完成后就会销毁,我们可以根据闭包的特性,通过内函数foo()来阅读这个变量,并将函数向外公布。这样msg就会存在foo()的Closure中,只要有指针指向foo,它就不会消失。
闭包的缺点
函数执行完后, 函数内的局部变量没有释放, 占用内存时间会变长,容易造成内存泄露。
内存溢出:程序运行需要的内存超过剩余内存就会抛出内存溢出错误。
内存泄露:占用的内存没有及时释放。太多的内存泄露会导致内存溢出。