这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天
今天讲到了JavaScript,其中有涉及到let var 的概念,提到let 和 var 我们经常会说var会导致变量提升,那到底为什么会存在变量提升的现象呢?这要从预编译开始说起。
1.概念
(1)什么是预编译
首先,我们要知道JavaScript是解释性语言 解释性:逐行解析,逐行执行 那么,什么是预编译呢? 再JavaScript真正被解析之前,js解析引擎会首先把整个文件进行预处理,以消除一些歧义,这个预处理的过程就被称为预编译
代码实例
console.log(a);
var a = 123;
console.log(a);
function a(){
console.log(a);
}
a();
//第一步:产生window对象
//第二步:查找变量声明,把a作为window对象的属性名,属性的值是undefined
//第三步:查找函数声明,把函数名a作为window对象的属性名,属性值是function
//全局预编译结束,结束后,代码从上到下,依次执行
//a的值改为100
首先大家思考一下 三个console.log分别会打印出来什么 如果要完全理解,就要知道js引擎到底是怎么工作的
第一次打印出a函数 第二次打印a变量 第三次 报错(a不是一个函数);
(2)全局对象 GO
全局对象(global object) 在浏览器环境中,js引擎会整合 < script > 标签种的内容,产生window对象,这个window对象就是全局对象 在node环境中,会产生global对象
全局变量
在 < script > 标签种声明的变量为全局变量,全局变量会作为window对象的属性存在 示例
var a = 100;
console.log(a);
console.log(window.a);
这里在打印的其实就是全局对象的a属性
整合
在第二个script标签中可以访问到a变量吗? 答:可以,因为window对象是一个,故可以访问到window对象的a属性 如果将script以文件src的方式引入,那还可以访问到吗? 依然可以,文件会整合所有的script标签生成一个window对象
全局函数
function a(){
console.log('111');
}
console.log(windows.a);
(3) 活动对象AO
活动对象:也叫激活对象
- 在函数被调用时产生,用来保存当前函数内部的执行环境,也叫执行期上下文
- 在函数调用结束时销毁
局部变量
在函数内部声明的变量叫局部变量,局部变量做为AO对象的属性存在
function a(){
var i = 0;//i为局部对象 问题来了 局部变量在程序中(js引擎里)是以怎样的形式表现出来的呢? 其实就是作为AO对象的属性存在
console.log(i);
}
a();
如何理解局部
在函数a的外部,不能访问变量i,变量i只在函数a的范围内才能使用。其实,这也就是作用域的由来
- 如果不执行函数,不会产生AO对象,就不会存在i属性
- 如果执行函数,就会产生AO对象,并将变量i作为AO对象的属性
- 函数执行完后,AO对象被销毁,也就意味着不能使用i属性
局部函数
在函数内部声明的函数叫局部函数,局部函数做为AO对象的方法存在 示例
function a(){
function b(){
console.log(222);
}//函数b作为AO对象的方法存在
b();
}
a();
首先在全局空间声明一个函数a,调用a函数。之后在函数a的内部声明一个函数b,并调用b函数。
2全局预编译
(1)流程
- 首先查找 script 标签中的变量声明,作为GO对象的属性名,值为undefined
- 查找函数声明,作为对象的属性名,值为function
变量声明
通过var关键字声明变量
var a//变量声明
var a = 111;//变量声明加变量赋值
函数声明
通过function关键字声明函数
function a(){} //函数声明
var a = function(){}//注意!这是函数表达式,不是函数声明
(2)结论
3函数预编译
(1)流程
- 在函数被调用时,为当前函数产生AO对象
- 查找形参和变量声明作为AO对象的属性名,值为undefined
- 使用实参的值改变形参的值
- 查找函数声明,作为AO对象的属性名,值为function
(2)示例
示例一
function a(test) {
var i = 0;
function b(){
console.log(222);
}
b();
}
a(1);
注意!先完成全局预编译,再完成函数预编译
示例二
function a(test){
console.log(b);
var b = 0;
console.log(b);
function b(){
console.log(222);
}
}
a(1);
示例三
function a(b,c){
console.log(b);
var b = 0;//
console.log(b);
function b(){
console.log(222);
}
console.log(c);
}
a(1);
示例四
function a(i){
var i;
console.log(i);
}
a(1);
(3)结论
- 只要声明了局部函数,局部函数的优先级最高
- 没有声明局部函数,实参的优先级最高
- 整体来说:局部函数>实参>形参和局部变量
二、作用域与作用域链
1概念
(1)域
域:范围、区域
在js中,作用域分为全局作用域和局部作用域
-
全局作用域:由 script 标签产生的区域,从计算机的角度可以理解为window对象
-
局部作用域:由函数产生的区域,从计算机的角度可以理解为该函数的AO对象
-
可将全局作用域理解成GO对象 将局部作用域理解成AO对象
(2)作用域链
在js中,函数存在一个隐式属性[[scopes]],这个属性用来保存当前函数在执行时的环境(上下文),由于在数据结构上是链式的,也被称为作用域链。我们可以把他理解成一个数组。
函数类型存在[[scopes]]属性
function a(){}
console.dir(a);//打印内部结构
[[scopes]]属性在函数声明时产生,在函数被调用时更新 [[scopes]]属性记录当前函数的执行环境 在函数被调用时,将该函数的ao对象压入[[scopes]]对象中
[[scopes]]中只有一个元素
0:Global{window:window,self:window,.............}
说明该函数可以使用Global中的属性和方法
当函数被调用的时候就会形成一个该函数的AO对象
function a(){}
a();
console.dir(a);
当函数执行的那一刻,是存在AO对象的,此时scopes[0] 是 AO scopes[1]是 GO
当函数执行完成时,AO对象就被销毁了,此时scopes[0]是 GO
代码示例
function a() {
console.dir(a);
function b() {
console.dir(b);
function c() {
console.dir(c);
}
c();
}
b();
}
a();
梳理流程
-
产生a函数的AO对象,a AO 函数a的scopes: 0:aAO = {b:fun} 1: GO
-
产生b函数的AO对象 函数b的scopes: 0:bAO = {c:fun}
1:aAO = {b:fun} 2: GO
-
产生c函数的AO对象
0:cAO={}
1:bAO = {c:fun}
2:aAO = {b:fun} 3: GO
2作用
作用域链有什么作用呢?
在访问变量或者函数时,会在作用域链上依次查找,最直观的表现是:
- 内部函数可以使用外部函数声明的变量
示例
function a(){
var aa=111;
function b(){
console.log(aa);
}
b();
}
a();
- 在函数a中声明定义了变量aa
- 在函数b中没有声明,却可以使用
思考如果在函数b中,也定义同名变量aa会怎么样
三.闭包
如果在内部函数使用了外部函数的变量,就会形成闭包,闭包保留了外部环境的引用 如果内部函数被返回到了外部函数的外面,在外部函数执行完毕后,依然可以使用闭包里的值
1闭包的形成
在内部函数使用外部函数的变量,就会形成闭包,闭包是当前作用域的延伸
示例
function a(){
var aa= 100;
function b(){
console.log(aa);
}
b();
}
a();
此时由于在函数b中访问了aa,所以aa会作为闭包的属性保存
但是当程序退出函数b后,闭包会被销毁,所以虽然执行b函数时形成了闭包,但是闭包并没有保持住
从代码的角度看,闭包也是一个对象,闭包里面包含哪些东西呢?
在内部函数b中使用了外部函数a中的变量,这个变量就会作为闭包对象的属性!!
思考
function a(){
var aa =100;
function b(){
console.log(b);
}
b();
}
a();
- 会不会形成闭包? 会形成闭包,由于b的声明是在外部函数a中的,在内部函数b中使用了b,就会形成闭包 闭包里存放了一个属性,就是b函数
思考
function a(){
var aa =100;
function b(){
var b = 200;
console.log(b);
}
b();
}
a();
- 会不会形成闭包 不会,没有使用外部函数的属性,就不会形成闭包
2闭包的保持
如果希望在函数调用后,闭包依然保持,就需要将内部函数返回到外部函数的外部
示例
function a(){
var num = 0;
function b(){
console.log(num++);//形成闭包
}
return b;
}
var demo = a();
console.dir(demo);
demo();//b()
demo();
//分析代码
//首先产生GO对象 GO:{demo:undefined,a:fun}
// 1.产生aAO
//aAO:{num:undefined,b:fun}
//aAO:{num:0,b:fun}
// return b :将b返回给demo 此时demo里面保存的是b
//GO:{demo:b,a:fun}
//2.产生bAO
//demo() 调用b函数时,产生bAO:{}
//[[scopes]] 0:bAO
// 1:aAO
// 2:GO
//先引用再自增 console.log -> 0
//aAO:{num:1,b:fun}
//函数执行完后bAO被销毁
//3.再执行一次,产生一个新的bAO对象
//bAO:{}
//[[scopes]] 0:bAO
// 1:aAO
// 2:GO
//先引用再自增 console.log -> 1
//aAO:{num:2,b:fun}
3总结
使用闭包要满足两个条件
- 闭包要形成:再内部函数使用外部函数的变量
- 闭包要保持:内部函数返回到外部函数的外面