进阶教程 3. JS 的堆栈内存管理/let&const

653 阅读5分钟

这是我参与8月更文挑战的第11天,活动详情查看:8月更文挑战

一、javascript中的堆栈内存及释放

1.1 js中的堆内存的创建和释放

JS引擎中的内存分类:

  • 栈内存(作用域)供js代码执行的环境,保存基本数据类型
  • 堆内存:存储引用数据类型

1.2 堆内存的创建和销毁

1.2.1 堆创建

创建一个对象、数组、函数、等引用数据类型,浏览器都会分配一块堆内存地址,存储引用数据类型的数据;

var obj = {
   name: '江外琉璃',
   age: 10,
   courses: ['高级前端是怎样炼成的', '前端FE师是怎样炼成的', 'UI设计师是怎样炼成的']
}; // 每创建一个引用数据类型,浏览器都会开辟一个堆内存,并把这个堆内存地址给obj变量
// 此时obj相当于引用了这一块堆内存,
var obj2 = obj; // 此时obj2也引用了这一块堆内存

1.2.2 堆内存释放

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

obj = null; // 将obj指向空对象指针null
obj2 = null; // 将obj2指向空对象指针null

1.3 js中的栈内存开辟和释放

  • 栈内存(作用域):供js代码执行的环境,也是用来保存基本数据类型的

1.3.1 栈内存的创建:

  • 当浏览器打开时,首先会开辟形成一个顶层的栈内存,就是全局作用域;
  • 函数执行时,也会开辟一个供函数代码执行的栈内存(私有作用域)
function add(a, b) {
   var total = 0;
   total = a + b;
   return total;
}

函数每一次执行,都会形成一个全新的栈内存,即每次函数执行都是在一个全新的环境里面执行,所以函数每次执行都是互相独立的;

add(1, 2);
add(1, 3);
add(1, 2);

function fe() {
   return {
      q1: 'q1'
   }
}
var o1 = fe(); //
var o2 = fe();
console.log(o1 === o2); // false fe执行几次,救护创建几个对象,所以o1和o2是两个不同的堆内存,没有任何关系。

1.3.2 栈内存销毁:

  • 全局栈内存:当页面关闭时才会销毁
  • 函数的私有作用域:一般函数执行完成后,栈内存自动销毁。但是有一些特殊情况需要注意;

1.4 栈内存不销毁的情形

  • 函数栈内存:正常情况下,函数执行会形成一个栈内存(作用域),当函数执行完成就会自动销毁。

  • 但是函数执行完成后,当前形成的栈内存中,某些内容被栈内存以外的变量占用了,此时栈内存不能释放(一旦释放外面找不到原有的内容了)。栈内存不销毁,保存在栈内存中的数据也不会被销毁

1.4.1 函数返回值被占用

function fe() {
   return {
      name: "江外琉璃"
   }
}
var obj = fe(); // 函数fe执行形成一个不销毁的栈内存,里面定义的对象被外部obj变量占用,因而作用域不销毁

fe(); // 他们正常形成栈内存,但是执行后会被销毁

1.4.2 函数内部的引用数据类型被外部占用,函数执行的作用域不销毁。

var x = null;

function fn() {
   x = {
      name: 'q1'
   }
};
fn(); // 此时x占用着fn的作用域中对象{name: 'q1'}

1.5 一个累加计数的例子

  • 需求:实现一个累加计数的功能,点击一次,让按钮中的数字累加;
var btn = document.getElementById('btn');
  • 思路:既然是累加,一定是有一个地方保存着它的上一个值。关键就在于保存在哪里。

1.5.1 把值保存在全局作用域

var count = 0; // count保存在全局作用域
btn.onclick = function () {
   count++; // 操作全局变量
   btn.innerHTML = count;
};

1.5.2 自定义不销毁的私有作用域

btn.onclick = (function () {
   var count = 0; // count的值保存在
   return function () {
      btn.innerHTML = count;
   }
})();

1.5.3 自定义不销毁的函数作用域2

(function () {
   var count = 0; // count保存在自执行函数形成的私有作用域中
   btn.onclick = function () {
      count++;
      this.innerHTML = count;
   }
})();

1.5.4 块级作用域

let / const 会把花括号变成块级作用域,等效于私有作用域,count的值保存在块级作用域中

{
   let count = 0; // 
   btn.onclick = function () {
      count++;
      this.innerHTML = count;
   }
}

二、 let & const

let const 是ES6新增关键字,用于声明变量

  • let 用来声明普通变量
  • const 用于声明常量

2.1 let & const 和 var 的区别

2.1.1. let const不存在变量提升,对于使用let const声明的变量,只能在定义后使用

console.log(a); // 报错:a is not defined
let a = 0;

console.log(b); // 报错:b is not defined
const b = 12;

2.1.2 let const 重复声明

不能重复声明变量(重复声明var或者function声明的也不行)

var n = 1;
var n;
var n = 2;

let n = 1; // 报错:a has already been declared

2.1.3 let const 对 window 无影响

在全局声明的变量或者常量不会有类似 var window 上增加全局属性的操作;

var num = 100;
console.log('num' in window); // true in 运算符,检测对象是否有某个属性
let css = 'css3';
console.log('css' in window); // false

2.1.4. let const 暂时性死区;

在代码块中会出现,会形成块级作用域:

  • if (condition) { 条件的花括号中使用let const 就会形成块级作用域 }
let num1 = 2;
if (true) {
   let num1 = 4;
   console.log(num1); // 4
}
  • for(...) { for 循环中的代码块 }
for (var i = 0; i < 12; i++) {
   console.log('var 没有块级作用域')
}
console.log(i); // var 会把i泄露成全局变量

for (let j = 0; j < 3; j++) {
   console.log('let j j不会泄露出去')
}
console.log(j); // Uncaught ReferenceError: j is not defined
  • 利用 {}形成块级作用域
{
   let a = 1;
   console.log(a); // a是块级作用域中
}


{
   let m = 12; // m所处的块级作用域是下面的块级作用域的上级作用域,同样遵从作用域链的查找规则
   {
      console.log(m);
   }
}

暂时性死区(TDZ: temporary dead zone)

在代码块中,用let const声明的变量,不能再声明之前使用。

let q = 12;
if (true) {
   console.log(q); // 这个变量q已经被let锁定在块级作用域中,
   let q = 14;
}

2.2 const 声明变量的细节问题

2.2.1 const声明时必须赋值

let m;
var m;
const m; // 报错:Missing initializer in const declaration

2.2.2 值一旦被定义,不能被修改

const num = 12;
num = 15; 报错:

2.2.3 const 声明的引用类型

如果 const 声明的常量是一个引用数据类型,那么常量带表的引用地址不能改变,但是堆内存中的内容是可以修改的。

const ary = [1, 2, 3];
ary.push('33');
console.log(ary);

// ary = [1, 3, 5]; // 报错: