文將從以下六個方面講解引用類型和基本類型
1. 概念
2. 內存圖
3. 引用類型和基本類型作為函數的參數體現的區別
4. 引用類型的優點:
5. 引用類型的賦值(對比基本類型)
6. 淺拷貝和深拷貝
以下為詳細內容:
1. 概念:
基本類型也叫簡單類型,存儲的數據是單一的,如:學生的個數就是一個數字而已;引用類型也叫復雜類型,存儲的數據是復雜的,如:學生,包括學號,姓名,性別,年齡等等很多信息。從內存(大家如果不懂內存,請查閱相關資料)的角度來說:基本類型只占用一塊內存區域;引用類型占用兩塊內存區域。即定義基本類型的變量時,在內存中只申請一塊空間,變量的值直接存放在該空間;定義引用類型的變量時(容易理解的是,我門看到new運算符,一般就是定義引用類型的變量),在內存中申請兩塊空間,第一塊空間存儲的是第二塊空間的地址,第二塊空間存儲的是真正的數據;第一塊空間叫作第二塊空間的引用(地址),所以叫作引用類型。
javaScript中的基本類型包括:數字(Number),字符串(String),布爾(Boolean),Null,Undefined五種;
javascript的引用類型是:Object。而Array,Date是屬于Obejct類型。
2. 內存圖:
如下代碼(都是定義了兩個局部變量):
以上兩行代碼的內存圖:
可以看到,num變量只占用了一塊內存區域;arr變量占用了兩塊內存區域,arr變量在棧區(不懂棧區的人,先不要想太多)申請了一塊內存區域,存儲著地址,存儲的地址是堆區的地址。而堆區中真正才存儲著數據,所以說,arr變量占用了兩塊內存區域。這樣看來,引用類型的變量好像還占用內存多了。哈哈,不要著急,后面了解了引用類型的優點后,你就會覺得這是問題了。
當我們讀取num變量的值時,直接就能讀到,但是當我們要讀取arr里的值時,先找到arr中的地址,然后根據地址再找到對應的數據。
引用類型,類似于windows操作系統中的快捷方式。快捷方式就是一個地址,真正的內容是快捷方式所指向的路徑的內容。如:我們把d:\t.txt文件創建一個快捷方式放在桌面上,那么,桌面上的快捷方式會占用桌面的空間,而d:\t.txt會占用d盤的空間,所以,占用了兩塊空間。
基本類型就相當于文件。
引用類型,類似于我們在入學報名填寫報名表時,填寫家庭地址,這個家庭地址就相當于第一塊空間,真正你家(第二塊內存空間)不在報名表上。學校要找你家,先在報名表上找到你家的地址,然后根據地址,才能找到你家去。
3. 引用類型的優點:
引用類型作為函數的參數時,優點特別明顯,第一,形參傳遞給實參時,只需要傳遞地址,而不需要搬動大量的數據(節約了內存開銷);第二,形參對應的數據改變時,實參對應的數據也在改變(很多時候,我們希望這樣)。
如以下代碼:
先定義函數(冒泡排序)
當調用冒泡排序時,
看看內存以上代碼執行時的,內存變化:
圖中,當執行,①對應的代碼(var arr1=[250,2,290,35,12,99];)時,內存中會產生①對應的變化,即在棧中申請一塊內存區域,起名為arr1,在堆區中申請內存空間放置250,2,290,35,12,99,并把堆區中的內存的地址賦給arr1的內存中;當執行②對應的代碼bubble(arr1)時,調用函數。這時候會定義形參arr(內存中③對應的變化),即在棧中申請一塊內存區域,起名為arr,并把arr1保存的地址賦給了arr(內存中②表示的賦值),這樣,形參arr和實參arr1就指向了同一塊內存區域。數組中的值250,2,290,35,12,99在內存中只有一份。即,不用把數組中每個元素的值再復制一份,節約了內存。如果對內存圖看懂了,那么,當形參arr對應的數據順序改變了,實參arr1對應的數據順序也就改變了。即,實現了形參數據改變時,實參數據也改變了。所以,bubble函數不需要返回值,依然可以達到排序的目的。可以運行我示例中的代碼,看看是不是達到了排序的效果。
補充,基本類型作為函數參數的內存變化:
內存圖:
4. 引用類型變量的賦值:
引用類型變量賦值時,賦的是地址。即兩個引用類型變量里存儲的是同一塊地址,也就是說,兩個引用類型變量都指向同一塊內存區域。所以,兩個引用類型變量對應的數據時一樣的。
基本類型變量賦值時的內存變化。
5. 淺拷貝和深拷貝
先說對象的復制,上面說了,引用類型(對象)的賦值,只是賦的地址,那么要真正復制一份新的對象(即克隆)時,又該怎么辦。
但是,當一個對象的屬性又是一個引用類型時,會出現淺拷貝和深拷貝的問題。用一個自定義的object類型來說明問題。
如:
person2.name="張四"; //不會改變掉person1的name屬性。
person2.address.country="北京";//會改變掉person1的address.country
大家注意看,person1和person2的name屬性各有各的空間,但是person1.address.country和person2.address.country是同一塊空間。所以,改變person2.address.country的值時,person1.address.country的值也會改變。這就說明拷貝(克隆)的不到位,這種拷貝叫作淺拷貝,而進一步把person1.address.country和person1.address.name也拷貝(克隆)了,就是深拷貝。要做到深拷貝,就需要對每個屬性的類型進行判斷,如果是引用類型,就再循環進行拷貝(需要用到遞歸)。
(未完待續)
千鋒HTML5教學部 田江
avaScript語言的一大特點就是單線程,也就是說,同一個時間只能做一件事。那么,為什么JavaScript不能有多個線程呢?這樣能提高效率啊。
JavaScript的單線程,與它的用途有關。作為瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操作DOM。這決定了它只能是單線程,否則會帶來很復雜的同步問題。比如,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另一個線程刪除了這個節點,這時瀏覽器應該以哪個線程為準?
所以,為了避免復雜性,從一誕生,JavaScript就是單線程,這已經成了這門語言的核心特征,將來也不會改變。
為了利用多核CPU的計算能力,HTML5提出Web Worker標準,允許JavaScript腳本創建多個線程,但是子線程完全受主線程控制,且不得操作DOM。所以,這個新標準并沒有改變JavaScript單線程的本質。
單線程就意味著,所有任務需要排隊,前一個任務結束,才會執行后一個任務。如果前一個任務耗時很長,后一個任務就不得不一直等著。
如果排隊是因為計算量大,CPU忙不過來,倒也算了,但是很多時候CPU是閑著的,因為IO設備(輸入輸出設備)很慢(比如Ajax操作從網絡讀取數據),不得不等著結果出來,再往下執行。
JavaScript語言的設計者意識到,這時主線程完全可以不管IO設備,掛起處于等待中的任務,先運行排在后面的任務。等到IO設備返回了結果,再回過頭,把掛起的任務繼續執行下去。
于是,所有任務可以分成兩種,一種是同步任務(synchronous),另一種是異步任務(asynchronous)。同步任務指的是,在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行后一個任務;異步任務指的是,不進入主線程、而進入"任務隊列"(task queue)的任務,只有"任務隊列"通知主線程,某個異步任務可以執行了,該任務才會進入主線程執行。
具體來說,異步執行的運行機制如下。(同步執行也是如此,因為它可以被視為沒有異步任務的異步執行。)
(1)所有同步任務都在主線程上執行,形成一個執行棧(execution context stack)。
(2)主線程之外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。
(3)一但"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務隊列",看看里面有哪些事件。那些對應的異步任務,于是結束等待狀態,進入執行棧,開始執行。
(4)主線程不斷重復上面的第三步。
復制代碼
只要主線程空了,就會去讀取"任務隊列",這就是JavaScript的運行機制。這個過程會不斷重復。
主線程從"任務隊列"中讀取事件,這個過程是循環不斷的,所以整個的這種運行機制又稱為Event Loop(事件循環)。為了更好地理解Event Loop,請看下圖
事件循環可以簡單描述為:
函數入棧,當Stack中執行到異步任務的時候,就將他丟給WebAPIs,接著執行同步任務,直到Stack為空; 在此期間WebAPIs完成這個事件,把回調函數放入CallbackQueue中等待; 當執行棧為空時,Event Loop把Callback Queue中的一個任務放入Stack中,回到第1步。
接下來看一個異步函數執行的例子:
var start=new Date();
setTimeout(function cb(){
console.log("時間間隔:",new Date()-start+'ms');
},500);
while(new Date()-start<1000){};
復制代碼
JS的異步有一個機制的,就是會分為宏任務和微任務。宏任務和微任務會放到不同的event queue中,先將所有的宏任務放到一個event queue(macro-task),再將微任務放到一個event queue(micro-task)中。執行完宏任務之后,就會先從微任務中取這個回調函數執行。
最開始, 執行棧為空, 微任務隊列為空, 宏任務隊列有一個 script 標簽(內含整體代碼)
將第一個宏任務出隊, 這里即為上述的 script 標簽
整體代碼執行過程中, 如果是同步代碼, 直接執行(函數執行的話會有入棧出棧操作), 如果是異步代碼, 會根據任務類型推入不同的任務隊列中(宏任務或微任務)
當執行棧執行完為空時, 會去處理微任務隊列的任務, 將微任務隊列的任務一個個推入調用棧執行完
微任務執行完后,檢查是否需要重新渲染 UI。
...往返循環直到宏任務和微任務隊列為空
出隊一個宏任務 -> 調用棧為空后, 執行一隊微任務 -> 更新界面渲染 -> 回到第一步
一個event loop有一個或者多個task隊列。task任務源非常寬泛,比如ajax的onload,click事件,基本上我們經常綁定的各種事件都是task任務源,還有數據庫操作(IndexedDB ),需要注意的是setTimeout、setInterval、setImmediate也是task任務源。總結來說task任務源:
microtask 隊列和task 隊列有些相似,都是先進先出的隊列,由指定的任務源去提供任務,不同的是一個 event loop里只有一個microtask 隊列。另外microtask執行時機和Macrotasks也有所差異
下圖是一個事件循環的流程
舉個簡單的例子,假設一個script標簽的代碼如下:
Promise.resolve().then(function promise1 () {
console.log('promise1');
})
setTimeout(function setTimeout1 (){
console.log('setTimeout1')
Promise.resolve().then(function promise2 () {
console.log('promise2');
})
}, 0)
setTimeout(function setTimeout2 (){
console.log('setTimeout2')
}, 0)
script里的代碼被列為一個task,放入task隊列。
循環1:
循環2:
循環3:
console.log('-------start--------');
setTimeout(()=> {
console.log('setTimeout'); // 將回調代碼放入另一個宏任務隊列
}, 0);
new Promise((resolve, reject)=> {
for (let i=0; i < 5; i++) {
console.log(i);
}
resolve()
}).then(()=>{
console.log('Promise'); // 將回調代碼放入微任務隊列
})
console.log('-------end--------');
-------start--------
0
1
2
3
4
-------end--------
Promise
setTimeout
由EXP1,我們可以看出,當JS執行完主線程上的代碼,會去檢查在主線程上創建的微任務隊列,執行完微任務隊列之后才會執行宏任務隊列上的代碼
主線程=> 主線程上創建的微任務=> 主線程上創建的宏任務
script里的代碼被列為一個task,放入task隊列。
循環1:
循環2:
setTimeout(_=> console.log('setTimeout4'))
new Promise(resolve=> {
resolve()
console.log('Promise1')
}).then(_=> {
console.log('Promise3')
Promise.resolve().then(_=> {
console.log('before timeout')
}).then(_=> {
Promise.resolve().then(_=> {
console.log('also before timeout')
})
})
})
console.log(2)
Promise1
2
Promise3
before timeout
also before timeout
setTimeout4
由EXP2,我們可以看出,在微任務隊列執行時創建的微任務,還是會排在主線程上創建出的宏任務之前執行(因為微任務只有一條,自增鏈不斷的話 會一直往下執行微任務,不會被中斷)
主線程=> 主線程上創建的微任務1=> 微任務1上創建的微任務2=> 主線程上創建的宏任務
script里的代碼被列為一個task,放入task隊列。
循環1:
循環2:
// 宏任務隊列 1
setTimeout(()=> {
// 宏任務隊列 1.1
console.log('timer_1');
setTimeout(()=> {
// 宏任務隊列 3
console.log('timer_3')
}, 0)
new Promise(resolve=> {
resolve()
console.log('new promise')
}).then(()=> {
// 微任務隊列 1
console.log('promise then')
})
}, 0)
setTimeout(()=> {
// 宏任務隊列 2.2
console.log('timer_2')
}, 0)
console.log('==========Sync queue==========')
==========Sync queue==========timer_1
new promise
promise then
timer_2
timer_3
主線程(宏任務隊列 1)=> 宏任務隊列 1.1=> 微任務隊列 1=> 宏任務隊列 3=>宏任務隊列2.2
循環1:
循環2
循環3
循環4
// 宏任務1
new Promise((resolve)=> {
console.log('new Promise(macro task 1)');
resolve();
}).then(()=> {
// 微任務1
console.log('micro task 1');
setTimeout(()=> {
// 宏任務3
console.log('macro task 3');
}, 0)
})
setTimeout(()=> {
// 宏任務2
console.log('macro task 2');
}, 0)
console.log('==========Sync queue(macro task 1)==========');
==========Sync queue(macro task 1)==========new Promise(macro task 1)
micro task 1
macro task 2
task 3
復制代碼
異步宏任務隊列只有一個,當在微任務中創建一個宏任務之后,他會被追加到異步宏任務隊列上(跟主線程創建的異步宏任務隊列是同一個隊列)
主線程=> 主線程上創建的微任務=> 主線程上創建的宏任務=> 微任務中創建的宏任務
循環1:
循環2
循環2
<div class="outer">
<div class="inner"></div>
</div>
var outer=document.querySelector('.outer');
var inner=document.querySelector('.inner');
function onClick() {
console.log('click');
setTimeout(function() {
console.log('timeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise');
});
}
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);
點擊 inner,最終打印結果為:
click
promise
click
promise
timeout
timeout
為什么打印結果是這樣的呢?我們來分析一下: (0)將 script 標簽內的代碼(宏任務)放入執行棧執行,執行完后,宏任務微任務隊列皆空。
(1)點擊 inner,onClick 函數入執行棧執行,打印 "click"。執行完后執行棧為空,因為事件冒泡的緣故,事件觸發線程會將向上派發事件的任務放入宏任務隊列。
(2)遇到 setTimeout,在最小延遲時間后,將回調放入宏任務隊列。遇到 promise,將 then 的任務放進微任務隊列
(3)此時,執行棧再次為空。開始清空微任務,打印 "promise"
(4)此時,執行棧再次為空。從宏任務隊列拿出一個任務執行,即前面提到的派發事件的任務,也就是冒泡。
(5)事件冒泡到 outer,執行回調,重復上述 "click"、"promise" 的打印過程。
(6)從宏任務隊列取任務執行,這時我們的宏任務隊列已經累計了兩個 setTimeout 的回調了,所以他們會在兩個 Event Loop 周期里先后得到執行。
可以看成是:
function onClick() {
//模擬outer click事件
setTimeout(function(){onClick1()},0)
console.log('click');
setTimeout(function() {
console.log('timeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise');
});
}
function onClick1() {
console.log('click1');
setTimeout(function() {
console.log('timeout1');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
});
}
//模擬inner click事件
onClick()
inner.click()
打印結果為:
click
click
promise
promise
timeout
timeout
依舊分析一下:
(0)將 script(宏任務)放入執行棧執行,執行到 inner.click() 的時候,執行 onClick 函數,打印 "click"
(1)當執行完 onClick 后,此時的 script(宏任務)還沒返回,執行棧不為空,不會去清空微任務,而是會將事件往上冒泡派發
...(關鍵步驟分析完后,續步驟就不分析了)
可以看成是:
function onClick() {
console.log('click');
setTimeout(function() {
console.log('timeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise');
});
}
onClick();
onClick();
在一般情況下,微任務的優先級是更高的,是會優先于事件冒泡的,但如果手動 .click() 會使得在 script代碼塊 還沒彈出執行棧的時候,觸發事件派發。
【執行進入microtask檢查點時,瀏覽器會執行以下步驟:】
總結以上規則為一條通俗好理解的:
[總結]所有的異步都是為了按照一定的規則轉換為同步方式執行。
備的一篇干貨文章,對你有用的話就收藏或者轉發吧!
HTML5 DOM 選擇器
javascript 代碼
阻止默認行為
javascript 代碼
阻止冒泡
javascript 代碼
鼠標滾輪事件
javascript 代碼
檢測瀏覽器是否支持svg
javascript 代碼
檢測瀏覽器是否支持canvas
javascript 代碼
檢測是否是微信瀏覽器
javascript 代碼
常用的一些正則表達式
javascript 代碼
js時間戳、毫秒格式化
javascript 代碼
getBoundingClientRect() 獲取元素位置
javascript 代碼
HTML5全屏
javascript 代碼
關注我
定期分享前端干貨、新技術解讀
如果你越學越迷茫,沒思路
加我微信 webwula
備注“想提升自己”
我教你學前端的方法
*請認真填寫需求信息,我們會在24小時內與您取得聯系。