JS中的堆栈内存及闭包详解

数据渲染机制及堆栈内存

栈内存:提供代码执行的环境

堆内存:存放东西(存放的是属性和方法)

浏览器会在计算机内存中分配一块内存,专门用来供代码执行的。
=》栈内存ECStack,执行环境栈

全局对象GO。浏览器会让window指向GO,浏览器把内置的一些方法和属性放在一个单独的内存中 ,叫堆内存(heap)

任何开辟的内存都有一个16进制的内存地址,方便后期找到这个内存(isNaN parseInt)

先进后出,垃圾回收机制

VO(变量对象):在当前的上下文中,用来存放创建的变量和值得地方(每一个执行上下文中都会有一个自己的变量对象)。函数私有上下文中叫做AO(活动对象,但是也是变量对象,AO是VO的分支。)

var a = 12;
创建变量和赋值操作的底层操作有3部:

  1. 创建一个值
  2. 创建一个变量
  3. 让变量和值关联

基本数据类型值都是直接存在栈内存中。而引用类型值是先开辟一个堆内存,把地址存储进去,最后把地址放到栈中供变量关联使用的。

所有的指针赋值都是指针的关联指向。

对象:

  1. 创建一个堆内存
  2. 把键值对存储到堆内存中
  3. 堆内存地址放到栈中,供变量调用
  • 栈内存也是作用域(包含全局栈内存 和 私有栈内存)
  1. 提供一个供JS代码自上而下执行的环境(代码都是在栈中执行的)
  2. 由于基本数据类型值比较简单,他们都是直接在栈内存中开辟一个位置,把值直接存储进去的

=>当 栈内存被销毁,存储的那些基本值也都跟着销毁了。

  • 堆内存:引用值对应的空间
  1. 存储引用类型值得(对象:键值对 函数:代码字符串)

=> 当前堆内存释放销毁,那么这个引用值彻底没了
=> 堆内存的释放:当堆内存没有被任何的变量或者其他东西所占用,浏览器会在空闲的时候,自主的进行内存回收,把所有不被占用的堆内存销毁掉(谷歌浏览器)

xxx=null 通过空对象指针null可以让原始变量(或者其他东西)谁都不指向,那么原有被占用的堆内存就没有被东西占用了,浏览器会销毁它。

考察 创建变量和赋值操作的底层操作有31. 创建值
    + 开辟一个堆AAAFFF000
    + 存储键值对
        name:'hhy'
        fn:自执行函数执行,需要把obj.name的值当做实参传递进来 =>其实是obj找不到,因为这个时候还没赋值 undefined.name
    
var obj = {
    name: 'hhy',
    fn: (function(x){
        return x + 10;
    })(obj.name)//在给fn赋值的时候,是把自执行函数执行的返回结果赋值给fn属性
}
console.log(obj.fn)  // 报错name找不到

复制代码

学会画框框,分析变量提升和栈堆内存,才能出正确的结果

变量提升

当栈内存(作用域)形成,JS代码自上而执行之前,浏览器首先会把所有带var、function关键词的进行提前“声明”或者“定义”,这种预先处理机制称之为“变量提升”。

  • 声明(declare):var a (默认值undefined)
  • 定义(defined):a=12 (定义其实就是赋值操作)

变量提升阶段

  • 带var的只声明未定义

  • 带function的声明和赋值都完成了

  • 变量提升只发生在当前作用域

例如:开始加载页面的时候只对全局作用域下的进行提升,因为此时函数中存储的都是字符串而已

  • 在全局作用域下声明的函数或者变量是“全局变量”,同理,在私有作用域下声明的变量是“私有变量”

带var和funciton的才是声明

  • 浏览器很懒,做过的事情不会重复执行第二遍,也就是,当代码执行遇到创建函数这部分代码后,直接的跳过即可(因为在提升阶段就已经完成函数的赋值操作了)

  • 私有作用域形成后,也不是立即代码执行,而是先进行遍历提升(变量提升钱,先形参赋值)

  • 在ES3/ES5语法规范中,只有全局作用域和函数执行的私有作用域(栈内存),其它大括号不会形成栈内存

带var和不带的区别

在全局作用域下声明一个变量,也相当于给window全局对象设置了一个属性,变量的值就是属性值(私有作用域中声明的私有变量和window没啥关系)

console.log(a) // undefined
console.log(window.a) //undefined
console.log('a' in window) // true  
在变量提升阶段,在全局作用域中声明了一个变量A,此时就已经把A当做属性赋值给window了,只不过此时还没有给A赋值,默认值undefined

in:检测某个属性是否隶属于这个对象
复制代码

**全局变量和window中的属性存在“映射机制” **

console.log(a) // 报错
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
复制代码

不加var的本质是window的属性

var a = b = 12; 这样写的b是不带var的
相当于 
var a = 12;
b = 12;
复制代码
console.log(a,b) //undefined  undefined
var a = 12, b =12;
function fn(){
    console.log(a,b) //undefined  12
    var a = b = 13;
    console.log(a,b)//13 13
}
fn()
console.log(a,b); //12 13
复制代码

私有作用域中带var和不带也有区别:

  1. 带var的在私有作用域变量提升阶段,都声明为私有变量,和外界没有任何的关系
  2. 不带var不是私有变量,会向它的上级作用域查找,看是否为上级的变量,不是,继续向上查找,一直找到window为止(我们把这种查找机制叫做:作用域链),也就是我们在私有作用域中操作的这个非私有变量,是一直操作别人的

变量提升的一些细节

只对等号左边进行变量提升

在当前作用域下,不管条件是否成立成立都要进行变量提升

  • 带var的还是只声明
  • 带function的在老版本浏览器渲染机制下,声明和定义都处理,但是为了迎合es6中的块级作用域,新版浏览器对于函数(在条件判断中的函数),不管条件是否成立,都只是先声明,没有定义,类似于var
console.log(a) //undefined
if('a' in window){
    var a = 100
}
console.log(a) //100
复制代码
f = function () {return true} ; => window.f=...
g = function () {return false}; =>window.g=...
//自执行函数肯定有私有作用域
~function (){
//变量提升:function g; //=>g是私有变量
    if(g() && [] == ![]){  //新版本报错  g is not a function(此时g是undefined)
        f = function() {return false} =>把全局中的d进行修改
        function g() {return true} => 私有
    }
}()
console.log(f())
console.log(g())  //执行全局的

//老版本if会执行,输出false  false
复制代码

条件判断下的变量提升到底有多坑

console.log(fn) //undefined  变量提升
if(1 === 1){
    console.log(fn) 
    //函数本身
    function fn(){
       console.log('ok')  
    }
}
console.log(fn) //函数本身
复制代码

当条件成立,进入到判断体中(在ES6中它是一个块级作用域)第一件事并不是代码执行,而是类似于变量提升一样,先把fn声明和定义了,也就是判断体中代码执行之前,fn就已经赋值了。

变量提升机制下重名的处理

  1. 带var 和 function 关键字声明相同的名字,这种也算是重名了(其实是一个fn,只是存储值得类型不一样)

  2. 关于重名的处理:如果名字重复了,不会重新的声明,但是会重新的定义(重新赋值)【不管是变量提升还是代码执行阶段都是如此】

fn()  //4
function fn(){console.log(1);}
fn() //4
function fn(){console.log(2);}
fn() //4
var fn=100; //带var提升只是声明,没有赋值,代码执行的时候赋值
fn(); //报错,fn is not a function  ,报错了后面的不会执行
function fn(){console.log(3);}
fn()   //这里执行不到
function fn(){console.log(4);}

fn函数声明并赋值
复制代码

ES6的let不存在变量提升

在es6中基于let、const等方式创建变量或者函数,不存在变量提升机制。

  • 切断了全局变量和window属性的映射关系

在相同的作用域中,基于let不能声明相同名字的变量(不管用什么方式在当前作用域下声明了变量,再次使用let创建都会报错)

虽然没有变量提升机制,但是在当前作用域代码自上而下执行之前,浏览器会做一个重复性检测(语法检测);自上而下查找当前作用域下所有变量,一旦发现有重复的,直接抛出异常,代码也不会在执行了(虽然没有把变量提前声明定义,但是浏览器已经记住了,当前作用域下有哪些变量)

a = 12;//报错:a has already been declared
console.log(a);
let a = 13;
console.log(a);
复制代码
var a = 12;//报错:a has already been declared
console.log(a);
let a = 13;
console.log(a);
复制代码
let a = 10, b = 10;
let fn = function(){
//私有作用域
    console.log(a,b)  //报错:a is not defined
    let a = b = 20;
}
fn();
console.log(a,b)
复制代码

ES6的暂时性死区问题

var a= 12if(true){
    //块级作用域
    console.log(a) //报错:a is not defined
    let a = 13;
}
复制代码

基于let创建变量,会把大部分{}当做一个私有的块级作用域(类似于函数的私有作用域),在这里也是重新检测语法规范,看一下是否是基于新语法创建的变量,如果是按照新语法规范来解析。

暂存死区

console.log(typeof a) // undefined
console.log(typeof a)  // 报错:a is not defined
复制代码

在原有浏览器渲染机制下,基于typeof等逻辑运算符检测一个未被声明过的变量,不会报错,返回undefined

console.log(typeof a)// 报错:a is not defined
let a;
复制代码

如果当前变量是基于es6语法处理,在没有声明这个变量的时候,使用typeof检测会直接报错,不会是undefined,解决了原有的JS死区。

私有变量和全局变量

var a = 12, b = 13, c = 14;
function fn(a) {
    console.log(a,b,c); //12 undefined 14
    var b = c = a = 20;
    console.log(a,b,c) //20 20 20
}
fn(a)
console.log(a,b,c) //12 13 20
复制代码

有关私有作用域和作用域链的练习

var ary = [12, 23];
function fn(ary){
// 形参ary是私有变量,和全局ary指向的地址是一样的
    console.log(ary);
    ary[0] = 100;
    ary = [100];  // 重新指向新的地址,此处ary是私有的,跟全局地址没关系了
    ary[0] = 0;
    console.log(ary)  // [0]
}
fn(ary)
console.log(ary) // [100, 23]
复制代码

注意:形参是私有变量

上级作用域查找

当前函数执行,形参衣蛾私有作用域A,A的上级作用域是谁,和他在哪执行的没有关系,和他在哪创建有关系,在哪里创建的,它的上级作用域就是谁。

var a = 12;
function fn() {
    console.log(a) //12
}
function sum() {
    var a = 120;
    fn()
}
sum()
复制代码
var n = 10;
function fn() {
    var n = 20;
    function f() {
        n++;
        console..log(n); 
    }
    f();
    return f;
}
var x = fn();  // 第一次结果是21
x();  // 第一次结果是22
x();  // 第一次结果是23
console.log(n)//10
复制代码

闭包及堆内存释放

堆内存:存储引用数据类型值(对象:键值对 函数:代码字符串)
栈内存:提供JS代码执行的环境和存储基本类型值

  • 【堆内存释放】

让所有引用堆内存空间地址的变量赋值为null即可(没有变量占用这个对内存了,浏览器会在空闲的时候把它释放掉)

  • 【栈内存释放】

一般情况下,当函数执行完成,所形成的私有作用域(栈内存)都会自动释放掉(在栈内存中存储的值也都会释放掉),但是也有特殊不销毁的情况:

  1. 函数执行完成,当前形成的栈内存中,某些内容被栈内存以外的变量占用了,此时栈内存不能释放(一旦释放外面找不到原有的内容了)
  2. 全局栈内存只有在页面关闭的时候才会被释放掉
var i = 1;
function fn(i){
    return function (n) {
        console.log(n + (++i));
    }
}
var f = fn(2)  // i = 2 
f(3)   // n=3  6
fn(5)(6) //i=5 n=6  12
fn(7)(8) //i=7 n=8   16
f(4) // i=3 n=4   8
复制代码
var k = 1;
console.log(5 + k++, k) //6  2
k = 1
console.log(5 + ++k, k) //7  2
复制代码

闭包作用之保护

函数执行形成一个私有的作用域,保护里面的私有变量不受外界的干扰,这种保护机制称之为“闭包”

市面上的开发者认为的闭包是:形成一个不销毁的私有作用域(私有栈内存)才是闭包

闭包:柯里化函数
function fn() {
    return function () {}
}
// 被f占有了,不能被销毁
var f = fn();
复制代码
闭包:惰性函数   立即执行函数
var utils = (function(){
    return {}
})()

复制代码

面试的时候可以写以上形式,都是形成不销毁内存

闭包项目实战应用:
真实项目中为了保证JS的性能(堆栈内存的性能优化),应该尽可能的减少闭包的使用(不销毁的堆栈内存是耗性能的)

  • 闭包具有“保护”作用:保护私有变量不受外界的干扰

在真实项目中,尤其是团队协作开发的时候,应当尽可能得减少全局变量的使用,以防止相互之间的冲突(“全局变量污染”),那么此时我们完全可以把自己这一部分内容封装到一个闭包中,让全局变量转换为私有变量

(function(){
    var n = 12;
    function fn(){console.log(n)}
    return fn
})()
复制代码

不仅如此,我们封装类库插件的时候,也会把自己的程序都存放到闭包中保护起来,防止和用户的程序冲突,但是我们又需要暴露一些方法给客户使用,这样我们如何处理呢?

  1. Jquery这种方式:把需要暴露的方法抛给全局
(function(){
    function jQuery(){c}
    window.jQuery = window.$ = jQuery; // 把需要供外面使用的方法,通过给win设置属性的方式暴露出去
})()
jQuery();
$();
复制代码
  1. Zepto这种凡是:基于return把需要供外面使用的方法暴露出来
var Zepto = (function(){
    return {
        xxx: function(){}
    }
})()
Zepto.xxx()
复制代码
  • 闭包具有“保存”作用:形成不销毁的栈内存,把一些值保存下来,方便后面得调取使用

闭包作用之保存

实例:tab切换

i不是私有的

for(var i=0, i<tabList.length;i++){
    tabList[i].onclick = function() {
        changeTab(i)//执行方法,形成一个私有的栈内存,遇到变量i,i不是私有变量,向上一级作用域查找(上级作用域是window),因为执行完了代码才会click,这时候全局i为3
    }
}

以上点击的时候会报错
复制代码

在老版本中,判断和循环不能形成作用域,只有全局作用域和函数中的私有作用域

所有的事件绑定都是异步编程,所以当我们点击执行方法的时候,循环早已结束(让全局的i等于循环最后的结果3)

作用域查找机制 和 异步编程解释以上代码报错

解决方案:自定义属性

for(var i=0, i<tabList.length;i++){
    tabList[i].myIndex = i
    //点击的时候执行的是小函数,自执行函数在给时间赋值的时候就已经执行
    tabList[i].onclick = function() {
        changeTab(this.myIndex)//this指向当前操作的元素对象
    }
}
复制代码

解决方案:闭包

for(var i=0, i<tabList.length;i++){
    tabList[i].onclick = (function(n) {
        var i = n;
        return function () {
          changeTab(i)  
        }
    })(i)
}
复制代码

总结:循环3次,形成3个不销毁的私有作用域(自执行函数执行),而每一个不销毁的栈内存中都存储了一个私有变量i,而这个值分别是每一次执行传递进来的全局i的值(也就是:第一个不销毁额作用域存储的是0,第二个是1,第三个是2);当点击的时候,执行返回的小函数,遇到变量i,向它自己的上级作用域查找,找到的i值分别是:0/1/2,达到了我们想要的效果;

耗性能,虽然能实现,最好不要这么写

每个循序形成自己的私有作用域
for(var i=0, i<tabList.length;i++){
    (function(n) {
        tabList[n].onclick = function() {
            changeTab(n)//this指向当前操作的元素对象
        }
    })(i)
}
复制代码

原理都是形成3个不销毁的私有作用域,分别存储需要的索引值

解决方案:基于es6

for(let i=0, i<tabList.length;i++){
    tabList[i].onclick = function() {
        changeTab(i)
    }
}
复制代码

js执行上下文与作用域

  1. var和function声明创建在全局对象中,而let const class声明的变量创建在全局scope中
  2. 先在全局scope中找变量,查找不到再在全局对象查找

顶级函数:不在大括号中

全局上下文

Step1::创建全局执行上下文,并加入栈顶
Step2:分析:

  • 找到所有的非函数中的var声明
  • 找到所有的顶级函数声明
  • 找到顶级let const class声明

Step3:

  • 名字重复处理

Step4: 创建绑定

  • 登录并初始化var为undefined
  • 顶级函数声明:登录function名字,并初始化为新创建函数对象
  • 块级中函数声明:登记名字,初始化为undefined
  • 登记let cosnt class,但未初始化

Step5:执行语句

函数对象体内会保存,函数创建时的执行上下文的文本环境。它对理解函数闭包和函数执行作用域很有帮助。

函数的调用是创建函数执行上下文。比如foo(),是创建foo执行上下文。

作用域

  • 作用域是解析(查找)变量名的一个集合,就是当前运行上下文(也可以是当前上下文的词法环境)

全局作用域就是全局运行上下文
函数作用域就是函数运行上下文

  • 函数调用时的执行上下文看“身世”——函数在哪里创建,就保存哪里的运行上下文

函数的作用域是在函数创建的时候绝对的而不是调用的时候决定

词法作用域

  • 并非根据调用嵌套形成(运行上下文)作用域,而是根据函数创建嵌套形成作用域链,也就是函数的书写位置形成作用域链,因此称为词法作用域。
function foo(){
    console.log(a) // 2
}
function bar(){
    var a = 3;
    foo();
}
var a = 2;
bar()
复制代码

块级作用域

let a = 'out if statement'
if(true){
    let a = 'in if statement'
    console.log(a)
}
console.log(a)
复制代码

总结关键点

  • 在当前作用域下,不管条件是否成立成立都要进行变量提升
  • 带var 和 function 关键字声明相同的名字,这种也算是重名了(其实是一个fn,只是存储值得类型不一样)
  • 关于重名的处理:如果名字重复了,不会重新的声明,但是会重新的定义(重新赋值)【不管是变量提升还是代码执行阶段都是如此】
  • 暂存死区 typeof a为undefined 可以说明
  • 堆内存:存储引用数据类型值(对象:键值对 函数:代码字符串)
  • 栈内存:提供JS代码执行的环境和存储基本类型值
  • 当前函数执行,形参是私有作用域A,A的上级作用域是谁,和它在哪执行的没有关系,和它在哪创建有关系,在哪里创建的,它的上级作用域就是谁
  • 闭包是形成一个不销毁的私有作用域(私有栈内存)
  • 闭包有保存和保护作用
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享