一、JavaScript 预编译
先思考这段代码的打印结果
<script>
console.log(a); // function
var a = 123;
console.log(a); // 123
function a() {
console.log(a);
}
a();
</script>
javascript 是解释型语言,逐行解析,逐行执行。
在JavaScript被真正解析之前,js解析引擎会首先把整个文件进行预处理,以消除一些歧义,这个预处理的过程就成为预编译
(一)全局对象GO (Global Object)
- 在浏览器环境中,js引擎会整合
<script>标签中的内容,产生window对象,这个window对象就是全局对象 - 在node环境中,全局对象是
global对象
1. 全局变量
在<scrip> 标签中声明的变量为全局变量,全局变量会作为window对象的属性存在
2. 整合
所有<script>标签的内容会被整合到同一个全局window对象中
<script>
var a = 100;
console.log(a); // 100
console.log(window.a) // 100
</script>
<script>
// 在这里能访问到a吗? 可以,因为js引擎会把所有<script>标签整合到一起,生成一个window对象
console.log(a); // 100
</script>
3. 全局函数
在<script>标签中声明的函数,就是全局函数,也会被js引擎整合到window对象中.
<script>
function a(){
console.log('111');
}
window.a();
console.dir(window.a);// 使用dir方法打印出a函数的内部结构
</script>
(二)活动对象 AO (Activation Object)
- 也叫做激活对象
- 在函数调用时产生,用来保存当前函数内部的执行环境(Execution Context),也叫执行期上下文。
- 在函数调用结束时销毁
注意:除非函数被调用,否则函数声明的代码,是不会被执行的
1. 局部变量
在函数内部声明的变量叫局部变量,局部变量作为AO对象的属性存在。
注意,函数声明的代码是不会在预编译期被执行的,只有调用函数时,代码才会被执行
function a(){
var i = 0;
console.log(i);
}
a();
如何理解局部变量
在函数a的外部,不能访问i变量,i变量只在函数a的范围内才能使用(这也是作用域的由来)
- 如果不执行函数,不会产生AO对象,就不会存在i属性;
- 如果执行函数,就会产生AO对象,并将变量i作为AO对象的属性;
- 函数执行完后,AO对象被销毁,也就意味着AO对象的i属性也没有了。
2. 局部函数
在函数内部声明的函数,就叫局部函数,局部函数作为AO对象的方法存在.
<script>
function a() {
function b() {
console.log(222)
}
b()
}
a()
</script>
(三)全局预编译
流程:
- 查找变量声明,作为GO对象的属性名,值为undefined
- 查找函数声明,作为GO对象的属性名,值为function
(四)函数预编译
流程
- 在函数被调用时,为当前函数产生一个AO对象
- 查找形参和变量声明作为AO对象的属性名,值为undefined
- 使用实参的值改变形参的值
- 查找函数声明,作为AO对象的属性名,值为function
函数预编译顺序:局部变量声明、局部变量赋值、形参声明、实参赋值、局部函数声明、局部函数赋值
最终编译成果优先级:局部函数 > 实参 > 形参、局部变量
二、作用域与作用域链
(一)作用域
在JS中,作用域分为 全局作用域 和 局部作用域
- 全局作用域:由
<script>标签产生的区域,从计算机角度可以理解为GO对象(浏览器中就是window对象) - 局部作用域:由函数产生的区域,从计算机的角度可以理解为该函数的AO对象 (ES6中引入了块级作用域,也是局部作用域)
(二)作用域链
在JS中,函数存在一个隐式属性,[[scopes]],这个属性用来保存当前函数在执行时的环境(上下文),由于在数据结构上是链式的,也被成为作用域链,我们可以把它理解成一个数组。
(三)作用域链的作用
在访问变量或者函数时,会在作用域链上由内及外,由近及远依次查找。最直观的表现就是:
- 内部函数可以使用外部函数声明的变量
<script>
function a() {
var aa = 111;
function b() {
var aa = 222;
console.log(aa);
}
b();
}
a();
// 1.产生a函数的AO对象,aAO
// 函数a的scopes:
// 0: aAO={aa: undefined,b:funtion} -> aAO={aa: 111,b:funtion}
// 1: GO
// 2.产生b函数的AO对象,bAO
// 函数b的scopes:
// 0: bAO={aa: undefined} -> bAO={aa: 222}
// 1: aAO={aa:111, b: function}
// 2: GO
</script>
结论:
- 内部函数可以使用外部函数的变量
- 外部函数不能使用内部函数的变量
三、闭包
(一)闭包的形成
- 如果在内部函数使用了外部函数的变量,就会形成闭包,闭包保留了外部环境的引用
- 如果内部函数被返回到了外部函数的外面,在外部函数执行完后,依然可以使用闭包里的值(闭包保持)
(二)闭包的保持
- 如果希望在函数调用后,闭包依然保持,就需要将内部函数返回到外部函数的外面
<script>
function a() {
var num = 0;
function b() {
console.log(num++);
}
return b;
}
var demo = a();
demo();// 0
demo();// 1
// 1.全局预编译,产生GO
// GO: { demo: undefined, a:function}
// 2.全局预编译结束,依次执行代码,执行到 var demo = a() 这一行, 会调用a这个函数,对a函数进行预编译,产生aAO, 并返回b给demo
// aAO:{ num: undefined, b: function} -> { num:0, b:function}
// GO:{ demo: b, a:function}
// 3.继续往下执行代码,执行到第一个demo()这一行,相当于执行b函数,对b函数进行预编译,产生bAO
// bAO:{}
// 此时b函数的作用域[[scopes]]为
// 0:bAO {}
// 1:aAO {num:0,b:function} 此时打印num 为0,然后++,变为1
// 2:GO {demo:b, a:function}
// 4.继续往下执行代码,执行到第二个demo()这一行,相当于再次执行b函数,再次对b函数进行预编译,产生新的bAO
// 此时b函数的作用域[[scopes]]为
// 0:bAO {}
// 1:aAO {num:1, b:function} 此时打印num为0,然后++,变为2
// 2:GO {demo:b, a:function}
</script>
(三)闭包的作用
- 在函数外部直接访问函数的私有变量
1. 闭包的优点
- 隐藏变量,避免全局污染
- 可以读取函数内部的变量
2. 闭包的缺点
- 导致变量不会被垃圾回收机制主动回收,有内存泄露的可能