堆栈内存及闭包详解

通过自己的总结和学习,记录的一点笔记

声明变量

  1. 先声明一个变量 a,没有赋值
  2. 在当前作用域开辟一个位置存储 12
  3. 让变量 a12 关联在一起(定义:赋值)
  4. 基础值类型就是直接操作值,和变量没有关系
  5. 引用类型是按照引用的空间地址进行操作,地址都是一个。
  6. 函数也是引用类型
    • 栈内存:作用域(代码都是在栈中执行的) 1. 提供一个提供 js 代码自上而下执行的环境 2. 由于基本数据类型值比较简单,他们都是直接在栈内存中开辟一个位置,把值直接存储进去的 3. 栈内存被销毁,存储的基本值也销毁
    • 堆内存:引用值对应的空间 1. 存储引用类型值的(对象:键值对 函数:代码字符串) 2. 当前堆内存销毁,那么引用消失 3. 堆内存没有被任何变量或者其他东西占用,就会被浏览器在空闲的时候,自主的进行内存回收,把所有不被占用的堆内存销毁 4. 销毁堆:XX=null

变量提升

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

    • 声明(declare):var a/function sum (默认值 undefined)
    • 定义(defined):a=12 (定义其实就是赋值)
    • 变量提升阶段:带var的只声明未定义,带funtion的声明和赋值都完成了
  2. 变量提升只发生在当前作用域(例如:开始加载页面的时候只对全局作用域下的进行提升,因为 此时函数中存储的都是字符串而已)

  3. 在全局作用域下声明的函数或者变量是“全局变量”,同理,在私有作用域下声明的变量是“私有变 量”(带var/function的才是声明)
  4. 浏览器很懒,做过的事情不会重复执行第二遍,也就是,当代码执行遇到创建函数这部分代码后,直接的跳过即可(因为在提升阶段就已经完成函数的赋值操作了)
  5. 私有作用域形成后,也不是立即执行代码,而是先进行变量提升(变量提升钱,先形参赋值)
  6. es3/es5语法规范中,只有全局作用域和函数执行的私有作用域(栈内存),其他大括号不会形成栈内存

带 var 和不带 var 的区别

  1. 在全局作用域下声明一个变量,也相当于给 window 全局对象设置了一个属性,变量的值就是属性值(私用作用域中声明的私有变量和 window没啥关系)
  2. 在变量提升阶段,在全局作用域中声明了一个变量a,此时就已经把a当做属性赋值给 window 了,只不过此时还没有给 a 赋值,默认 undefined
  3. in操作符可以检测某个属性是否属于这个对象
  4. 全局变量和 window 中的属性存在“映射机制”
  5. a=12=>window.a=12 不加 var 的本质是 window 的属性。var a=b=12 这样写 b 是不带 var
  6. 私有作用域中带 var 和不带也有区别
    1. var 的在私有作用域变量提升阶段,都声明为私有变量,和外界没有任何的关系
    2. 不带 var 不是私有变量,会向它的上级作用域查找,一直找到 window 为止(我们把这种查找机制叫做:‘作用域链’),也就是我们在私有作用域中操作的这个非私有变量,是一直操作别人的。

### 作用域链的扩展

  1. 在作用域链查找的过程中,如果找到 window 也没有这个变量,相当于给 window 设置一个属性

变量提升的一些细节问题(关于条件判断下的处理)

  1. 匿名函数之函数表达式var fn = function () {}
  2. var fn只对左边进行变量提升
  3. (条件判断下)在当前作用域下,不管条件是否成立都要进行变量提升,带 var 的还是只声明,带 function 的在老版本浏览器渲染机制之下,声明加定义都处理了,但是为了迎合 es6 中的块级作用域, 新版浏览器对于函数(在条件判断中的函数),不管条件是否成立,都只是先声明,没有定义,类似 var
    4.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 没有变量提升
f=function(){return true};
g=function(){return false};
~function(){
// 变量提升:funtion g; g是私有变量
if(g()&&[]==![]){
// 新浏览器Uncaught TypeError: g is not a function (此时的g是undefined)
// 老浏览器不会
// []==![]是true
f=function(){return false} // 把全局中的f进行修改 false
function g(){return true}
}
}
// 老版本浏览器
console.log(f()); // false
console.log(g()); // false

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

1
2
3
4
5
6
7
8
9
// 变量提升:function fn;
console.log(fn); =>undefined
if (1===1){
console.log(fn);=>函数本身:当条件成立,进入到判断体中(在es6中它是一个块级作用域)第一件事并不是代码执行,而是类似于变量提升一样,先把fn声明和定义了,也就是判断体中代码执行之前,fn就已经赋值了
function fn() {
console.log('ok')
}
}
console.log(fn);=>输出函数本身

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

  1. varfunction 关键字声明相同的名字,这种也算是重名了(其实是一个变量,只是存储值的类型不一样)
  2. 关于重名的处理:如果名字重复了,不会重新的声明,但是会重新的定义(重新赋值)<不管是变量的提升还是代码执行阶段皆是如此>
  3. 代码有报错,下面的代码不在执行

ES6 中的 let 不存在变量提升

  1. es6 中基于 let/const 等方式创建变量或者函数,不存在变量提升机制,切断了全局变量和 window 属性的映射机制
  2. 在相同的作用域中,基于 let 不能声明相同名字的变量(不管用什么方式在当前作用域下声明了变量,再次使用 let 创建都会报错)
  3. 虽然没有变量提升机制,但是在当前作用域代码自上而下执行之前,浏览器会做一个重复性检测(语法检测):自上而下查找当前作用域下所有变量,一旦发现有重复的,直接抛出异常,代码也不会再执行了(虽然没有把变量提前升声明定义,但是浏览器已经记住了,当前作用域下有哪些变量)

暂时性死区

  1. 基于 let 创建变量,会把大部分{}当做一个私有的块级作用域(类似于函数的私有作用域),在这里也是重新检测语法规范,看一下是否基于新语法规范创建的变量,如果是则按照新语法规范来解析
  2. 在原有浏览器渲染机制下,基于 typeof 等逻辑运算符检测一个未被声明过的变量,不会报错,返回 undefined
  3. 如果当前变量是基于 es6 语法处理,在没有声明这个变量的时候,使用 typeof 检测会直接报错,不会 undefined 解决了原有的 js 的暂时性死区

区分私有变量和全局变量

  1. 在私有作用域中,只有以下两种情况是私有变量:A.声明过的变量(带 var/function)B.形参也是私有变量; 剩下的都不是自己私有的变量,都需要基于作用域链的机制向上查找

上级作用域的查找

  1. 当前函数执行,形成一个私有作用域 A,A 的上级作用域是谁,和他在哪执行的没有关系,和他在哪创建(定义)的有关系,在哪创建的,他的上级作用域就是谁
  2. arguments 是函数实参集合,arguments.callee 是函数本身,arguments.callee.caller 是当前函数在哪执行的,caller 就是谁(记录的是他执行的宿主环境),在全局下执行 caller 的结果是 null

闭包及堆栈内存释放

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

  2. 堆内存释放:让所有引用堆内存空间地址的变量赋值为 null 即可(没有变量占用这个堆内存了,浏览器会在空闲的时候把它释放掉)
    栈内存释放: 一般情况下,当函数执行完成,所形成的私有作用域(栈内存)都会自动释放掉(在栈内存中存储的值也会释放掉),但是也有特殊不销毁的情况:

    • 函数执行完成,当前形成的栈内存中,某些内容被栈内存以外的变量占用了,此时栈内存不能释放(一旦释放外面找不到原有的内容了)
    • 全局栈内只有在页面关闭的时候才会被释放掉,如果当时栈内存没有被释放,那么之前在栈内存中存储的基本值也不会被释放,能够一直保存下来。
  3. var f = fn(2) =>先把 fn 执行(传递实参 2),把 fn 执行的返回结果(return 后面的值)赋值给 f,f()=>把返回的结果执行。fn(2)()=>和上面两步骤类似,都是先把 fn 执行,在 fn 执行的返回结果再次执行

  4. i++ 自身累加 1: 先运算在累加
    ++i 自身累加 1: 先累加在运算

闭包的应用

  1. 闭包:
  • 函数执行形成一个私有的作用域,保护里面的私有变量不受外界的干扰,这种保护机制称之为闭包
  • 市面上的开发者认为的闭包:形成一个不销毁的私有作用域(私有栈内存)才是闭包
  • 闭包:柯理化函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function fn() {
return function (){

}
}
var f = fn
```

- 闭包:惰性函数

```
var utils = (function () {

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

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

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

      }
      })()

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

    • jq 这种方式:把需要暴露的方法抛到全局
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    (function(){
    function jQuery() {
    ...
    }
    ...
    把需要提供外面使用的方法,通过给win设置属性的方式暴露出去
    window.jQuery = window.$ = jQuery;
    })()
    jQuery();
    $()
    • Zepto 这种方式:基于 return 把需要供外面使用的方法暴露出去
    1
    2
    3
    4
    5
    6
    7
    8
    9
    var Zepto = (function(){
    ...
    return {
    xxx: function(){

    }
    }
    })();
    Zepto.xxx()
  • 闭包具有保存作用:形成不销毁的栈内存,把一些值保存下来,方便后面的调取使用

  1. 闭包作用之保存
  • 循环和判断都不会形成私有作用域(栈内存),for 循环执行,形成一个私有的栈内存,遇到变量 ii 不是私有变量,向上一级作用域查找(上级作用域是 win)
  • 所有的事件绑定都是异步编程(同步编程:一件事一件事的做,当前这件事没有完成,下一个任务不能处理/异步编程:当前这件事件没有彻底完成,不再等待,继续执行下面的任务),绑定事件后,不需要等待执行,继续执行下一个循环任务,所以当我们点击执行方法的时候,循环早已结束(让全局的等于循环最后的结果)
  • 解决方案:

    • 自定义属性
    1
    2
    3
    4
    5
    6
    7
    for (var i = 0,i<tabList.length;i++){
    tabList[i].myIndex = i;
    tabList[i].onclick = function(){
    changeTab(this.myIndex)
    }
    // this:给当前元素的某个时间绑定方法,当事件触发,方法执行的时候,方法中的this是当前操作的元素对象
    }
    • 闭包解决
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    for (var i = 0,i<tabList.length;i++){
    tabList[i].onclick = (function(n){
    // 让自执行函数执行,把执行的返回值(return)赋值给此处on-click(on-click绑定的是返回的小函数,点击的时候执行的是小函数),自执行函数在给事件赋值的时候就已经执行了
    var i=n;
    return function () {
    changeTab(i); // 上级作用域:自执行函数形成的作用域
    }
    })(i)
    }

    // 总结:循环几次,形成几个不销毁的私有作用域(自执行函数执行),而每一个不销毁的栈内存中都存储了一个私有变量,而这个值分别是每一次执行传递进来的全局i的值,当点击的时候,执行返回的小函数,遇到变量i,向它自己的上级作用域查找,找到i的值
    • 基于 ES6 解决
    1
    2
    3
    4
    5
    6
    7
    8
    9
    for (let i = 0,i<tabList.length;i++){
    // 循环体也是块级作用域,初始值设置的变量是当前本次块级作用域中的变量(形成了i个块级作用域,每个块级作用域中都有一个私有变量i,变量值就是每一次循环的i)
    tabList[i].onclick = function(){
    changeTab(i)
    }
    }
    // 基于es6中的let来创建变量,是存在块级作用域的(类似于私有作用域)
    // 作用域(栈内存)1. 全局作用域 2.私有作用域 3.块级作用域(一般用大括号包起来的都是块级作用域,前提是es6语法规范)
    // 对象的{}不是块级作用域
-------------本文结束感谢您的阅读-------------

本文标题:堆栈内存及闭包详解

文章作者:Water

发布时间:2019年03月13日 - 14:03

最后更新:2023年08月01日 - 06:08

原始链接:https://water.buging.cn/2019/03/13/堆栈内存及闭包详解/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

坚持原创技术分享,您的支持将鼓励我继续创作!