摘要:解析首先簡稱是由歐洲計算機制造商協會制定的標準化腳本程序設計語言。級在年月份成為的提議,由核心與兩個模塊組成。通過引入統一方式載入和保存文檔和文檔驗證方法對進行進一步擴展。其中表示的標記位正好是低三位都是。但提案被拒絕了。
JS高級入門教程 目錄
本文章定位及介紹
JavaScript與ECMAScript的關系
DOM的本質及DOM級介紹
JS代碼特性
基本類型與引用類型
JS的垃圾回收機制
作用域鏈介紹及其實現原理
閉包
this指針
自執行函數的介紹及應用
聲明提前
JS線程問題
本培訓的定位及相關介紹 內容特點:配套代碼解析,示例代碼
配套測試試題
內容實在,不會講爛大街的東西。
目標對象定位:主要面向對象:對于有一年左右工作經驗的前端工程師。提高JS的認識,突破JS學習瓶頸。
對于沒有經驗的小伙伴,可以通過本文章對JS有初步認識,了解JS的相關特性。
除了系統性的JS內容,還會穿插介紹一些筆者在學習過程中遇到的認識上的困惑與小問題。希望能減少小伙伴在學習過程中遇到的障礙。
對了,里面有些內容是本人覺得有趣的,好玩的概念。可能除了能幫大家理清楚概念以外,并沒什么卵用。希望大家不要見怪。
JavaScript與ECMAScript的關系 前言:可能大家在閱讀JS相關書籍或瀏覽網上資料的時候會多多少少看過ECMAScript這個詞匯,它與JS有什么關系呢?還有為什么JS最新的語法不是叫JS6而是叫ES6呢?在這里給大家介紹一下JS與ES的關系。
首先ECMAScript(簡稱ES)是由歐洲計算機制造商協會ECMA(European Computer Manufacturers Association)制定的標準化腳本程序設計語言。
其次ES只是JS的其中一部分。一個完整的JS主要包括這幾部分(ES,DOM,BOM)。他們的結構圖是這樣的。
其中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從本質上是單線程的。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/81870.html
摘要:前端入門的門檻相對較低,學習曲線是越來越陡峭,由淺入深,可以分為四個階段。第二階段高級程序設計有的書是用來成為經典的,比如犀牛書還有些書是用來超越經典的,顯然這本書就是。接下來可以看看教程,看看源代碼,嘗試著寫一寫這些效果。 前端入門的門檻相對較低,學習曲線是越來越陡峭,由淺入深,可以分為四個階段。 第一階段:《JavaScript DOM編程藝術》 看這本書之前,請先確認你對J...
摘要:推薦高性能網站建設指南高性能網站建設進階指南理由在讀完前幾本書之后我們對前端的性能和自己的代碼的效率已經達到相當的高度了,然后我們在接觸一些前端工程師的一些精髓。 WEB前端研發工程師,在國內算是一個朝陽職業,這個領域沒有學校的正規教育,大多數人都是靠自己自學成才。本文主要介紹自己從事web開發以來(從大二至今)看過的書籍和自己的成長過程,目的是給想了解JavaScript或者是剛...
摘要:推薦高性能網站建設指南高性能網站建設進階指南理由在讀完前幾本書之后我們對前端的性能和自己的代碼的效率已經達到相當的高度了,然后我們在接觸一些前端工程師的一些精髓。 WEB前端研發工程師,在國內算是一個朝陽職業,這個領域沒有學校的正規教育,大多數人都是靠自己自學成才。本文主要介紹自己從事web開發以來(從大二至今)看過的書籍和自己的成長過程,目的是給想了解JavaScript或者是剛...
摘要:推薦高性能網站建設指南高性能網站建設進階指南理由在讀完前幾本書之后我們對前端的性能和自己的代碼的效率已經達到相當的高度了,然后我們在接觸一些前端工程師的一些精髓。 WEB前端研發工程師,在國內算是一個朝陽職業,這個領域沒有學校的正規教育,大多數人都是靠自己自學成才。本文主要介紹自己從事web開發以來(從大二至今)看過的書籍和自己的成長過程,目的是給想了解JavaScript或者是剛...
摘要:前言今天和大家一起聊聊的推薦書籍,每一本都是精選,做前端開發的朋友們如果沒讀過,可以嘗試一下。如果怕麻煩,也可以關注曉舟報告,發送獲取書籍,四個字,就可以得到電子書的提取碼。 前言 今天和大家一起聊聊JavaScript的推薦書籍,每一本都是精選,做前端開發的朋友們如果沒讀過,可以嘗試一下。下面給大家簡單介紹了書的內容,還有讀書的方法,希望可以幫大家提升讀書效率。 一、《JavaScr...
閱讀 1887·2021-11-15 11:46
閱讀 1077·2021-10-26 09:49
閱讀 1819·2021-10-14 09:42
閱讀 3374·2021-09-26 09:55
閱讀 827·2019-08-30 13:58
閱讀 1024·2019-08-29 16:40
閱讀 3462·2019-08-26 10:27
閱讀 601·2019-08-23 18:18