JS高級入門教程

JS高級入門教程

目錄

  1. 本文章定位及介紹php

  2. JavaScript與ECMAScript的關係html

  3. DOM的本質及DOM級介紹前端

  4. JS代碼特性java

  5. 基本類型與引用類型node

  6. JS的垃圾回收機制程序員

  7. 做用域鏈介紹及其實現原理web

  8. 閉包面試

  9. this指針ajax

  10. 自執行函數的介紹及應用chrome

  11. 聲明提早

  12. JS線程問題

本培訓的定位及相關介紹

內容特色:

目標對象定位:

  • 主要面向對象:對於有一年左右工做經驗的前端工程師。提升JS的認識,突破JS學習瓶頸。

  • 對於沒有經驗的小夥伴,能夠經過本文章對JS有初步認識,瞭解JS的相關特性。

  • 除了系統性的JS內容,還會穿插介紹一些筆者在學習過程當中遇到的認識上的困惑與小問題。但願能減小小夥伴在學習過程當中遇到的障礙。

  • 對了,裏面有些內容是本人以爲有趣的,好玩的概念。可能除了能幫你們理清楚概念之外,並沒什麼卵用。但願你們不要見怪。

JavaScript與ECMAScript的關係

前言:

可能你們在閱讀JS相關書籍或瀏覽網上資料的時候會多多少少看過ECMAScript這個詞彙,它與JS有什麼關係呢?還有爲何JS最新的語法不是叫JS6而是叫ES6呢?在這裏給你們介紹一下JS與ES的關係。
ES6

解析:

  • 首先ECMAScript(簡稱ES)是由歐洲計算機制造商協會ECMA(European Computer Manufacturers Association)制定的標準化腳本程序設計語言。

  • 其次ES只是JS的其中一部分。一個完整的JS主要包括這幾部分(ES,DOM,BOM)。他們的結構圖是這樣的。JS與ES的關係

  • 其中ES負責腳本的基本語法及邏輯的實現。

  • DOM負責HTML標籤的解析及渲染實現。

  • BOM負責提供瀏覽器的API接口。

結論:

  • ES是一個與應用場景無關的純語言,只提供基本邏輯操做的實現,原則上不實現視覺上的功能。

  • WEB只是ES實現的宿主環境之一。ES在WEB中結合DOM和BOM造成了JS。

  • 這也是爲何最新的JS語法稱爲ES6而不是JS6。(實際上,像前面所說的,ES只是一個純語言,因此ES6上更新的只是JS語法上的內容。並不會提供其餘新的WEB功能。新的WEB功能的內容屬於HTML或CSS的內容,如HTML5和CSS3)

  • 小插曲,有的小夥伴可能看到過JScript這個詞。JScript是在未制定出ES標準的混亂時代中,微軟出的自家使用腳本語言,至關於IE版本的JS。但如今JS已經獲得了統一。JScript已經不復存在了。

DOM的本質及DOM級介紹

前言:

在前端學習過程當中,咱們一聽到DOM會天然地聯想到HTML的標籤。可是DOM真的只是和HTML有關係嗎?到底什麼纔是DOM呢?另外時常遇到的DOM0123級又是指什麼東西呢?

解析(DOM的本質)

  • 咱們先來看看DOM的字面意思是什麼:DOM(Document Object Model),文檔對象模型。是將基於某文檔結構(如XML結構)的字符串轉化爲一棵在常駐內容的樹狀數據結構的模型。對於XML來講,就是對XML標籤進行解析後的數據結構體(咱們稱之爲DOM樹)。

  • 它的理念是:開發者經過該數據結構得到文檔結構(即有特定字符串組成的文檔)的控制權。經過DOM提供的接口,對該文檔結構進行增,刪,改等操做,即對數據結構進行操做。

  • DOM自己是獨立於平臺和語言的。是進行腳本解析的解決思路(將文檔轉化爲對象,並以樹狀結構組織起來)。不一樣的語言能夠針對自身的特色製做本身的DOM實現。(而咱們理解中的DOM只是針對HTML語言的是其中一種實現而已)

  • 除了HTML DOM外,其餘的語言也發佈了只針對本身的DOM標準。如SVG(可伸縮矢量圖),MathML(數學標記語言),SMIL(同步多媒體集成語言)

解析(DOM級別介紹)

解釋完DOM的本質之後,接下來的DOM全都特指HTML DOM

  • 一句話說完,DOM級別只是DOM的版本。不一樣的DOM級實現了不一樣的功能。

  • DOM0是指在W3C進行標準化以前,還處於未造成標準的初期階段的版本。即還未造成標準的東西。嚴格意義上並不在DOM版本的範疇,只是爲版本出來前的DOM起個名字,因此才說0版本。

  • DOM1級在1998年10月份成爲W3C的提議,由DOM核心與DOM HTML兩個模塊組成。DOM核心能映射以XML爲基礎的文檔結構,容許獲取和操做文檔的任意部分(即對某標籤的get和set)。DOM HTML經過添加HTML專用的對象與函數對DOM核心進行了擴展(即封裝了一些函數和對象,更方便地get和set)。

  • DOM2經過對象接口增長了對鼠標和用戶界面事件、範圍、遍歷(重複執行DOM文檔)和層疊樣式表(CSS)的支持。

  • DOM3經過引入統一方式載入和保存文檔和文檔驗證方法對DOM進行進一步擴展。

  • 並沒什麼卵用,只是學習幾個名詞的意思。

JS代碼特性介紹

前言

這部份內容適合沒有實際經驗的同窗,能夠經過這一部分了解JS的特性。有經驗的同窗也能夠了解一下,由於接下來的內容都是圍繞這幾大特性進行講解的。

特性

  • 弱類型語言:在JS中,變量沒有固定的數據類型。不一樣數據類型的變量是能夠相互轉換的,如:var a = 0; a = "a";(前面賦值爲數值類型,後面變成了字符串類型) 而C++,PHP則是強類型語言,不能直接進行數據類型的轉換。

  • 解析性語言:不一樣於java或C#等編譯性語言,js是不須要進行編譯的,而是由瀏覽器進行動態地解析與執行。能夠理解爲瀏覽器是一個大型的函數,而JS代碼是函數的參數。由瀏覽器去解析JS代碼。

  • 跨平臺性:因爲JS只依賴瀏覽器自己。底層實現無關,使得其他操做環境無關。實現了跨平臺。

  • 一切皆對象:JS是一項面向對象的語言。所看到的一切都是一個對象。

  • 單線程:JS是單線程的。這牽扯到JS的代碼執行順序,下面章節會進行介紹。

  • 垃圾自動回收:JS不須要主動回收內存。JS引擎內部會週期性地檢查內容,定時回收無用的內存。

基本類型與引用類型

前言

本小節將介紹JS的基本類型和引用類型。固然咱們不會講哪些無聊的基本語法,而是深刻內部,介紹一些有趣的東西。

解析

  • ECMAScript中有5種基本數據類型:Undefined,Null,Boolean,Number和String。以及一種引用數據類型Object。

  • 其中Undefined和Null最爲特殊,由於他們是隻有一個值的數據類型(undefined和null)。並且他們幾乎是同義的,他們之間有什麼區別呢?(雖然並沒什麼卵用,但面試卻最喜歡出這個題)。在這裏作一下介紹。示例代碼

    • 其實Underfined表示「缺失值」。表示有一個變量存在,即進行了定義,但該變量沒有被賦值。

    • 而Null表示一個空對象指針,根本不存在這個變量。Null更多地是起到語義做用。強調不存在這個變量。並且在實際編程過程當中,除非主動爲變量賦值爲null,不然不多出現變量爲null的狀況。另外null的做用是提早標示該變量已無用,讓GC回收機制能早點回收該資源。

    • 還有一個常見的面試題:請寫出typeof null的值。如示例代碼所示typeof null的值爲object。有沒有小夥伴會好奇爲何其餘4中基本數據類型的類型值都是其自身。而null的類型卻爲object

    • 其實這與JS的設計有關。JS類型值是存在32 BIT空間裏面的,在這32位中有1-3位表示其類型值,其它位表示真實數值。其中表示object的標記位正好是低三位都是0。000: object. The data is a reference to an object.
      而JS裏的Null是機器碼NULL空指針(空指針以全0標示)故其標示爲也是0,最終體現的類型仍是object。曾經有提案 typeof null === 'null'。但提案被拒絕了。

  • 基本數據類型和引用數據類型的區別:基本數據類型是按值訪問的。即該變量就存在了實際值。而引用數據類型保存的是則是對實際值的引用(即指向實際值的指針)。而咱們知道這個有什麼用呢?固然有用,這涉及到對象複製(淺複製與深複製)的問題。

    • 咱們來看一個例子示例代碼。能夠看到,到直接使用b=a進行賦值時。這時b獲取到是a變量的指針值,即此時b和a指向的是同一個地址值。因此當修改b對象中的值時,a對象中的值也會發生改變。這種只複製指針地址值的行爲稱爲淺複製。相應的,若是能返回獨立對象的值,咱們稱爲深複製。這也是爲何array的複製須要用到concat函數而不是直接用「=」進行復制。

    • 另外,插播一個知識點。在進行函數參數傳遞時。是經過按值傳遞的。即傳遞的是變量自己的值,而不是變量的引用。一樣咱們看一下示例代碼。即在函數在進行傳遞參數時,會新建一個形參變量elem,併爲其賦予實參變量的值。也許有同窗會認爲,在修改objA的時候明明會影響函數外部值,爲何還能叫作按值傳遞呢?其實這偏偏是按值專遞的結果。由於這裏傳遞的是objA這個指針對象的值,即對象的地址。那麼elem指向的就行該對象。即objA和elem指向的是同一個值(淺複製)。因此外部會發生變化

    • 引用類型和按值傳遞的概念很重要,後面講函數特性及this指針的時候會用到。

  • 另外,還有一個有趣的知識點。var a = "aaaa";這裏定義的a明明只是一個基本數據類型。而不是object,爲何會有a.substr這樣的函數呢?示例代碼。其實JS引擎在讀取基本數據類型時,會在後臺建立一個對應的類對象(稱爲基本包裝類型)。從而使咱們更加方便地對該變量進行操做。但這個基本操做類型的生存時間很是短,在相應的函數調用完成後就會自動銷燬,變回基本數據類型。因此對其添加變量的操做是無效的。

JS的垃圾回收機制

  • 首先科普一下GC是什麼意思。GC是垃圾回收的英文縮寫GC(Gabage collection)。(額,本人以前一直覺得什麼是高大上的東西....)

  • JavaScript有自動垃圾回收機制,也就是說執行環境會負責管理代碼執行過程當中使用的內存,在開發過程當中就無需考慮內存回收問題。

  • JavaScript垃圾回收的機制很簡單:找出再也不使用的變量,而後釋放掉其佔用的內存,可是這個過程不是實時的,由於其開銷比較大,因此垃圾回收器會按照固定的時間間隔週期性的執行。

  • JS的內存回收機制通常有兩種實現方式,分別是"標記清除""和"引用計數""方式。

  • 其中"標記清除"是最多見的回收方式,當某變量進入到執行上下文(做用域)時。JS引擎會將該變量標記爲「進入環境」狀態,當該環境所在的執行上下文(做用域)執行完畢時。裏面的變量將被標記爲「離開環境」狀態。而後當JS引擎週期性地檢測內存時。會將標記爲「離開環境」的變量所佔內存清空。以起到回收內存的目的。

  • 另一種垃圾回收實現方式是"引用計數"。即JS引擎會跟蹤每個變量的被引用次數。當被其餘變量引用時,計數就加一。但引用結束後,就將計數減一。當引用次數爲0時,進行內存回收。但這種方法有可能會致使內存的泄露。示例代碼

  • 在平常開發中,咱們能夠經過將變量的值設置爲null的方法,將該變量的引用計數清爲0。主動回收內存資源

做用域鏈介紹及其實現原理

前言的前言

做用域是JS中至關重要的一部份內容。是理解JS其餘高級內容(如:閉包,this指針,自執行函數)的基礎。若是能把JS做用域的原理及其應用理解透,那至關於已經拿下半個JS了。

前言

首先,咱們先來看這麼一段代碼。示例代碼。這段代碼很簡單,相信你們都能猜到預期的結果,外部函數不能訪問內部函數定義的變量,因此最後一個輸出elem2 is not defined。可是,有同窗好奇過爲何外部函數就不能訪問內部函數的變量嗎?其底層的實現原理是什麼?

做用域及做用域鏈介紹

在介紹上面的問題以前,咱們先來介紹做用域這個概念。

  • 做用域的定義

    • 定義什麼的咱們就直接略過了,咱們用人話來講:是指函數中定義過的內容的集合,即定義了的所有內容加在一塊兒就是做用域(除了程序員主動定義的,還有程序內部幫咱們定義的變量,例如函數中arguments變量)。

  • 做用域有這麼幾個特色

    • 每定義一個函數,都會產生一個對應的做用域

    • 做用域的層級關係與函數定義時所處的層級關係相同

    • 各層做用域間經過單向鏈式結構組織起來,造成做用域鏈,連接方向爲由內向外。

    • 變量的訪問是從當前所在做用域起,沿着不斷向外訪問,直到訪問到爲止,因此做用域間的訪問範圍是單向的,是內部做用域能夠訪問外部做用域。

  • 結論:因此在上面的例子中,外部函數不能訪問內部函數的變量

  • 彩蛋

    • 相信你們在看書的時候,常常會看到「做用域鏈」,「執行環境」,「執行上下文」這樣的字眼。其實這三個詞彙的意思是同樣的。都是指在執行這段代碼(一般以函數的形式存在)的時候,所能訪問的資源集合。

  • 很簡單,對吧。但這個概念後面會反覆用到。

閉包

前言

講閉包以前,咱們先來看一段示例代碼。你們以爲這段代碼的運行結果是什麼呢?

解析

  • 如控制檯所示,log出來的結果是「elem a」。funA函數已經執行完了。elemA這個變量應該會被回收釋放啊?那麼爲何還會輸出a呢?咱們使用JS的其中一種回收機制「引用計數」發來分析一下。

  • 咱們把變量elemA的內存記爲memoryA。memoryA首先會被elemA引用,引用計數爲1。

  • 在funB函數中使用了elemA,memoryA引用計數加一,爲2

  • funA執行完畢。回收funA中的elemA。memoryA引用計數減一。還剩1,不爲零。因此JS回收機制不會回收memoryA的內存空間。因此在執行c()實際是執行funB的時候。還能訪問到memoryA的內存。因此輸出爲"elem a"。

  • 像這種函數自己已執行完,但因爲其內部變量仍被使用。而得不到釋放的現象。咱們就稱做爲「閉包」。那麼怎麼釋放該內存呢?繼續減小其引用計數便可。就如代碼中所示。執行funB函數。memoryA的引用計數再減一。爲0。回收機制便可回收該內存。

  • 咱們能夠經過做用域鏈的角度,使用「標記清除」法再分析一遍。在定義elemA的時候,elemA標記爲「進入環境」(即進入其自己的做用域)。在funA執行完成後,本來其做用域須要回收的,可是因爲funB使用到elemA變量。因此funA的做用域沒能回收,因此elemA仍在「做用域鏈中」,沒能被標記爲「離開環境」。因此沒能被釋放。

  • 接下來,咱們經過一些小練習強化一下對閉包的理解。示例代碼

  • 爲何輸出的兩遍都是2?咱們來分析一下。

  • setTimeout函數裏面的i是在那個做用域裏面?是在setTimeout函數外部的test函數裏面。因此這裏造成了閉包。即便test執行完畢,i變量也不會被釋放。

  • setTimeout函數和test函數那個函數先被執行?很明顯setTimeout中的函數是後於test函數執行的。因此當setTimeout函數執行的時候。i以及被自增爲2了。因此兩邊的輸出爲2。

  • 再來一個更難的。咱們結合做用域和閉包來看一道題示例代碼

  • 這不是造成了閉包嗎?爲何輸出的是2而不是3呢?

  • 咱們回過頭看一下做用域鏈的定義。函數的做用域層級是在定義函數的時候就被固定下來的,與函數定義時的層級關係相同。因此funA和funB的做用域是處於同一級的。不存在閉包關係。因此funA中的輸出的A的值是外部定義的A的值。因此是2而不是3。

this指針

this指針的介紹

  • 什麼是this指針。

    • this指針是指向某個對象空間的指針,通常狀況下是指向(和強類型語言同樣)定義當前做用域所在的對象示例代碼。如示例代碼所示,logName函數中的this指針指向了objA對象(logName的做用域是在objA對象中定義的)。因此輸出了aaa。

    • 接着看上面的示例代碼,咱們在後面定義了一個沒有logName函數的對象objB,但沒有定義logName函數。那須要輸出objB中的name要這麼辦呢?咱們能不能向objA對象借用一下logName函數?讓objA裏面的this指針指向objB對象呢?

  • JS中this指針的特色

    • 不一樣於強語言的是,JS中的this指針是能夠經過修改,動態變化的。咱們能夠經過修改this指針來實現上面的需求。如示例代碼

    • 比較兩個例子,能夠發現,第二個代碼多加了bind(objB)。是這裏改變了this指針的指向嗎?是的。其實在JS中。有三個函數能夠改變this指針的指向。分別是bind,call,和apply函數。

  • bind,call,apply的介紹和比較

    • 先來看看這三個函數的使用示例示例代碼

    • 首先,三個裏面,最突出的函數就是bind。爲何惟獨bind函數不是直接使用。還須要在外面添加一個setTimeout呢?若是不加setTimeout會怎麼樣呢?示例代碼。在不加setTimeout的時候沒有輸出任何東西。而加了setTimeout纔會輸出。其實那是由於使用bind的時候,是僅僅修改this指針,並不會執行函數。在setTimeout中,計時後,才由setTimeout去調用執行這個函數。

    • 接着,call和apply直接執行了函數。他們的區別是什麼呢?他們惟一的區別是傳參方式的不一樣,call函數以枚舉(即直接列出來)的方式傳參。而apply是以數組的方式傳參的。咱們試一下調轉過來傳參示例代碼

    • 彩蛋,除了使用bind來修改this指針之外。咱們還可使用它來返回一個函數,日後再去執行。示例代碼。其實這個實例中並沒什麼卵用,只是這個實例說明兩個問題。

      • 當bind函數的第一個參數傳null時,表示不改變函數中this的指向。

      • 當函數前面的參數已經被賦值時,再使用bind時,是從剩餘沒有賦值的函數參數開始賦值的。

this指針的應用

  • 將this的指針指向正確的值。這個內容後面再講。

  • 利用this指針借用某對象的函數,前面的幾個例子就是利用了this指針借用函數。在介紹更多例子以前,咱們先插入一個僞數組的概念

    • 僞數組是指哪些只有length和中括索引功能,沒有數組相應功能函數的數據結構。例如示例代碼。如這個例子中pList1就沒有slice的函數。

    • 在前端中最常常接觸到的僞數組有函數中的隱藏變量 arguments 和用 getElementsByTagName得到到的元素集合。

    • 這時咱們就能夠利用到this指針去借用數組中的函數,實現咱們想要的目的。示例代碼

    • 另外怎麼把僞數組轉化爲真數組呢?其實只要在上一個例子上再修改一下就能夠了。示例代碼

  • 在此基礎上,咱們還能夠利用this指針實現一部分類功能。示例代碼

this指針與做用域的關係(主要是window)

其實若是沒有該死的window對象的話,本來this指針和做用域是沒有太大關係的

  • 先看代碼,示例代碼。!!!?竟然輸出的是HanMeiMei而不是LiLei?爲何會輸出HanMeiMei?這是否是意味着this指針發生變化了?咱們log一下this指針的值.

  • 示例代碼。果真,this指針真的是指向了window??WTF?咱們明明沒有修改過this指針的值啊?爲何this的指向改變了?

  • 在這裏就要補充一下的this指針的定義了。上面講到(通常狀況下是指向定義當前做用域時所在的對象,即在那個對象內定義就是指向誰。)。但實際上,this是指向經過點操做符(如objA.funA())調用本函數的那個對象。即誰調用我,我就指向誰。示例代碼如在這裏,objB並無定義logName函數。只是定義了一個變量並賦值爲函數的引用。這時使用objB.logName實際上調用的是objA對象裏面的函數。而log出來的對象就是objB。

  • 再回到setTimeout函數。咱們前面log過this,發現this是指向window的。這證實在setTimeout中的是window對象去調用logName的。即至關於window.logName();發現問題了嗎?HanMeiMei既是做用域中的變量,也是window對象中的變量。

  • 實際上,全部在全局做用域(注意僅僅是全局做用域)中定義的變量都是window對象下的變量。均可以經過window對象進行訪問。因此一旦沒有經過對象去調用某函數,而是直接運行的話(如不是objA.fun()而是直接fun()),等價於在window下調用(fun()等價於window.fun())。this指針的值都指向window。

  • 再因爲全部在全局做用域(注意僅僅是全局做用域)中定義的變量都是window對象下的變量因此logName中的值指指向了window.name。也就是做用域中的name值。做用域就是經過window與this指針掛上了關係。

this指針和閉包的比較

  • 這是一個性能優化的探討問題。

  • 有時候咱們會遇到這樣的狀況。使用閉包和this指針均可以實現一樣的功能。例如示例代碼。使用這兩種方式都可以成功log出name的值。那這時候咱們使用哪一個好呢?

  • 因爲log函數會輸出到控制檯,執行速度慢,咱們經過修改name的值來模擬內部操做示例代碼能夠看到。直接使用閉包的性能更佳。性能是使用bind的5倍以上。因爲時間緣由。原理我就再也不這裏介紹了。有興趣的同窗能夠經過這個連接看看.爲何閉包比this更快

  • 至此,本教程中,最困難的兩個點(this指針與JS線程)中的其中一個已經介紹完了。接下來休息一下,穿插幾個比較簡單的概念。

自執行函數

什麼是自執行函數

  • 這個術語看起來很高大上。其實說到底就是一個定義完了立刻就執行的函數。其實這不是JS的新特性。而是利用JS特性作出來的效果。即JS特性的巧妙應用。它的表現形式是這樣的。示例代碼.其中第一個()是用於隔離第二個括號,使其function(){}函數定義完,避免語法錯誤。而第二個()是爲了執行剛剛定義好的函數。

自執行函數的做用

  • 第一個,也是百度答案最多的一個避免污染全局做用域

    • 究竟是怎麼污染全局做用域呢?示例代碼。這裏假設調用了兩個JS的狀況,本來LaoWang要向HanMeiMei表白的,可是因爲第二個文件中,name的值被修改爲了LiLei,致使LaoWang表錯白。恩,小明是爽了。隔壁老王就糟了。

    • 那怎麼用自執行函數來解決呢?很簡單,用自執行函數把每一個人本身寫的JS代碼包裹起來就能夠了;示例代碼.

    • 原理:利用到了做用域的概念。因爲每自執行函數自己造成了一個做用域。而這兩個做用域是處於同一級的。互不影響,從而避免了污染全局做用域。這個技巧在多人協做的項目裏面頗有用。

  • 第二個,也是用得比較少的一個構建私有屬性

    • 使用強類型語言的同窗應該都知道私有的概念。而在JS中,沒有類的概念(在ES5以前是沒有的,但ES6新增了類的概念),從而也沒有了私有屬性。但咱們能夠利用自執行函數構建一個。示例代碼。這樣就避免了使用直接調用name變量。實現了私有屬性的功能。

    • 原理:前面對閉包及做用域理解了的同窗,相信大家已經能夠猜想背後的原理了。這裏使用到了閉包(使得name不被回收),做用域(使得返回的對象中的函數能訪問name變量),已經自執行函數(沒有留下函數的引用,使得外界不能訪問)的特色。

聲明提早

前言

人們常說JS是解釋性語言,語句都是執行到哪裏才進行解析的。但實際上真的是這樣嗎?

什麼是聲明提早

  • JS聲明提早是指,在做用域中(即在函數中啦),變量的聲明老是優先於其餘語句被執行的。(即老是先執行聲明語句,再執行其餘語句)。示例代碼

  • 但若是把var name;改爲var name = "LiLei"又會怎麼樣呢?示例代碼。說好的提早呢?其實JS引擎在解析這段代碼的時候會把var name = "LiLei"分紅兩條語句來執行。一個是變量定義,一個是變量賦值。因此實際上登記於這樣示例代碼

  • 聲明對於函數一樣適用示例代碼。並且注意點也是同樣的。示例代碼

何時容易出現錯誤

講這個點以前,先插入兩個待會要用到的概念

  • 沒有塊級做用域

    • 在JS中是沒有塊級做用域的概念的。for,while等控制語句不構成塊級做用域。示例代碼

  • 沒有經過var定義的變量均是定義全局變量

    • 如標題所說。 沒有經過var定義的變量都是全局變量,都在全局做用域中找獲得。示例代碼。當這兩個概念在加載聲明提早上就很噁心了。

  • 例子示例代碼。這個例子比較特殊,在chrome中可能回報錯。建議你們用其餘瀏覽器打開。我這裏使用safari打開。執行結果是HanMeiMei。給你們一點時間整理一下思路。其實它等價於

  • 固然這些例子都比較極端。並且看起來很傻。其實只是想告訴你們。當你們在見解中遇到很奇怪的bug的時候。能夠考慮是否是因爲聲明提早引發的了。

JS線程問題

前言

這是前面提到過在本教程中,最複雜的兩個知識點(this指針與JS線程)中的JS線程問題。雖然咱們平常開發中可能不會主動提到這個概念,但其實咱們常常會用到。譬如ajax異步請求,事件處理,計時器延遲執行等都涉及到JS線程的概念。學習好本概念雖然不會像this指針那樣解決不少問題,但有助於加深咱們對JS底層的理解。有助於理清邏輯關係。

什麼是線程

  • 首先,咱們先來了解一下什麼是線程。百度一下。恩,第一句進程什麼的咱們就略過,先無論啦。重點是後面那句。線程是程序執行流的最小單元。用人話翻譯就是說,一次只能執行一段代碼的執行器。同一時間內只能完成一項任務。後一項任務必須等待前面的任務執行完成才能被執行。

  • 單線程語言,顧名思義,是指一次只能執行一項任務的語言。

  • 多線程語言,是指一次能執行多項任務的語言。典型的多線程語言有C,C++等強類型語言。

  • JS是單線語言。介紹到這裏的時候,不知道小夥伴們會不會有這樣的疑惑。不對啊,JS能執行異步操做(例如AJAX操做)的啊,異步操做不就是容許後面的代碼先執行嗎?這和單線程的概念相沖突啊。JS怎麼會是單線程的呢?恩,咱們帶着這個問題往下看。

頁面加載流程解析

爲何js文件要放在body標籤的底部,而不建議放在頭部。由於當JS文件加載過程太慢的時候,會阻礙後面標籤的執行。恩,這個知識點相信你們都知道。但爲何JS文件的加載會影響HTML標籤的解析呢?它底層的原理是什麼呢?

先問你們一個小問題,你知道在HTML頁面裏面,怎麼樣作到不引人script標籤去執行JS代碼?

  • 先拋出兩個概念,瀏覽器的核心是兩部分:渲染引擎和JavaScript解釋器(又稱JavaScript引擎)。

    • 其中渲染引擎負責解析HTML標籤,將標籤轉化爲HTML視圖。渲染引擎處理頁面,一般分紅四個階段

      • 解析代碼:HTML代碼解析爲DOM,CSS代碼解析爲CSSOM(CSS Object Model)

      • 對象合成:將DOM和CSSOM合成一棵渲染樹(render tree)

      • 佈局:計算出渲染樹的佈局(layout)

      • 繪製:將渲染樹繪製到屏幕

    • JavaScript引擎的主要做用是,讀取網頁中的JavaScript代碼,對其處理後運行。

    • 渲染引擎和JS引擎分別使用不一樣的線程

  • 其實整個HTML頁面的加載過程是這樣的。

    • 瀏覽器一邊下載HTML網頁,一邊開始解析。

    • 解析過程當中,發現script標籤。

    • 暫停解析,網頁渲染的控制權轉交給JavaScript引擎。

    • 若是script標籤引用了外部腳本,就下載外部腳本,不然就直接執行腳本。

    • 執行完畢,控制權交還渲染引擎,恢復往下解析HTML網頁

  • 也就是說,加載外部腳本時,瀏覽器會暫停頁面渲染,等待腳本下載並執行完成後,再繼續渲染。既然渲染引擎和JS引擎是用不一樣的線程去執行的。那應該能夠並行執行啊。例如渲染線程繼續解析標籤,JS引擎去執行JS語句。爲何不這樣作呢?

  • 緣由是JavaScript能夠修改DOM(好比使用document.write方法),因此必須把控制權讓給它,不然會致使複雜的線程競賽的問題。(例如同時對同一個DIV進行修改,那以那個爲準?)

  • 彩蛋,恩,回答前面問的那個問題。其實在html頁面中,在標籤裏面設置的每個事件。都是一個JS執行段。都會以事件回調的方式執行裏面的代碼。示例代碼

JS事件循環(Event Loop)

注意,這裏講的是JS的事件循環機制,不是JS的事件傳遞機制,經過本節的學習,你將明白JS異步是怎麼實現的.

  • 首先,須要明確一個概念:JS是單線程的,但並不意味着瀏覽器也是單線程的。實際上瀏覽器是多線程的(例如前面講到的渲染引擎就是其中一個線程),JS經過和瀏覽器後臺線程的配合,實現了JS的事件機制。

  • 咱們以示例代碼爲例。JS的執行流程是這樣的。

    • 首先JS在底層維護了一個是消息隊列的隊列。

    • JS執行到addEventListener時,將回調函數的地址交給監聽頁面操做的其中一個瀏覽器後臺線程(假設叫作監聽線程)。

    • 當監聽的事件被觸發的時候,監聽線程將以前JS交代的回調函數地址放入到消息隊列當中。

    • 重點來了,JS引擎是怎麼讀取消息列表的呢?JS引擎是先把JS同步操做所有執行完畢(即JS文件中的代碼所有執行完畢,咱們先用「主邏輯操做」),纔會去按順序調用消息隊列的回調函數地址。並且這個從消息隊列中調用回調函數的過程是循環執行的。

  • 從上面的分析中,咱們能夠獲得如下結論:

    • 所有回調操做都在主邏輯操做完成後才被執行的.

    • 因爲消息隊列是以隊列的形式保存起來的。而隊列自己是一個先進先出的數據結構,因此會優先調用隊列排在前面的回調函數,(因爲JS的執行是單線程的,一次只能執行一個代碼段)因此只有前一個回調函數被執行完了,第二個回調函數才能被執行。因此回調函數的執行順序是在被加入到消息隊列的那一刻決定的。

    • 另外,除了前面提到的監聽線程,瀏覽器還有處理定時器的進程(如setTimeout)、處理用戶輸入的進程(input)、處理網絡通訊的進程(AJAX)等等

setTimeout問題

  • 一樣的setTimeout函數也可使用上面的過程進行分析。只不過把上面的監聽線程換成處理定時器的瀏覽器線程(假設叫作定時器線程)。即

    • JS執行到setTimeout時,將回調函數的地址交給定時器線程。

    • 定時間線程進行計時,當計時結束後。定時器線程將所攜帶的回調函數地址放入消息隊列中。

    • JS引擎把主邏輯執行完成後,調用消息隊列中的回調函數。

  • 前面兩步都沒有問題,但到最後一步就可能會出現問題了。

    • 因爲定時器線程和JS引擎是使用不一樣線程,同時進行的。而JS引擎去讀取消息隊列以前須要先將主操做執行完。那麼一旦主操做的執行時間大於定時器的計時時間。那麼回調函數的時間等待時間將大於程序所設置的計時時間。示例代碼

    • 另外,除了主操做會延遲計時器的回調函數執行。因爲JS引擎在讀取消息隊列的時候是按順序讀取的。這意味着排在前面的回調函數也可能會推遲排在後面的計時器回調函數的執行示例代碼

    • 插播一個知識HTML5標準規定了setTimeout()的第二個參數的最小值(最短間隔),不得低於4毫秒,若是低於這個值,就會自動增長。在此以前,老版本的瀏覽器都將最短間隔設爲10毫秒。示例代碼。固然了,實際運行時間還須要結合電腦自己的運行狀況。再加上console.log自己須要消耗必定的時間。因此每次的運行時間可能都會有變化。在這是也只是告訴你們會有這個規定。

  • setTimeout的妙用。

    • 除了日常的計時做用,咱們還能夠利用消息隊列必須在主邏輯以後被執行的特色,結合setTimeout把某段代碼放在最後執行(即主邏輯以後)。示例代碼setTimeout(fn,0)的含義是,指定某個任務在主線程最先可得的空閒時間執行。

HTML5新內容:worker介紹

web Worker這是H5新出的一個內容,使用這個API,咱們能夠簡單地建立後臺運行的線程。但JS本質上仍是單線程的。

  • 咱們先來看看他的基本使用方法示例代碼。注意,worker的調用是異步的。因此after post優先執行。

  • 須要注意的是worker只能進行數據操做,不能調用DOM,和BOM。它裏面連window對象都沒有。這個我就不進行深刻講解的。這些均可以在網上找獲得。我想把哪些不怎麼容易找獲得的內容給你們講解一下。

  • worker實現的不是真正意義上的線程,它徹底受主線程所控制的。示例代碼注意,這裏執行將執行死循環,小夥伴們根據本身的狀況考慮要不要運行】。能夠看到,一開始worker能夠被正常執行,但當JS主線程被的死循環執行的時候,worker立刻中止了工做,貌似JS和worker同一時間只能執行一個。而真正的多線程運行結果應該是script和worker隨機交替出現的。(其實這裏的分析是不夠全面的)

  • 前面只給到JS主線程貌似阻塞了worker的線程。那麼反過來。worker會不會阻塞JS主線程的操做呢?咱們來看示例代碼。【注意,這裏也是死循環的】(其實你們應該已經猜到了運行的結果,若是worker會阻塞JS主邏輯的操做,那要worker還有什麼用?)能夠看到,worker線程並不會阻塞JS主線程的執行。這意味着worker頗有用。咱們能夠將一些很耗時的操做放到worker中執行。保證主頁面的流暢運行。譬如對大型的圖片或附件進行壓縮,加密操做等。參考網站,這是一個分解質因數的網站,以前高等數學的老師告訴咱們。分解質因數的難度複雜度是O(n1/4)。這意味着整個分解過程是很耗時間的。而這個頁面使用了worker在後臺進行分解。因此在等待的過程當中,頁面是不卡的。

worker的本質分析

  • 咱們來分析一下worker的本質是什麼,其實worker本質上和瀏覽器的計時器線程等是沒有區別的。是瀏覽器新創建的一個線程,用於執行特定的任務。

  • 而worker實際上和JS主線程是能夠同步執行的,是能夠組成多線段的。以前貌似JS阻塞了worker的緣由,只是由於console對象不能同時被多個線程所擁有。而JS主線程的優先級比worker高,因此從worker中搶走了console對象的控制器。形成了這個現象發生。

  • 而第二個例子則證實了worker和JS主線程的並行性。由於在作修改數字的操做時。console的輸出沒有被打斷。

  • 因此證實worker是能夠實如今數據計算上的多線程。但因爲它自己不徹底具有JS的所有功能,如不能操做DOM,BOM。因此廣泛被認爲worker實現的是ECMAScript的多線程,JS從本質上是單線程的

相關文章
相關標籤/搜索