夯實基礎,徹底掌握js的核心技術(三):堆棧內存及閉包詳解

數據渲染機制及堆棧內存

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 />


棧內存:作用域

  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 />
  • 帶var和不帶的區別

在全局作用域下聲明一個變量,也相當於給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 />


作用域鏈的擴展:

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 />

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

<br />

<br />

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 />

<br />

4. 閉包之保護機制

閉包:<br />函數執行形成一個私有的作用域,保護裏面的私有變量不受外界的干擾,這種保護機制稱之爲“閉包”。<br />市面上的開發者認爲的閉包是:形成一個不銷燬的私有作用域(私有棧內存)纔是閉包。<br />

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

//閉包:惰性函數
var utils = (function(){
    return {
  }
})()

真實項目中爲保證js的性能(堆棧內存的性能優化),應該可能減少閉包的使用(不銷燬的堆棧內存是耗性能的)<br />閉包保護功能:

  1. 閉包具有“保護”作用:保護私有變量不受外界的干擾(在真實項目只能夠,尤其是團隊協作開發的時候,應該儘可能地減少全局變量的使用,防止相互之間的衝突(“全局變量污染”)),那麼此時我們完全可以把自己這一部分內容封裝到一個閉包中,讓全局變量轉換爲私有變量。
  • jQuery方式:把需要暴露的方法拋到全局
  • zepto這種方式:基於return把需要供外面使用的方法暴露出去
  1. 閉包具有“保護”作用:形成不銷燬的棧內存,把一些值保存下來,方便後面的調取使用

如果想了解更多,請掃描下面二維碼,關注公衆號<br />
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章