前端技术

夯实基础,彻底掌握js的核心技术(三):堆栈内存及闭包详解

2020-07-21  本文已影响0人  前端岚枫

数据渲染机制及堆栈内存

1. 数据值操作机制

/*
* 1. 先声明一个变量a,没有赋值(默认值谁undefined)
* 2. 在当前作用域中开辟一个位置存储12这个值
* 3. 让变量a和12关联在一起(定义:赋值)
*/
var a = 12
var b = a;
b = 13;
console.log(a);
var ary1 = [12, 23];
var ary2 = ary1;
ary2.push(100);
console.log(ary1)
function sum() {
    var total = null;
  for(var i = 0; i< arguments.length; i++;) {
    var item = arguments[i];
    item = parseFloat(item);
    !isNaN(item) ? total += item : null;
  }
  return total;
}
console.log(sum(12, 23, '34', 'AA'))

解析如下图:<br />

image.pngimage.png <br />栈内存:作用域
  1. 提供一个供js代码自上而下执行的环境(代码都是在栈中执行)
  2. 由于基本数据类型值比较简单,它们都是直接在栈内存中开辟一个位置,把值直接存储进去的
  3. 当栈内存被销毁,存储的那些基本值也都跟着被销毁了

堆内存:引用值对应的空间

  1. 存储引用类型值的(对象:键值堆 函数:代码字符串)
  2. 当前堆内存释放销毁,那么这个引用值彻底没了
  3. 堆内存的释放: 当堆内存没有被任何的变量或者其它东西所占用,浏览器会在空闲的时候,自主的进行内存回收,把所有不被占用的堆内存销毁掉(谷歌浏览器)(xxx= null,通过空对象指针null可以让原始变量(或者其它东西)谁都不指向,那么原有被占用的堆内存就没有被东西占用了,浏览器会销毁它)

2. 变量提升机制

变量提升: 当栈内存(作用域)形成,js代码自上而下执行之前,浏览器首先会把所有带va r/function关键词进行提前“声明”或者“定义”,这种预先处理机制称为“变量提升”<br />声明:(declare):var a/ function sum<br />定义:(defined)a= 12 (定义其实就是赋值操作)<br />在变量提升阶段:<br />1. 带“var”的只声明未定义 <br />2. 带“function”的声明和赋值都完成了

  1. 变量提升只发生在当前作用域
  2. 在全局作用域下声明函数或者变量是“全局变量”,同理,在私有作用域下声明的变量是“私有变量”(带var/function的才是声明)
  3. 浏览器很懒,做过的事情不会重复执行第二遍,也就是,当代码执行遇到创建函数这部分代码后,直接的跳过即可(因为在提升阶段就已经完成函数的赋值操作了)
var a = 12
var b = a;
b = 13;
console.log(a);
var ary1 = [12, 23];
var ary2 = ary1;
ary2.push(100);
console.log(ary1)
function sum() {
    var total = null;
  for(var i = 0; i< arguments.length; i++;) {
    var item = arguments[i];
    item = parseFloat(item);
    !isNaN(item) ? total += item : null;
  }
  return total;
}
console.log(sum(12, 23, '34', 'AA'))
变量提升机制解析:<br /> image.pngimage.png

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

console.log(a) // undefined
console.log(window.a) // undefined
console.log('a' in window) // true 在全局作用域中声明了一个变量a,此时就已经把a当成属性赋值给window了,只不过此时还没有给a
//赋值,默认值undifined  in:检测某个属性是否隶属于这个对象
var a = 12; // 全局变量值修改,window的属性值也跟着修改
console.log(window.a) // window的一个属性名a 12
a = 13
console.log(window.a) // 13
window.a = 14;
console.log(a) // 14

以上例子说明:全局变量和window中的属性存在“映射机制”<br />**<br />不加var的本质是window的属性<br />如下面例子:

console.log(a) // Uncaught ReferenceError: a is not defined
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 a = 12,
    b = 13; // 这样写b是带var的
var a = b = 12; // 这样写b是不带var的

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

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

解析如下图:<br />

image.pngimage.png <br />作用域链的扩展:
function fn() {
  
     b= 13;
  console.log('b' in window) // true,在作用链查找的过程中,如果找到window也没有这个变量,相当于给
                             // 给window设置了一个属性b(window.b = 13)
  console.log(b)
}
console.log(b)
/*
    * 变量提升:
  * var fn; 只对等号左边进行变量提升
  
**/
 
sum();
fn(); // Uncaught TypeError: fn is not a function

// 匿名函数之函数表达式
var fn = function () {
    console.log(1)
}

// 普通函数
function sum() {
    console.log(2)
}

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

  1. 带var的还只是声明
  2. 带function的在老版本浏览器渲染机制下,声明+定义,但是为了迎合ES6中的块级作用域,新浏览器对于函数(在条件判断中的函数)是否成立,都只是先声明,没有定义,类似var
  3. 在全局作用域下声明的全局变量也相当于给window设置了一个属性
/**
    * 在当前作用域下,不管条件是否成立都要进行变量提升
  * 带var的还只是声明
  * 带function的在老版本浏览器渲染机制下,声明+定义,但是为了迎合ES6中的块级作用域,新浏览器对于函数(在条件判断中的函数)
    是否成立,都只是先声明,没有定义,类似var
*/
console.log(a) // undefined
if(1 === 2) {
    var a = 12
}
console.log(a) //undefined
//在全局作用域下声明的全局变量也相当于给window设置了一个属性
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() && ([] == ![])) { // Uncaught TypeError: g is not a function [] == ![] => 0 == 0
    
    // 把全局中的f进行修改
    f= function () {
        return false
    };
    function g() {
        return true;
    }
  }
}()
console.log(f());
console.log(g());

/*
 * 全局下变量提升
 * function fn
*/

console.log(fn) // undefined
if(1 === 1) {
    console.log(fn)
  function fn() {
    console.log('ok')
  }
}
console.log(fn) //函数本身
  1. 带var 和function 关键字声明相同的名字,这种也算事重名了(其实是一个,只是存储的值类型不一样)
  2. 关于重名处理:如果名字重复了,不会重新的声明,但是会重新的定义(重新赋值)【不管是变量提升还是代码执行阶段都是如此】
fn(); // 4
function fn() {
    console.log(1);
}
fn(); // 4
function fn() {
    console.log(2);
}
fn(); // 4
var fn = 100 // 带var的在提升阶段只把声明处理了,赋值操作没处理,所在在代码执行的时候需要完成赋值100
fn(); // Uncaught TypeError: fn is not a function
function fn() {
    console.log(3);
}
fn();
function fn() {
    console.log(4);
}
fn();

3. ES6中let不存在变量提升

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

//-------------
let a = 10,
    b = 10
let fn =function () {
  // console.log(a, b)  //Uncaught ReferenceError: a is not defined
    let a = b = 20
  console.log(a, b) // 20, 20
}
fn();
console.log(a, b) // 10, 20

//-------------
let a = 12
console.log(a) 
let a = 13  // Uncaught SyntaxError: Identifier 'a' has already been declared
console.log(a)
  1. 基于let创建变量,会把大部分{}当作一个私有块级作用域(类似函数的私有作用域),在这里也是重新检测语法规范,看一下是否基于新语法创建的变量,如果是按照新语法规范来解析
  2. es6解决了浏览器的暂时性死区问题
var a = 12;
if(true) {
    console.log(a)
  let a = 13 
}

//--------
console.log(typeof a) // "undefined" 在原有浏览器渲染机制下,基于typeof等逻辑运算符检测一个未被声明的
                      // 变量不会报错,返回undefined

//--------
console.log(typeof a)  // Uncaught ReferenceError: a is not defined
let a //如果当前变量是基于es6语法处理,在没有声明这个变量的时候,使用typeof检测会直接报错,不会是undefined,解决了原有的js死区问题

闭包作用域(scope)

1. 区分私有变量和全局变量

在私有作用域中,只有以下两种情况是私有变量:

  1. 声明过的变量(带var/function)
  2. 行参也是私有变量

剩下的都不是自己私有变量,都需要基于作用域链的机制向上查找

var a = 12,
    b = 13,
    c = 14;
function fn(a) {
  /*
  * 行参赋值 a = 12
  * 变量提升: var b
  * 在私有作用域中,只有以下两种情况是私有变量
  * 1.声明过的变量(带var/function)
  * 2.行参也是私有变量
  * 剩下的都不是自己私有变量,都需要基于作用域链的机制向上查找
  **/
    console.log(a, b, c) // 12, undefined, 14
  var b = c = a = 20;  // var b = 20; c=20; 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) {
    console.log(ary) //[12, 23]
  ary[0] = 100;  //[100, 23]
  ary = [100] // [100]
  ary[0] = 0 // [0]
  console.log(ary) //[0]
}
fn(ary)
console.log(ary) //[100, 23]
解析如下图:<br /> image.pngimage.png

2. 查找上级作用域

  1. 当前函数执行,形成一个私有作用域A,A的上级作用域是谁,和它在哪执行的没有关系,和它在哪创建的有关系,在哪创建(定义)的,它的上级作用域就是谁
var a = 12
function fn() {
  // arguments:实参集合,arguments.callee:函数本身fn
    console.log(a)
}
function sum() {
  
    var a = 120
  fn(); // 12
}
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

image.pngimage.png <br /> image.pngimage.png <br /> image.pngimage.png

3. 闭包及堆栈内存释放

js中的内存分为堆内存和栈内存<br />堆内存:存储引用数据类型值(对象:键值对 函数: 代码字符串)<br />栈内存:提供js代码执行的环境和存储基本类型数据<br />【堆内存释放】<br />让所引用的堆内存空间地址的变量赋值为null即可(没有变量占用这个堆内存了,浏览器会在空闲的时候把它释放)<br />【栈内存释放】<br />一般情况下,当函数执行完成,所形成的私有作用域(栈内存)都会自动释放掉(在栈内存中存储的值也都会释放掉),但是也有特殊不销毁的情况:

  1. 函数执行完成,当前形成的栈内存中,某些内容被栈内存以外的变量占用了,此时栈内存不能释放(一旦释放,外面找不到原有的内容了)。
  2. 全局栈内存只有在页面关闭的时候才会被释放掉

如果当前栈内存没有释放,那么之前在栈内存中存储的基本值也不会释放,能够一直保存下来

var i = 1;
function fn(i) {
  return function (n) {
    console.log(n+ (++i));
  }
}
var f = fn(2); //先把fn执行(传递实参2),把fn执行的返回结果(return后面的值)赋值股f
f(3) // 把返回的结果执行 =》 6
fn(5)(6) //12 和上面两个步骤类似,都是把fn执行,都是先把fn执行,把fn执行的返回的结果再执行
fn(7)(8) // 16
f(4) //8

//---------
// 在和其它值进行运算的时候一些区别
// i++ ;自身累加1(先拿原有值进行运算,运算结束后,本身累加)
// i++; 自身累加1 (先自身累加1,拿累加后的结果进行运算)
var k = 1;
console.log(5+(k++), k) // 6, 2
console.log(5+ (++k), k) //7, 2


//--------

解析如下图:<br /> image.pngimage.png <br /> image.pngimage.png

4. 闭包之保护机制

闭包:<br />函数执行形成一个私有的作用域,保护里面的私有变量不受外界的干扰,这种保护机制称之为“闭包”。<br />市面上的开发者认为的闭包是:形成一个不销毁的私有作用域(私有栈内存)才是闭包。<br />

// 闭包:柯理化函数
function fn() {
return function(){
}}
var f = fn()

//闭包:惰性函数
var utils = (function(){
    return {
  }
})()

真实项目中为保证js的性能(堆栈内存的性能优化),应该可能减少闭包的使用(不销毁的堆栈内存是耗性能的)<br />闭包保护功能:

  1. 闭包具有“保护”作用:保护私有变量不受外界的干扰(在真实项目只能够,尤其是团队协作开发的时候,应该尽可能地减少全局变量的使用,防止相互之间的冲突(“全局变量污染”)),那么此时我们完全可以把自己这一部分内容封装到一个闭包中,让全局变量转换为私有变量。
  1. 闭包具有“保护”作用:形成不销毁的栈内存,把一些值保存下来,方便后面的调取使用
如果想了解更多,请扫描下面二维码,关注公众号<br /> qrcode_for_gh_4d3763fa9780_258 (1).jpgqrcode_for_gh_4d3763fa9780_258 (1).jpg
上一篇下一篇

猜你喜欢

热点阅读