《知识补充》
一、栈内存
1、代码执行的地方,也叫做执行环境栈ECStack;
2、存储变量、基本数据类型;
3、当栈内存被销毁后,存储的基本数据类型的值也随之销毁;
4、栈内存怎么销毁:包含全局执行上下文、函数执行上下文;也是利用null原理;关闭页面。
==》全局执行上下文一般在页面关闭后销毁;
函数执行上下文一般在函数进栈执行完后就出栈销毁,如果被引用,则不出栈。
二、堆内存
1、存储引用数据类型;
==》堆内存释放销毁后,引用值也随之销毁;
==》堆内存释放:(谷歌的垃圾回收机制)当堆内存没有被任何变量占用时,浏览器在空闲时会自主进行内存回收,进行销毁;
2、怎么让堆内存不被占用:利用变量名=null;通过空指针null将变量谁都不指向,原来指向的堆内存就没有被占用了,等待浏览器销毁。
《变量提升》
全局变量提升时间:栈内存形成之后,js代码执行之前;
函数变量提升时间:栈内存中先形成函数执行上下文,其次形参赋值,再变量提升,最后代码执行。
一、什么是变量提升
1、概念
变量提升机制:当栈内存形成,JS代码自上而下执行之前,浏览器首先会把所有带“var”、“function”关键词的进行提前“声明(var)”或“定义(function)”。
=>声明(declare):带var、function的;(没复制默认undefined)
=>定义(defined):只有带function的;(定义就是创建堆内存,给函数变量赋值)
=>变量提升只发生在当前执行上下文内,函数没有执行时在堆内存中存储的是代码字符串、键值对,还没有去栈内存中形成执行上下文;
=>浏览器很懒,不会重复操作,所以在代码执行时,遇到创建函数这部分代码时直接跳过,因为在变量提升时已经完成了(堆内存已有啦);
=>在ES3/ES5语法规范中,只有全局作用域、函数私有作用域才会占用栈内存,其他大括号不形成栈内存。
console.log(a);//undefined;
因为变量提升了,已经声明过,所以不会报错,但是没有赋值,所以是undefined。
var a=12;
2、全局作用域中带var与不带的区别
带var的,是在全局作用域下声明的变量,同样也是给window对象添加的属性。相当于给window去全局对象设置了一个属性,变量值就是属性值。(私有变量和window对象没关系)
=> 变量提升阶段,“var‘’一个变量,同时就已经把变量当做属性赋值给window对象了,只不过没有值,是undefined;
=>全局变量与window中属性存在“映射机制”,双方改变互相影响;(ES3/ES5就是这个问题,ES6中出现let,就让这种映射避免了,let声明变量没有变量提升,同时也不给window对象添加属性)
console.log(window.a);//undefined
var a=12;
console.log(window.a);//12
window.a=13;
console.log(a);//13
不带var时,不是变量,本质是window对象下的属性,变量没有变量提升,也不是window全局对象的属性;只不过本来不是对象的属性时它的值也是undefined;可以用“in”验证。
console.log(a);//报错不再往下执行,没有声明,找不到a,浏览器先按变量中找,
//因为没有变量提升,所以找不到,再去window对象的属性中找,也没找到,报错。
console.log(window.a);//undefined,告诉浏览器这是对象机制啦,
//没有对应属性名也不报错
console.log('a' in window);//false
a=12;//完整写法是window.a=12;
console.log(a);//12
console.log(window.a);//12,上一行的完整写法
3、私有(函数)作用域中带var与不带的区别
带var的在私有作用域变量提升时声明为私有变量;
不带var的不是私有变量,去上一级作用域的变量中找,直到找到window为止,再没有,就给window创建一个对应属性,跟全局下一样(“作用域链”)。私有作用域下操作非私有变量,就是操作别人的变量。
console.log(a,b);//undefined undefined
var a=12,b=12;//两个12在栈内存位置是相同的
function fn(){
console.log(a,b);//undefined 12,当前作用域有变量a,就不去外面找;
//当前没有变量b,所以去外面找。
var a=b=13;//var a=13;b=13;不带VAR不是私有变量,相当于操作作用域链上的变量。b的值相当于把原来的12位置改成13
console.log(a,b);//13 13
}
fn();
console.log(a,b);//12 13
二、变量提升情况
1、只对等号左边变量提升
不管等号右边是不是函数,只看左边是不是var或者function;
sum();//2;函数的变量提升不仅声明还定义;
console.log(fn);//undefined,当做一般var变量对待;
fn();//报错 Uncaught TypeError: fn is not a function
var fn=function(){console.log(1);};//函数表达式,也是常用的,符合先声明定义后再使用。
function sum(){console.log(2);};
2、在条件判断下
- 在当前作用域下,不管条件是否成立,都会进行变量提升;
- 带var的,还是只声明;
- 带function的,在老版本浏览器渲染机制下,还是声明、定义都进行;
但新浏览器中为迎合ES6(有块级作用域),只进行声明,跟var一样。
全局下,没有var、function,匿名函数也没有变量提升;
f=function(){return true;};
g=function(){return false;};
~function(){//匿名函数自身没有变量提升,定义后立即执行,形成私有作用域;
if(g() && []==![]){//新浏览器中条件判断中的function提升只进行声明,所以function g=undefined不能执行;
f=function(){return false;};//私有作用域中没有,去外面找,找不到给window对象定义这个属性;
function g(){return true;}//私有变量
}
}();
console.log(f());
console.log(g());
新浏览器:报错!
旧浏览器:false false
注:
新浏览器为响应ES6块级作用域,在条件判断内部的变量提升为正常提升(var:声明,function:声明+定义)。
console.log(fn);//undefined,新浏览器条件下的变量提升:只声明;
if(1==1){
console.log(fn);//这里是函数本身,块级作用域中正常的函数变量提升:声明+定义
function fn(){console.log('ok');}
}
3、重名情况
同一作用域中,变量重名,不会重新声明,而是覆盖定义,(不管是变量提升还是代码执行皆如此!)
//变量提升后:fn = function(){console.log(4)}
fn();
function fn(){console.log(1);}//变量提升:声明+定义
fn();
function fn(){console.log(2);};
fn();
var fn = 100;//变量提升时只声明,执行到这赋值
fn();//报错,Uncaught TypeError: fn is not a function
function fn(){console.log(3);};
fn();
function fn(){console.log(4);};
fn();
执行结果:4 4 4 报错
三、Let创建的变量,不存在变量提升(ES6)
变量提升机制本质上是不好的,对代码太包容,所以为了代码的严谨性,ES6后不推荐使用var,使用 let/const 不存在变量提升,且切断了全局变量与window属性的映射机制!!
console.log(a);//变量机制,报错,没有定义
let a=12;
console.log(window.a);//undefined,对象机制不报错
console.log(a);//12
1、Let定义变量的特点
- 执行上下文形成后,代码执行前,没有变量提升;
- 定义变量时,不再给window对象添加属性;
- 浏览器提前检查代码,let定义的变量不能与代码中其他变量、window属性重名,否则直接报错,此时代码还没有执行;
变量提升机制×<==>变量检测机制√:在代码执行之前,浏览器对代码进行重复性检测,自上而下查找当前作用域下所有变量(给window添加的属性也算进去了),一旦发现与使用 let 定义的变量有重复,直接抛出异常,代码不执行(虽然没有把变量提前声明或定义,但浏览器已经记住当前作用域有哪些变量了)。
<1>、没有变量提升
console.log(a);//报错
let a=10;
<2>、不再给window添加属性
let a=12;
console.log(window.a);//undefined
<3>、变量监测机制
let a=10,b=10;
let fn = function(){
console.log(a,b);//报错,浏览器检测机制;
//代码执行前检测到内部作用域有变量a,且是let定义的,没有变量提升,所以提前使用会报错;
//而不是沿作用域链找。
let a=b=20;//let a=20;b=20;去上一级找到变量b修改
console.log(a,b);
}
fn();//如果报错那行去掉,20 20
console.log(a,b);//如果报错那行去掉,10 20
在相同作用域中,基于let不能声明一个与其他变量或window新属性(eg:a=12)重名的变量,如果已经声明或定义过,再用let声明就会报错;
a=12;//报错
let a=12;
2、包含Let变量的代码分析思路
针对JS代码,以浏览器视角,先检查区分代码中的ES6新语法、老语法,看看新老语法中同一作用域下变量是否有重复,有重复就报错啦,代码不执行;新语法走变量检测机制,老语法走变量提升机制。
3、暂时性死区
基于 let 创建变量,会把大部分{}当做一个块级作用域(类似函数的私有作用域),在这里会重新进行语法检测,看是否是基于新语法创建的变量,如果是就按新语法规范来解析。
var a=12;
if(true){
console.log(a);//报错:a未定义,不是因为变量重复,因为有块级作用域隔开了;这里是因为提前使用了,a还没有声明。
let a=13;
}
在原有浏览器渲染机制下,基于typeof等逻辑符检测一个未被声明过的变量,不会报错,返回undefined;而es6使用快外声明了但是块内没有声明的变量会报错===这就是es6的暂时性死区!
console.log(typeof a);//undefined
'a' in window;//false
ES6使用 let 带来了暂时性死区:
console.log(typeof a);//报错;没有声明之前不能检测。
let a;
四、全局变量与私有变量
1、函数私有作用域中私有变量
- 首先形参赋值,然后变量执行;
- 私有变量就不去外面找;只有不是私有变量的变量才沿着作用域链去找,最后找不到就给window对象添加这个属性;
- 变量是基本类型时:全局变量跟私有变量中形参之间没有关系;
- 变量是引用类型时:全局变量与私有变量的形参有关联。
2、函数中私有变量只有两种:形参;声明过的变量(带var/function)。
全局变量是基本类型:
var a=12,b=13,c=14;
function fn(a){
console.log(a,b,c);//12 undefined 14;首先a是形参先赋值=12,b是变量提升有值=undefined,c没有去上一级找;
var b=c=a=20;//==>var b=20;c=20;a=20;
console.log(a,b,c);//20 20 20
}
fn(a);//小括号是实参,相当于fn(12)
console.log(a,b,c);//12 13 20
全局变量是引用类型:
var ary=[12,23];
function fn(ary){
console.log(ary);//[12.23];此处是形参赋值,没有变量提升。
ary[0]=100;
ary=[100];
ary[0]=0;
console.log(ary);//[0]
}
fn(ary);
console.log(ary);//[100,23]
五、上级作用域
当前函数执行,形成一个私有作用域A,A的上级作用域是谁,和它在哪里执行没有关系,和它在哪(创建)定义有关系。在哪里创建的上级作用域就是谁;
上级作用域与THIS不同:
- 上级作用域用于作用域链的查询,是变量的查找;【和函数在哪创建有关】
- this是对调用者的查询;【和函数被谁执行有关】
var a=12;
function fn(){
//arguments:实参集合
//arguments.callee:函数本身
//arguments.callee.caller:函数的宿主,在哪执行(全局作用域下是null)
console.log(a);
console.log(arguments.callee.caller);//function sum(){...}
}
function sum(){
var a=120;
fn();
}
sum();//12,上级作用域是创建时的。
六、闭包及堆栈内存释放
1、堆内存
堆内存:存储引用数据类型存放的空间。
堆内存释放:
让所有引用堆内存空间地址的变量=null即可,浏览器在空闲时就会自主销毁这个堆内存;
2、栈内存
栈内存(执行环境栈):存储基本类型值的空间;代码执行的地方。
- 全局栈内存(执行上下文):打开页面时创建;
- 私有栈内存(执行上下文):函数执行时创建。
<1>全局栈内存释放:
- 只有在页面关闭的时候才能被释放掉;
<2>私有栈内存释放:
- 一般情况下,函数执行完成,所形成的私有栈内存都会自动释放,栈内存中存储的值也都会释放;
- 特别地,函数执行完,当前形成的栈内存中,某些内容被此栈内存以外的变量占用了,此时栈内存就不能释放;(释放了,外面就找不到啦!)
var i=1;
function fn(i){
return function(n){
console.log(n+(++i));
}
}
var f=fn(2);
f(3);
fn(5)(6);
fn(7)(8);
f(4);
3、记住:函数执行==>进栈内存,执行完没被占用出栈销毁;被占用就放栈里,等别的函数进栈,然后依次按先进先出顺序出栈。
4、闭包(柯理化函数/惰性函数)
效果:函数包含函数,内部函数可以使用外部函数的私有变量。
概念:
- 函数形成一个私有作用域,保护里面的私有变量不受外界干扰的机制就是闭包;
- 形成一个不销毁的私有作用域就是闭包(市面上的说法);
为了保证JS性能,不销毁的堆栈内存耗性能,少用闭包;
柯理化函数:(闭包常见的一种)
function fn(){
return function(){}//让原本需要多个参数的内部函数只需要传一个实参就可以执行,因为外层函数执行已经传了其他参数;
}
var f=fn();//外层函数不销毁
惰性函数:(闭包的一种)
var utils = (function(){
return{}
}
)();//外层作用域不销毁
本质作用:
- 保护机制:保护自身所需要的上一级函数作用域内私有变量不被外界干扰;(针对内部函数而言)
- 保存机制:形成一个不被销毁的栈内存,在栈内存中保存外层函数的私有变量。(针对外层函数而言)
5、闭包之保护
保护:保护私有变量不被外界干扰
真实项目中,尤其是团队开发时,尽可能减少全局变量的使用,以防止相互之间的冲突(“全局变量污染”);
<1>、那么此时我们完全可以把自己的这一部分内容封装在一个闭包中,让全局变量转换成私有变量。
使用自执行函数封装:(自己的变量都变成私有的变量了),Jquery插件就是这种闭包。
(function(){
var n=12;
function fn(){}
//自己的代码
})();
<2>、当我们又需要暴露一些变量/方法时:(如jquery中’$'为什么能被使用)
方法一:JQuery的方式,通过给window设置属性的方式把需要暴露的方法抛到全局;
(function (){
function jQuery(){//...}
//...
window.jQuery=window.$=jQuery;//把需要外面使用的方法,通过给window加属性暴露出去;
})();
jQuery();
$();
方式二:Zepto的方式,基于return把需要给外面使用的方法暴露;
var Zepto = (function(){
//...
return $;//就是函数对象{xxx:function(){}};
})();
Zepto.xxx();
6、闭包之保存
保存:保存函数执行的私有作用域;
<1>、不用闭包时:(给列表绑定事件)
- 作用域链思想,沿作用域链往上找,找的是全局变量;
- 异步编程思想,所有事件绑定都是异步编程,循环不等待这个事件的执行,所以当我们点击执行时,循环早已结束,让全局的“i”等于循环结束最后的结果 3;
<2>、解决方案:
方法一:自定义属性
function changeTab(index){
for(var i=0;i<tabList.length;i++){
tabList[i].className = '';
contentList[i].className='';
}
tabList[index].className = 'active';
contentList[index].className = 'active';
}
for(var i=0;i<tabList.length;i++){
tabList[i].myIndex=i;
tabList[i].onclick=function(){
changeTab(this.myIndex);//this:给当前元素绑定方法,当事件触发,方法执行的时候,方法中的THIS是当前操作的元素对象(this用法的七种之一)
}
}
方法二:闭包:保存私有作用域(相对方法一,闭包耗性能,所以这里不用)
原理:循环三次,形成三个不销毁的私有作用域(利用自执行函数),每一个都存储了一个私有变量i。
for(var i=0;i<tabList.length;i++){
(function(n){//arguments:{n=i};形参赋值
tabList[n].onclick=function(){
changeTab(n);
}
})(i);
}
方法三:基于ES6语法(性能也不好,形成块级作用,)
let创建变量,存在块级作用域(类似私有作用域);
for(let i=0;i<tabList.length;i++){
tabList[i].onclick=function(){
changeTab(i);
}
}