所謂的變量提升,是指在 JavaScript 代碼執行過程中,JavaScript 引擎把變量的聲明部分和函數的聲明部分提升到代碼開頭的“行為”。變量被提升後,會給變量設定預設值,這個預設值就是我們熟悉的 undefined。
實際上變量和函數聲明在代碼裡的位置是不會改變的,而且是在編譯階段被 JavaScript 引擎放入記憶體中。
首先是編譯階段。遇到了第一個 showName 函數,會将該函數體存放到變量環境中。接下來是第二個 showName 函數,繼續存放至變量環境中,但是變量環境中已經存在一個 showName 函數了,此時,第二個 showName 函數會将第一個 showName 函數覆寫掉。這樣變量環境中就隻存在第二個 showName 函數了。接下來是執行階段。先執行第一個 showName 函數,但由于是從變量環境中查找 showName 函數,而變量環境中隻儲存了第二個 showName 函數,是以最終調用的是第二個函數,列印的内容是“極客時間”。第二次執行 showName 函數也是走同樣的流程,是以輸出的結果也是“極客時間”。綜上所述,一段代碼如果定義了兩個相同名字的函數,那麼最終生效的是最後一個函數。總結好了,今天就到這裡,下面我來簡單總結下今天的主要内容:JavaScript 代碼執行過程中,需要先做變量提升,而之是以需要實作變量提升,是因為 JavaScript 代碼在執行之前需要先編譯。在編譯階段,變量和函數會被存放到變量環境中,變量的預設值會被設定為 undefined;在代碼執行階段,JavaScript 引擎會從變量環境中去查找自定義的變量和函數。如果在編譯階段,存在兩個相同的函數,那麼最終存放在變量環境中的是最後定義的那個,這是因為後定義的會覆寫掉之前定義的。以上就是今天所講的主要内容,當然,學習這些内容并不是讓你掌握一些 JavaScript 小技巧,其主要目的是讓你清楚 JavaScript 的執行機制:先編譯,再執行。如果你了解了 JavaScript 執行流程,那麼在編寫代碼時,你就能避開一些陷阱;在分析代碼過程中,也能通過分析 JavaScript 的執行過程來定位問題。
作用域是指在程式中定義變量的區域,該位置決定了變量的生命周期。通俗地了解,作用域就是變量與函數的可通路範圍,即作用域控制着變量和函數的可見性和生命周期。在 ES6 之前,ES 的作用域隻有兩種:全局作用域和函數作用域。全局作用域中的對象在代碼中的任何地方都能通路,其生命周期伴随着頁面的生命周期。函數作用域就是在函數内部定義的變量或者函數,并且定義的變量或者函數隻能在函數内部被通路。函數執行結束之後,函數内部定義的變量會被銷毀。在 ES6 之前,JavaScript 隻支援這兩種作用域,相較而言,其他語言則都普遍支援塊級作用域。塊級作用域就是使用一對大括号包裹的一段代碼,比如函數、判斷語句、循環語句,甚至單獨的一個{}都可以被看作是一個塊級作用域。
在 JavaScript 中,根據詞法作用域的規則,内部函數總是可以通路其外部函數中聲明的變量,當通過調用一個外部函數傳回一個内部函數後,即使該外部函數已經執行結束了,但是内部函數引用外部函數的變量依然儲存在記憶體中,我們就把這些變量的集合稱為閉包。比如外部函數是 foo,那麼這些變量的集合就稱為 foo 函數的閉包。
對象的屬性值有三種類型:
原始類型 (primitive),所謂的原始類的資料,是指值本身無法被改變
JavaScript 中的原始值主要包括 null、undefined、boolean、number、string、bigint、symbol
對象類型 (Object),對象的屬性值也可以是另外一個對象,比如上圖中的 info 屬性值就是一個對象。
函數類型 (Function),如果對象中的屬性值是函數,那麼我們把這個屬性稱為方法,是以我們又說對象具備屬性和方法
在 JavaScript 中,函數是一種特殊的對象,它和對象一樣可以擁有屬性和值,但是函數和普通對象不同的是,函數可以被調用。
函數除了可以擁有常用類型的屬性值之外,還擁有兩個隐藏屬性,分别是 name 屬性和 code 屬性。
隐藏 name 屬性的值就是函數名稱,如果某個函數沒有設定函數名
該函數對象的預設的 name 屬性值就是 anonymous,表示該函數對象沒有被設定名稱。
code 屬性,其值表示函數代碼,以字元串的形式存儲在記憶體中
JavaScript 的每個對象都包含了一個隐藏屬性 __proto__ ,我們就把該隐藏屬性 __proto__ 稱之為該對象的原型 (prototype),__proto__ 指向了記憶體中的另外一個對象,我們就把 __proto__ 指向的對象稱為該對象的原型對象,那麼該對象就可以直接通路其原型對象的方法或者屬性。
繼承就是一個對象可以通路另外一個對象中的屬性和方法,在JavaScript 中,我們通過原型和原型鍊的方式來實作了繼承特性。
全局作用域和函數作用域類似,也是存放變量和函數的地方,但是它們還是有點不一樣: 全局作用域是在 V8 啟動過程中就建立了,且一直儲存在記憶體中不會被銷毀的,直至 V8 退出。 而函數作用域是在執行該函數時建立的,當函數執行結束之後,函數作用域就随之被銷毀掉了。
全局作用域中包含了很多全局變量,比如全局的 this 值,如果是浏覽器,全局作用域中還有 window、document、opener 等非常多的方法和對象,如果是 node 環境,那麼會有 Global、File 等内容。
構造資料存儲空間:堆空間和棧空間由于 V8 是寄生在浏覽器或者 Node.js 這些宿主中的,是以,V8 也是被這些宿主啟動的。比如,在 Chrome 中,隻要打開一個渲染程序,渲染程序便會初始化 V8,同時初始化堆空間和棧空間。棧空間主要是用來管理 JavaScript 函數調用的,棧是記憶體中連續的一塊空間,同時棧結構是“先進後出”的政策。在函數調用過程中,涉及到上下文相關的内容都會存放在棧上,比如原生類型、引用到的對象的位址、函數的執行狀态、this 值等都會存在在棧上。當一個函數執行結束,那麼該函數的執行上下文便會被銷毀掉。棧空間的最大的特點是空間連續,是以在棧中每個元素的位址都是固定的,是以棧空間的查找效率非常高,但是通常在記憶體中,很難配置設定到一塊很大的連續空間,是以,V8 對棧空間的大小做了限制,如果函數調用層過深,那麼 V8 就有可能抛出棧溢出的錯誤。
堆空間是一種樹形的存儲結構,用來存儲對象類型的離散的資料,在前面的課程中我們也講過,JavaScript 中除了原生類型的資料,其他的都是對象類型,諸如函數、數組,在浏覽器中還有 window 對象、document 對象等,這些都是存在堆空間的。
惰性解析。所謂惰性解析是指解析器在解析的過程中,如果遇到函數聲明,那麼會跳過函數内部的代碼,并不會為其生成 AST 和位元組碼,而僅僅生成頂層代碼的 AST 和位元組碼。
JavaScript 閉包相關的三個重要特性:可以在 JavaScript 函數内部定義新的函數;内部函數中通路父函數中定義的變量;因為 JavaScript 中的函數是一等公民,是以函數可以作為另外一個函數的傳回值。
因為在上述的解析的過程中,如果碰到了script或者link标簽,就會根據src對應的位址去加載資源,在script标簽沒有設定async/defer屬性時,這個加載過程是下載下傳并執行完全部的代碼,此時,DOM樹還沒有完全建立完畢,這個時候如果js企圖通路script标簽後面的DOM元素,浏覽器就會抛出找不到該DOM元素的錯誤。
值得注意的是:從bytes到Tokens的這個過程,浏覽器都可以交給其他單獨的線程去處理,不會堵塞浏覽器的渲染線程。但是後面的部分就都在渲染線程下進行了,也就是我們常說的js單線程環境。
在标簽沒有設定async/defer屬性時,js會阻塞DOM的生成。原因是js會改變DOMTree的内容,如果不阻塞,會出現一邊生成DOM内容,一邊修改DOM内容的情況,無法確定最終生成的DOMTree是确定唯一的。
同理,JS也會可以修改CSS樣式,影響CSSOMTree最終的結果。而我們前面提到,不完整的CSSOMTree是不可以被使用的,如果JS試圖在浏覽器還未完成CSSOMTree的下載下傳和建構時去操作CSS樣式,浏覽器會暫停腳本的運作和DOM的建構,直至浏覽器完成了CSSOM的下載下傳和建構。也就是說,JS腳本的出現會讓CSSOM的建構阻塞DOM的建構。