day-032-thirty-two-20230321-变量提升-作用域-闭包
变量提升
- 当浏览器开辟出供js执行的栈内存之后,代码并不是立即自上而下执行,而是需要先做一些事情
- 把当前作用域中带var和function 的关键字进行提前的声明和定义,这叫做变量提升机制
- var 和function 在变量提升阶段区别
- var 在变量提升阶段,是只声明,未定义(不赋值)
- function 在此阶段是声明和定义都完成(ES5,主要场景)
- 只声明,未定义(ES6)
- 变量提升的特殊性
-
不论判断条件是否成立,都会进行变量提升
- 在当前作用域下,不管判断条件是否成功,都会进行变量提升
- function:
- 新版本浏览器中的function 只是声明,不定义
- 在老版本中还是声明和定义(仅限判断语句)
- 判断语句里,会以函数体为分界线,上面为全局作用域,下面为块级作用域
- function语句到块级作用域上方,应用ES5规则
- 既声明又赋值,把声明与赋值提到if语句的第一个花括号开头那。并且该变量是全局变量
- 既对这个变量的操作在此时还是操作全局变量的
- function语句到块级作用域下方,应用ES6规则
- 只声明不赋值,到执行时才声明,并且该变量是块级变量
- function语句到块级作用域上方,应用ES5规则
- var :声明操作提到到私有作用域上方,赋值操作还在原地
- function:
- 在当前作用域下,不管判断条件是否成功,都会进行变量提升
-
只对等号左边的进行变量提升
console.log(fn); console.log(fn(1,2)); var fn=function (n,m){ return n+m; } console.log(fn(3,4)); //相当于 var fn; console.log(fn);//undefined console.log(fn(1,2));//fn is not a function fn=function (n,m){ return n+m; } console.log(fn(3,4));
-
return 不阻断变量提升,只会阻断后面的代码执行
-
return 下面的代码虽然不能执行,但是可以进行变量提升,return 后面的代码不进行变量提升
function show(){ var a=10; console.log(a,b);//10 undefined return true;//中断 var b=20; console.log(a,b); } show()
function fn(){ console.log(f2);//整个f2函数 return function f1(){ } function f2(){ console.log("f2") } } fn();
-
-
如果变量名字重复,依旧会变量提升
-
var变量提升 声明一次,后面的语句在后面会在原地等待进行赋值
console.log(num);//undefined var num=2; console.log(num);//2 var num=3; console.log(num);//3 //相当于 var num console.log(num);//undefined num=2; console.log(num);//2 var num=3; console.log(num);//3 //实际 var num console.log(num);//undefined num=2; console.log(num);//2 num=3; console.log(num);//3
-
function变量提升 声明一次,赋值多次。顺序依旧会改变,函数都放前面了
console.log(fn)//ƒ fn(){ console.log(3); } function fn(){ console.log(1); } function fn(){ console.log(2); } function fn(){ console.log(3); } //相当于 function fn(){ console.log(1); } function fn(){ console.log(2); } function fn(){ console.log(3); } console.log(fn)//ƒ fn(){ console.log(3); } //实际 function fn(){ console.log(1); } fn=function fn(){ console.log(2); } fn=function fn(){ console.log(3); } console.log(fn)//ƒ fn(){ console.log(3); }
-
-
自执行函数在当前所在的作用域中不进行变量提升,自执行函数自己所形成的私有作用域照常进行
a=1 console.log(a);// 1 // 自执行函数在此处不进行变量提升 (function (){ console.log(a);// undefined, 照常进行变量提升 var a=3; })(); console.log(a);// 1
-
带var与不带var区别
- 变量提升
- 带var的可以变量提升
- 不带var的不能变量提升
- 是否可被delete删除
-
带var的不可被删除
-
不带var的可以被删除
b = 100 console.log('b' in window);//true console.log(delete window.b);//true
b = 100 console.log('b' in window);//true console.log(delete window.b);//true
-
var a = 100 console.log('a' in window);//true //console.log(delete window.a);//false console.log(delete a);//false b = 100 console.log('b' in window);//true //console.log(delete window.b);//true console.log(delete b);//true
- 变量提升
-
作用域
作用域的概念
- 作用域就是变量与函数的可访问范围。
- 看变量的作用域是谁,就看它是在哪定义的
作用域分类
-
全局作用域EC(G): 全局执行上下文
- 全局变量(VO(G))
- 在全局作用域中定义的变量就是全局变量
- window----GO------全局对象
- 变量提升
- 全局变量(VO(G))
-
函数形成的私有作用域EC(fn): 私有的函数执行上下文
-
私有变量(AO(fn))
- 形参
- 在函数私有作用域中定义的变量
-
在私有作用域中,如果没有定义这个私有变量,就会向上一级作用域进行查找
- 如果都没有,继续进行查找,直到查找到全局作用域EC(G)
- 如果还没找到,就去window上找
- 关于window与全局作用域变量VO
- 在全局作用域如果此变量声明的时候用了 let、const,就相当于给VO添加一个这样的变量,
- 在全局作用域如果声明的时候用了var、function或者压根没声明就相当于给GO全局对象window添加了一个属性
- 也相当于给window添加了一个属性
- 如果在window上还没找到
- 如果只是查找,就会报错
- 如果是赋值,就相当于给GO全局对象window添加属性和属性值
- 关于window与全局作用域变量VO
- 如果还没找到,就去window上找
- 如果都没有,继续进行查找,直到查找到全局作用域EC(G)
-
初始化步骤
- 确定作用域链 <EC(G),EC(fn)>
- 全局变量始终是
- 上一级作用域,只看函数在那里创建的,不要看在那里执行的
- 变量提升
- 变量提升提到私有变量(AO(fn))里 - 形参赋值
- 也在私有变量(AO(fn))里 - 初始this
- 也在私有变量(AO(fn))里 - 初始arguments
- 也在私有变量(AO(fn))里 - 代码进栈执行
//console.log(b);//报错 b is not defined function fn(){//0x001 // EC(fn):私有的函数执行上下文 // 私有变量( AO(fn) ): // 1.作用域链 <EC(G),EC(fn)> 上一级:只看函数在哪里创建的,不要看在哪里执行的 // 2.变量提升 --- // 3.形参赋值 --- // 4.初始this --- // 5.初始arguments --- // 6.代码进栈执行 b=13; console.log(b);//13 } fn(); console.log(b);//13
console.log(a,b); var a=12,b=12; function fn(){ console.log(a,b); var a=b=13; console.log(a,b); } fn(); console.log(a,b);
var x=[12,23]; function fn(x){ x[0]=100; x=[100]; x[1]=200; console.log(x); } fn(x); console.log(x);
- 确定作用域链 <EC(G),EC(fn)>
-
-
es6新增的块级作用域
内存层级思路
电脑分运行内存与硬盘内存
- 硬盘内存就是固态硬盘与机械硬盘与U盘与内存卡,一般比较大,断电时不丢失,速度慢,一般如图片视频音频等数据就存在这里
- 运行内存就是电脑内存条占用的内存,一般16G到128G,断电时丢失,速度快,软件的操作一般在这里
- 浏览器就运行在运行内存上,它在运行时,为每个页面开辟了两块区域
- 堆内存Heap 这里存储一些引用数据类型(即方法或对象)
- 默认有GO(global Object) 存储一些浏览器内置的属性和方法
- 栈内存ECStack(Execution context Stack)
- 栈内存主要存放EC(Execution Context)
- 全局作用域EC(G): 全局执行上下文、全局上下文。
- 全局变量(VO)----变量对象
Variable Object
。 VO(G)指的是VO与GO合并,简写。- 在全局作用域用let及const等声明的变量
- window 指向椎内存中的GO
- 在全局作用域
用var声明
及用function声明
或无声明关键字直接声明
或在window对象用点语法或中括号
定义的变量
- 在全局作用域
- 全局变量(VO)----变量对象
- 函数作用域EC(函数名): 函数执行上下文、私有上下文。 函数被用小括号调用后生成的作用域
- 局部变量AO—活动对象
Activive Object
,函数的活动对象。
- 局部变量AO—活动对象
- 全局作用域EC(G): 全局执行上下文、全局上下文。
- 栈内存主要存放EC(Execution Context)
- 堆内存Heap 这里存储一些引用数据类型(即方法或对象)
- 浏览器就运行在运行内存上,它在运行时,为每个页面开辟了两块区域
let和const
es6新增的
-
let不存在变量提升
-
阻断了与window的关系
- 在全局作用域里 let声明的变量存放在VO,var声明的变量存放在window里
-
在同一个作用域里不能重复声明
- es6中没有变量提升,但是有一个自我检测的一个机制,在代码自上而下执行前,会先进行检测,看是否有重复声明的变量,如果有的话,就先报错。
- 不同作用域可以重复声明
-
暂时性死区
- let和const声明的变量,在当前作用域里只能声明之后使用,不能在声明之前使用
- let与const会形成一个暂时性死区
- ES6明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
// console.log(a); // let a=10; // let a=10; // console.log(window.a);//undefined // let a=10; // { // let a=20; // } // console.log(a); // let a=2; // console.log(a); // var a=3; // console.log(3);
- let和const声明的变量,在当前作用域里只能声明之后使用,不能在声明之前使用
typeof
一般typeof xxx
如果变量没有,结果就是undefined
console.log(typeof undefinedVariable)//undefined
console.log(typeof undefinedVariable)//undefinedVariable is not defined
let undefinedVariable = 1
上级作用域
- 上级作用域:
- 当前函数执行,形成一个私有作用域A,这个A的上级作用域是谁,跟它在哪执行无关,跟它在哪定义(创建)有关系,在哪创建,它的上级作用域就是谁
- 上级作用域和函数在哪执行无关,和函数在哪定义有关
var a=2;
function fn(){
//没有自己的私有变量---》找上级作用域中变量 EC(G)
console.log(a);//2
}
fn();
function sum(){
var a=3;
fn();//2//而不是3,因为fn是在全局作用域里定义的,故而它的上级作用域依旧是全局作用域
}
sum();
//2
//2
arguments.callee 和arguments.callee.caller
-
arguments.callee:指的是函数本身
function fn(){ console.log(arguments.callee); // 打印出的是fn 函数本身 } fn();
-
arguments.callee.caller 指的是函数执行的宿主环境
- 如果是在函数A中执行,打印出来的就是A。
- 如果是在全局作用域中执行,打印出来的就是null。
- 上级作用域对于一个函数来说一般是不会变的
function fn(){ console.log(arguments.callee.caller);//null---》EC(G) } fn(); // 此时打印出的是 null(在全局作用域中执行)
//arguments.callee.caller 函数在哪个域中执行的 function fn(){ console.log(arguments.callee.caller); } function A(){ fn(); // 此时打印出的是 A这个函数 } A();
闭包
var n=10;
function fn(){
var n=20;
function f(){
n++;
console.log(n);
}
f();
return f;
}
var x=fn();
x();
x();
console.log(n);
闭包概念
- 函数执行过程中,内部的引用类型的对象被外部作用域所引用。
- 函数执行,返回一个引用类型的对象,被外层所占用,得不到释放,就是闭包。
- 闭包使用结束后,可以手动释放。
var n=10;
function fn(){
var n=20;//21 22 23
function f(){
n++;
console.log(n);
return n;
}
f();
return f;
}
var x=fn();
x();//22
x();//23
//x=null;//闭包释放
console.log(n);
闭包的常见形成方式
- 函数返回一个引用类型的对象
function fn(){
var n=20;//21 22 23
function f(){
n++;
console.log(n);
return n;
}
f();
return f;
}
var x=fn();
- 函数外部变量引用了函数内部的引用类型对象
var ary=[];
function fn(){
var num=2;
ary=function(){
console.log(num);
};
};
var f=fn();
闭包的作用
- 【保护】:保护被闭包的
私有作用域私有变量
不会被外界干扰
- 【保存】:形成不销毁的私有作用域,可以把里面的
修改后私有变量的值
保存下来供后续使用
使用闭包的原因
设置一个变量,不被外界所干扰,但又可以在任何地方使用它
内存小知识
【堆内存】:只要用来存储引用数据类型的值(对象存的是键值对,函数存的是字符串)
【栈内存】:供js运行的环境(函数执行),存基本数据类型的值
释放内存的原因
每次给变量存值或者执行函数的时候都会占用内存空间,如果一直这样下去,日积月累,电脑总会装不下的,所以内存是需要释放的。
释放内存可以加快代码运行时间。可以让页面更流畅。
堆内存释放
常见的浏览器堆内存释放方式
- 主要有以下两种:
- 谷歌浏览器是标记方式,每隔一段时间就会检测以下当前作用域中的内存,是否被占用,如果没有被占用,就会释放掉。
- ie和火狐等浏览器是采用计数方法,当前作用域中如果一个空间地址被占用一次,就会累加一,如果减少一次占用就会减1,直到0的时候,说明已经没有被占用了,就释放了。
- 但2023年基本上都是标记方式了,火狐好像已经是了。IE已差不多死了。
堆内存的释放
- 让所有引用这个堆内存的所有变量赋值为null,进而堆内存地址不在被占用,浏览器在空闲的时候就会把堆内存 释放
- 让引用该堆内存的所有变量赋值为基础数据类型应该也行
//代码中,只要对象变量的值等于null,堆就会释放
var obj={};
var obj2={};
obj=null;//obj堆 释放了
obj2=null;//obj2堆 释放了
栈内存的释放
-
全局栈内存,只有当页面关闭的时候才会被释放
-
函数执行完成,一般情况下都会被销毁掉
function fn(x){ return x+3; } fn(1);
-
当前作用域中如果有一个引用数据类型的值被外面的变量占用就不销毁(闭包)
function fn(){ var num=2; return function(){ console.log(num); }; }; var f=fn();
var ary=[]; function fn(){ var num=2; ary=function(){ console.log(num); }; }; var f=fn();
-
函数返回一个结果函数,但结果函数没有被变量或对象属性接收,而是直接被调用。那么不立即销毁,(当函数执行完毕之后销毁)
function fn(x){//0x001 x---1 return function(y){//0x002 y--2 return x+y; } } fn(1)(2);//当两个函数都执行完成后,才会释放 //fn(1)-->0x001(1)===》0x002 //fn(1)(2)--》0x002(2)--->3
-
通过fn(1)(2)(3)得到6
function fn(x){
return function(y){
return function(z){
return x+y+z
}
}
}
fn(1)(2)(3)
闭包与非闭包的内存引用
function fn(i){
return function (n){
console.log(n+(++i));
}
}
var f=fn(2);
f(3);
fn(5)(6);
fn(7)(8);
f(4);
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rPofulyK-1679414077513)(./闭包与非闭包内存引用图.png)]
闭包的实际应用
- 闭包之私有变量的保护应用
-
【jQuery】 通过window添加属性暴漏到全局
(function(){ function jQuery(){ //..... } //把jquer 这个方法通过window添加属性暴漏到全局 window.jQuery=window.$=jQuery; })() //在使用的时候: jQuery() 或者$(); //而在jQuery定义过程中用到的变量,都不会被外部影响到。也不影响到外部使用同名变量去做其它事。
-
【zepto】把自执行函数的通过return把返回结果在外面用一个变量进行接收。
-
通过使用该变量来间接访问或操作闭包内部的数据与方法。
var zepto=(function(){ return { fn:function(){}, //..... } })() // 在使用的时候:zepto.fn 可以调用闭包内的方法,而外界影响不到它内部的代码及变量
-
-