一:前置知识
二:js的执行机制
2.1:执行机制中的基础
先来一段简单的代码,感受一下v8在js中的执行机制
showName()
console.log(myName)//undefined,变量提升
var myName = 'midsummer'
function showName() {
console.log('函数showName被执行')
}//函数showName被执行
原始类型数据存放在栈里
引用类型数据存放在堆里
- 变量提升: js引擎在执行js的过程中把变量声明部分和函数声明部分提升到代码的头部,默认赋值为undefined,声明提升发生在编译阶段,执行阶段不会发生
- 预编译完成之后,开始执行:第二行会打印出undefined,之后myName被赋值为‘midsummer’,然后函数showName被执行,打印'函数showName被执行'
2.2:执行机制中的栈结构
- 栈结构: 特殊的数组,先进后出
- 调用栈: JS引擎用来追踪函数调用关系的
- 栈溢出: 调用栈的内存超过限制
函数执行完毕后,该函数的执行上下文会销毁(出栈)
var a = 2
function add(b,c){
return b+c
}
function addAll(b,c){
var d=10
var result = add(b,c)
return a + result +d
}
addAll(3,6)
以这份代码为例,在执行时,调用栈的变化如下:
1. 全局预编译之后,全局执行上下文入栈,进行全局的执行
2. addAll函数内部预编译之后,addAll执行上下文入栈,进行局部的执行
3. add函数内部预编译之后,add执行上下文入栈,进行局部的执行
4. add函数执行完成,add执行上下文出栈
5. addAll函数执行完成,addAll执行上下文出栈
6. 整份代码执行完毕,全局执行上下文出栈
2.3:执行机制中的作用域链
先上一个简单的例子引入一下
function foo(){
var a =1
let b =2
{
let b =3
var c =4
let d =5
console.log(a)//1
console.log(b)//3
}
console.log(b)//2
console.log(c)//4
console.log(d)//error
}
foo()
-
词法环境中也是一个栈结构
-
先在词法环境中找值,再到变量环境中找值(如图中箭头走向),故console.log(b)为3
-
外层作用域不可以访问内层作用域,故console.log(d)为error
以上是一个关于块级作用域链的例子,接下来为大家展示一个关于函数作用域链的例子,让读者更深入了解以下:
作用域链:并不是在调用栈中从上往下查找,而是看当前执行上下文变量环境中的outer指向来定,而outer指向的规则是,词法作用域在哪里,outer就指向哪里
词法作用域:在函数定义时所在的作用域,由函数声明的位置来决定,跟函数在哪里调用没有关系
function bar () {
console.log(myName);
}
function foo(){
var myName = 'Tom';
bar();
}
var myName = 'Jerry';
foo();
所以,打印结果是根据作用域链得到的“jerry”,而非依据栈结构得到的“Tom”
三:闭包
3.1:作用域与词法作用域
词法作用域:由函数声明的位置来决定,跟函数在哪里调用没有关系
3.2:闭包初识
请读者仔细阅读实例代码,试着自己画出调用栈的动态变化
function foo(){
function bar(){
var age = 18
console.log(myName)
}
var myName = 'midsummer'
return bar
}
var myName = 'Jerry'
var fn = foo()
fn()
读者们思路是不是这样:foo函数执行完成后,出栈,然后fn()的调用带来了bar()的执行,bar执行上下文入栈。可是这样有一个问题,此时在调用栈中的bar执行上下文的outer是指向foo执行上下文,foo执行上下文已经执行完毕出栈了,那不是会报错?其实不然,接下来为大家介绍闭包
在javaScript中,根据词法作用域的规则,内部函数一定能访问外部函数中的变量,当内部函数被拿到外部函数之外调用时,即使外部函数执行完毕,但是内部函数对外部函数中的变量依然存在引用,那么这些被引用的变量会以一个集合的方式保存下来,这个集合就是闭包
在foo执行上下文出栈时,留下bar执行上下文需要的变量集合就是闭包
3.3:闭包的运用
阅读下面代码,请读者们思考是否可以实现count的累加,输出1,2,3?
function add(){
let count = 0
count++
return count
}
console.log(add())
console.log(add())
console.log(add())
显然是不可以的,会输出1,1,1。因为调用一次add之后,执行完毕后,执行上下文会被销毁,再次调用add,会重新预编译和执行,所以还是会输出1。想要达到预期效果,可以将count声明在全局,add执行上下文的销毁不会影响全局变量,但这种做法会带来命名重复,项目维护成本大,这时候闭包就是很好的封装手段,读者可以自行尝试用闭包修改代码,并画出调用栈
function add(){
let count = 0
function fn (){
count++
return count
}
return fn
}
var res = add()
console.log(res())
console.log(res())
console.log(res())
那么,闭包的作用也就不言而喻了
闭包作用:实现变量私有化
四:知识点get
-
变量提升:js引擎在执行js的过程中把变量声明部分和函数声明部分提升到代码的头部,默认赋值为undefined,声明提升发生在编译阶段,执行阶段不会发生
-
执行上下文:
- 变量环境:var声明的变量
- 词法环境:let,const声明的变量
-
调用栈:
- 栈结构:特殊的数组,先进后出
- 调用栈:JS引擎用来追踪函数调用关系的
- 栈溢出:调用栈的内存超过限制
-
作用域链: 并不是在调用栈中从上往下查找,而是看当前执行上下文变量环境中的outer指向来定,而outer指向的规则是,我的词法作用域在哪里,outer就指向哪里
-
词法作用域:在函数定义时所在的作用域,由函数声明的位置来决定,跟函数在哪里调用没有关系
-
闭包: 在javaScript中,根据词法作用域的规则,内部函数一定能访问外部函数中的变量,当内部函数被拿到外部函数之外调用时,即使外部函数执行完毕,但是内部函数对外部函数中的变量依然存在引用,那么这些被引用的变量会以一个集合的方式保存下来,这个集合就是闭包
-
闭包作用: 实现变量私有化