天天看點

兄台:JS閉包了解一下簡明扼要文章概要函數即對象閉包

簡明扼要

  1. JS是一門基于對象 (

    Object-Based

    ) 的語言
  2. 對象是由資料、方法以及關聯原型三個組成部分
  3. 函數是一種特殊的對象
  4. 函數是一等公民(

    First-class Function

    )
  5. 根據「詞法作用域」的規則,内部函數引用外部函數的變量被儲存到記憶體中,而這些「變量的集合」被稱為閉包
  6. 閉包和詞法環境的「強相關」
  7. 閉包在每次建立函數時建立(閉包在JS編譯階段被建立)
  8. 産生閉包的核心兩步: 1. 「預掃描」内部函數 2. 把内部函數引用的外部變量儲存到堆中
  9. 每個閉包都有三個作用域:

    1. Local Scope (Own scope)

    2. Outer Functions Scope

    3. Global Scope

文章概要

  1. 函數即對象
  2. 閉包

函數即對象

根據MDN描述JS特性的時候。提到

❝JavaScript is designed on a simple object-based paradigm

JS是一門基于對象 (

Object-Based

) 的語言(也就是我們總說的JS是object-oriented programming [OOP]語言 )

JavaScript 中每個對象就是由一組組屬性和值構成的集合。

var person=new Object();
person.firstname="John";
person.lastname="Doe";
person.age=50;
person.eyecolor="blue";
           

複制

兄台:JS閉包了解一下簡明扼要文章概要函數即對象閉包

同時, 在 JS 中,對象的值可以是「任意類型」的資料。(在JS篇之資料類型那些事兒簡單的介紹了下基本資料類型分類和判斷資料類型的幾種方式和原理,想了解具體細節,可移步指定文檔)

在OOP的程式設計方式中,有一個心智模式需要了解

❝對象是由資料、方法以及關聯原型三個組成部分

資料就是屬性值為非函數類型(表示對象的資料屬性),方法就是屬性值為函數類型(表示對象的行為屬性),而關聯原型涉及到對象的繼承。(這個我們後續會有相關介紹)。

函數的本質

在JS中,一切皆對象。那從語言的設計層面來講,

❝函數是一種特殊的對象

它和對象一樣可以擁有屬性和值。

function foo(){
    var test = 1
    return test;
}
foo.myName = 1
foo.obj = { x: 1 }
foo.fun = function(){
  return 0;
}
           

複制

根據對象的資料特性:

foo

函數擁有

myName

/

obj

/

fun

的屬性

但是函數和普通對象不同的是,「函數可以被調用」。

我們從V8内部來看看函數是如何實作可調用特性。

在 V8 内部,會為函數對象添加了兩個隐藏屬性

  1. name 屬性
  2. code 屬性
兄台:JS閉包了解一下簡明扼要文章概要函數即對象閉包

name屬性

屬性的值就是函數名稱。

function test(){
  let name = '789';
  console.log(name);
}
           

複制

如果某個函數沒有設定函數名, 該函數對象的預設的 name 屬性值就是

""

。表示該函數對象沒有被設定名稱。

(function (){
    var test = 1
    console.log(test)
})()
           

複制

code屬性

code值表示「函數代碼」,以字元串的形式存儲在記憶體中。

當執行到,一個「函數調用」語句時,V8 便會從函數對象中取出 code 屬性值(也就是函數代碼),然後再解釋執行這段函數代碼。

在解釋執行函數代碼的時候,又會生成該函數對應的執行上下文,并被推入到調用棧裡。

驗證

我們通過Chrome_devTool中的工具來驗證剛才的論證。(我是用Chromium:95版本)

Sources新增Snippets

兄台:JS閉包了解一下簡明扼要文章概要函數即對象閉包

最後不要忘記點選

Enter

執行代碼。

function Parent(){

}

let c1 = new Parent();

c1.fn = function fn_name_789(){
  console.log('789')
}

c1.fn2 = function(){
  console.log('匿名函數')
}
           

複制

Memory查詢記憶體快照

将開發者工具切換到 Memory 标簽,然後點選左側的小圓圈就可以捕獲目前的「記憶體快照」

兄台:JS閉包了解一下簡明扼要文章概要函數即對象閉包

搜尋

Parent

,在Parent的執行個體

c1

,可見存在兩個方法屬性(fn/fn2),處理該對象的隐藏類的

map

屬性(後面我們會有文章介紹)還有繼承相關的

__proto__

fn

是一個方法屬性,也就是指向了函數對象。而通過上文得知,函數對象中包含可調用特性的屬性。從圖中可知,

code

表示函數代碼(并且還是延遲編譯的), 上文的

name

存放在

shared

對象中。

兄台:JS閉包了解一下簡明扼要文章概要函數即對象閉包

關于CPU如何執行程式的簡單介紹,可以參考CPU如何執行程式。

關于執行上下文的相關介紹,可以參考兄台: 作用域、執行上下文了解一下

針對JS的點,還有一點需要強調一下

❝函數是一等公民(First-class Function):函數可以和其他的資料類型做一樣的事情

1. 被當作參數傳遞給其他函數

2. 可以作為另一個函數的傳回值

3. 可以被指派給一個變量

閉包

❝在 JS 中,根據「詞法作用域」的規則,内部函數總是可以通路其外部函數中聲明的變量。當通過調用一個外部函數「傳回」一個内部函數後,即使該外部函數已經執行結束了。但是「内部函數引用外部函數的變量依然儲存在記憶體中」,就把這些變量的集合稱為閉包。

function test() {
    var myName = "fn_outer"
    let age = 78;
    var innerObj = {
        getName:function(){
            console.log(age);
            return myName
        },
        setName:function(newName){
            myName = newName
        }
    }
    return innerObj
}
var t = test();
console.log(t.getName());//fn_outer 
t.setName("global")
console.log(t.getName())//global
           

複制

根據詞法作用域的規則,内部函數

getName

setName

總是可以通路它們的外部函數

test

中的變量。

兄台:JS閉包了解一下簡明扼要文章概要函數即對象閉包

test

函數執行完成之後,其執行上下文從棧頂彈出了 但是由于傳回的

setName

getName

方法中使用了

test

函數内部的變量

myName

age

是以這兩個變量依然儲存在記憶體中(

Closure (test)

兄台:JS閉包了解一下簡明扼要文章概要函數即對象閉包

當執行到

t.setName

方法的時,調用棧如下:

兄台:JS閉包了解一下簡明扼要文章概要函數即對象閉包

利用

debugger

來檢視對應的作用鍊和調用棧資訊。

兄台:JS閉包了解一下簡明扼要文章概要函數即對象閉包

通過上面分析,然後參考作用域的概念和使用方式,我們可以做一個簡單的結論

❝閉包和詞法環境的「強相關」

我們再從V8編譯JS的角度分析,執行JS代碼核心流程 1. 先編譯 2. 後執行。而通過分析得知,閉包和詞法環境在某種程度上可以認為是強相關的。而JS的作用域由詞法環境決定,并且作用域是「靜态」的。

是以,我們可以得出一個結論:

❝閉包在每次建立函數時建立(閉包在JS編譯階段被建立)

閉包是如何産生的?

閉包是什麼,我們知道了,現在我們再從V8角度談一下,閉包是咋産生的。

先上結論:

❝産生閉包的核心兩步:

1.「預掃描」内部函數

2. 把内部函數引用的外部變量儲存到堆中

function test() {
    var myName = "fn_outer"
    let age = 78;
    var innerObj = {
        getName:function(){
            console.log(age);
            return myName
        },
        setName:function(newName){
            myName = newName
        }
    }
    return innerObj
}
var t = test();
           

複制

我們,還是那這個例子來講。

當 V8 執行到

test

函數時,首先會編譯,并建立一個空執行上下文。在編譯過程中,遇到内部函數

setName

, V8還要對内部函數做一次「快速的詞法掃描」(預掃描) 發現該内部函數引用了 test 函數中的

myName

變量。由于是内部函數引用了外部函數的變量,是以 V8 判斷這是一個閉包。于是在堆空間建立換一個

closure(test)

的對象 (這是一個内部對象,JavaScript 是無法通路的),用來儲存 myName 變量。

test

函數執行結束之後,傳回的

getName

setName

方法都引用“clourse(test)”對象。

即使

test

函數退出了,“clourse(test)”依然被其内部的

getName

setName

方法引用。

是以在下次調用t.setName或者t.getName時,在進行「變量查找」時候,根據作用域鍊來查找。

這裡再多說一句:

❝每個閉包都有三個作用域:

1. Local Scope (Own scope)

2. Outer Functions Scope

3. Global Scope

// global scope
var e = 10;
function sum(a){
  return function(b){
    return function(c){
      // outer functions scope
      return function(d){
        // local scope
        return a + b + c + d + e;
      }
    }
  }
}

console.log(sum(1)(2)(3)(4)); // log 20           

複制