从预编译来看变量提升与原型链 | 青训营笔记

68 阅读8分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 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)流程

  1. 首先查找 script 标签中的变量声明,作为GO对象的属性名,值为undefined
  2. 查找函数声明,作为对象的属性名,值为function

变量声明 通过var关键字声明变量

	var a//变量声明
	var a = 111;//变量声明加变量赋值

函数声明 通过function关键字声明函数

function a(){} //函数声明
var a = function(){}//注意!这是函数表达式,不是函数声明

(2)结论

3函数预编译

(1)流程

  1. 在函数被调用时,为当前函数产生AO对象
  2. 查找形参变量声明作为AO对象的属性名,值为undefined
  3. 使用实参的值改变形参的值
  4. 查找函数声明,作为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();

梳理流程

  1. 产生a函数的AO对象,a AO 函数a的scopes: 0:aAO = {b:fun} 1: GO

  2. 产生b函数的AO对象 函数b的scopes: 0:bAO = {c:fun}

    1:aAO = {b:fun} 2: GO

  3. 产生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();
  1. 会不会形成闭包? 会形成闭包,由于b的声明是在外部函数a中的,在内部函数b中使用了b,就会形成闭包 闭包里存放了一个属性,就是b函数

思考

function a(){
	var aa =100;
	function b(){
	var b = 200;
	console.log(b);
	}
	b();
}
a();
  1. 会不会形成闭包 不会,没有使用外部函数的属性,就不会形成闭包

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总结

使用闭包要满足两个条件

  1. 闭包要形成:再内部函数使用外部函数的变量
  2. 闭包要保持:内部函数返回到外部函数的外面