javascript
《JavaScript高级程序设计(第四版)》红宝书学习笔记(2)(第四章:变量、作用域与内存)
個人對第四版紅寶書的學習筆記。不適合小白閱讀。這是part2。持續(xù)更新,其他章節(jié)筆記看我主頁。
(記 * 的表示是ES6新增的知識點,記 ` 表示包含新知識點)
第四章:變量、作用域與內(nèi)存
4.1 原始值與引用值
ECMAScript變量可以包含兩種不同類型的數(shù)據(jù):原始值和引用值。原始值(primitive value)就是最簡單的數(shù)據(jù),引用值(reference value)則是由多個值構(gòu)成的對象。
在把一個值賦給變量時,JavaScript引擎必須確定這個值是原始值還是引用值。上一章討論了6種原始值:Undefined、Nu11、Boolean、Number、String和Symbol。保存原始值的變量是按值(byvalue)訪問的,因為我們操作的就是存儲在變量中的實際值。
引用值是保存在內(nèi)存中的對象。與其他語言不同,JavaScript不允許直接訪問內(nèi)存位置,因此也就不能直接操作對象所在的內(nèi)存空間。在操作對象時,實際上操作的是對該對象的引用(reference)而非實際的對象本身。為此,保存引用值的變量是按引用(by reference)訪問的。
注意:在很多語言中,字符串是使用對象表示的,因此被認為是引用類型。ECMAScript打破了這個慣例。
4.1.1 動態(tài)屬性
原始值和引用值的定義方式很類似,都是創(chuàng)建一個變量,然后給它賦一個值。不過,在變量保存了這個值之后,可以對這個值做什么,則大有不同。
-
對于引用值而言,可以隨時添加、修改和刪除其屬性和方法。
-
原始值不能有屬性,盡管嘗試給原始值添加屬性不會報錯。
注意,原始類型的初始化可以只使用原始字面量形式。如果使用的是new關(guān)鍵字,則JavaScript會創(chuàng)建一個Object類型的實例,但其行為類似原始值,下面來看看這兩種初始化方式的差異:
let name1 "Nicholas": let name2 = new String("Matt"); name1.age = 27; name2.age = 26: console.log(name1.age): //-> undefined console.log(name2.age); //-> 26 console.10g(typeof name1); //-> string console.log(typeof name2); // object4.1.2 復(fù)制值
除了存儲方式不同,原始值和引用值在通過變量復(fù)制時也有所不同。
1)在通過變量把一個原始值賦到另一個變量時,原始值會被復(fù)制到新變量的位置:
let num1 = 5: let num2 = num1;這里,num1包含數(shù)值5。當把num2初始化為num1時,num2也會得到數(shù)值5。這個值跟存儲在numl 中的5是完全獨立的,因為它是那個值的副本。這兩個變量可以獨立使用,互不干擾。這個過程如圖4-1所示。
2)在把引用值從一個變量賦給另一個變量時,存儲在變量中的值也會被復(fù)制到新變量所在的位置。區(qū)別在于,這里復(fù)制的值實際上是一個指針,它指向存儲在堆內(nèi)存中的對象。操作完成后,兩個變量實際上指向同一個對象,因此一個對象上面的變化會在另一個對象上反映出來,如下面的例子所示:
let obj1 = new Object(); let obj2 = obj1: obj1.name = "Nicholas"; console.log(obj2.name); // “Nicholas"在這個例子中,變量obj1保存了一個新對象的實例。然后,這個值被復(fù)制到obj2,此時兩個變量都指向了同一個對象。在給obj1創(chuàng)建屬性name并賦值后,通過obj2也可以訪問這個屬性,因為它們都指向同一個對象。圖4-2展示了變量與堆內(nèi)存中對象之間的關(guān)系:
4.1.3 傳遞參數(shù)
ECMAScript中所有函數(shù)的參數(shù)都是按值傳遞的。這意味著函數(shù)外的值會被復(fù)制到函數(shù)內(nèi)部的參數(shù)中,就像從一個變量復(fù)制到另一個變量一樣。如果是原始值,那么就跟原始值變量的復(fù)制一樣,如果是引用值,那么就跟引用值變量的復(fù)制一樣。
在按值傳遞參數(shù)時,值會被復(fù)制到一個局部變量(即一個命名參數(shù),或者用ECMAScript的話說,就是arguments對象中的一個槽位)。在按引用傳遞參數(shù)時,值在內(nèi)存中的位置會被保存在一個局部變量,這意味著對本地變量的修改會反映到函數(shù)外部(這在ECMAScript中是不可能的)。
1)“Javascript中函數(shù)按值傳參”這個性質(zhì)在原始值上可以很明顯的看到:
function addTen(num){num += 10;return num; } let count = 20; let result = addTen(count); console.log(count); //-> 20,沒有變化,原始值沒有受到影響 conaole.log(result); //-> 302)而對于引用值來說:
function setName (obj){obj.name = "Nicholas"; } let person = new Object(); setName (person); console.log(person.name); //-> "Nicholas" //看的出來,傳入person對象之后,person對象多了一個name屬性局部作用域中修改對象,這種變化反映到了全局作用域。但這是不是意味著對象的傳參是按引用傳參呢?再看下一個例子:
function setName(obj){obj.name ="Nicholas";//添加兩條語句:obj = new Object();obj.name = "Greg";//這里試圖將obj重新定義為一個有著不同name屬性的新對象//如果person是按引用傳遞的,那么person應(yīng)該自動將指針改為指向 name為'Greg'的新對象 } let person = new Object(); setName(person); console.log(person.name); //-> "Nicholas"person對象的name屬性值仍然是"Nicholas",這意味著:函數(shù)中參數(shù)的值改變后,原始的引用仍然沒變。當obj在函數(shù)內(nèi)部被重寫時,它變成了一個指向本地對象的指針。而那個對象在函數(shù)執(zhí)行結(jié)束時就被銷毀了。
注意:ECMAScript中函數(shù)的參數(shù)就是局部變量。
4.1.4 確定類型
前一章提到的typeof操作符最適合用來判斷一個變量是否為原始類型。更確切地說,它是判斷一個變量是否為字符串、數(shù)值、布爾值或undefined的最好方式。如果值是對象或nu11,那么typeof 會返回"object"。
看的出來,typeof操作符對于引用值的用處不大。所以,ECMAScript提供了一個instanceof操作符:
result = variable instanceof constructor
如果變量是給定引用類型(由其原型鏈決定,將在第8章詳細介紹)的實例,則 instanceof 操作符返回true。如下:
console.log (person instanceof Object); //變量person是Object 嗎? console.log (colors instanceof Array); //變量 colors是Array 嗎? console.log (pattern instanceof RegExp); //變量 pattern是RegExp嗎?instanceof 檢測任何引用值和Object構(gòu)造函數(shù)都會返回true,因為所有引用值都是Object的實例。當然,對于原始值而言,instanceof則會返回false。
注意:typeof 操作符在用于檢測函數(shù)時也會返回“function"。當在 Safari(直到 SafariS)和 Chrome(直到 Chrome 7)中用于檢測正則表達式時,由于實現(xiàn)細節(jié)的原因,typeof也會返回“function"。ECMA-262 規(guī)定,任何實現(xiàn)內(nèi)部[[Ca11]]方法的對象都應(yīng)該在typeof檢測時返回“function"。因為上述瀏覽器中的正則表達式實現(xiàn)了這個方法,所以typeof 對正則表達式也返回“function"。在IE和Firefox中,typeof 對正則表達式返回"object"。
4.2 執(zhí)行上下文與作用域 `
執(zhí)行上下文(以下簡稱“上下文”)的概念在JavaScript中是頗為重要的。變量或函數(shù)的上下文決定了它們可以訪問哪些數(shù)據(jù),以及它們的行為。每個上下文都有一個關(guān)聯(lián)的變量對象(variable object),而這個上下文中定義的所有變量和函數(shù)都存在于這個對象上。雖然無法通過代碼訪問變量對象,但后臺處理數(shù)據(jù)會用到它。
1)全局上下文是最外層的上下文。根據(jù) ECMAScript實現(xiàn)的宿主環(huán)境,表示全局上下文的對象可能不一樣。
在瀏覽器中,全局上下文就是我們常說的window對象(第12章會詳細介紹)。因此所有通過 var定義的全局變量和函數(shù)都會成為 window 對象的屬性和方法。使用 let 和 const 的頂級聲明不會定義在全局上下文中,但在作用域鏈解析上效果是一樣的。
上下文在其所有代碼都執(zhí)行完畢后會被銷毀,包括定義在它上面的所有變量和函數(shù)(全局上下文在應(yīng)用程序退出前才會被銷毀,比如關(guān)閉網(wǎng)頁或退出瀏覽器)。
2)每個函數(shù)調(diào)用都有自己的上下文,即函數(shù)上下文。當代碼執(zhí)行流進入函數(shù)時,函數(shù)的上下文被推到一個上下文棧上,在函數(shù)執(zhí)行完之后,上下文棧會彈出該函數(shù)上下文,將控制權(quán)返還給之前的執(zhí)行上下文。ECMAScript程序的執(zhí)行流就是通過這個上下文棧進行控制的。
3)上下文中的代碼在執(zhí)行的時候,會創(chuàng)建變量對象的一個作用域鏈(scope chain)。這個作用域鏈決定了各級上下文中的代碼在訪問變量和函數(shù)時的順序,代碼正在執(zhí)行的上下文的變量對象始終位于作用域鏈的最前端。
如果上下文是函數(shù),則其活動對象(activation object)用作變量對象。活動對象最初只有一個定義變量:arguments(全局上下文中沒有這個變量)。作用域鏈中的下一個變量對象來自包含上下文,再下一個對象來自再下一個包含上下文。以此類推直至全局上下文;全局上下文的變量對象始終是作用域鏈的最后一個變量對象。
代碼執(zhí)行時的標識符解析是通過沿作用域鏈逐級搜索標識符名稱完成的。搜索過程始終從作用域鏈的最前端開始,然后逐級往后,直到找到標識符。(如果沒有找到標識符,那么通常會報錯)
看一看下面這個例子:
var color = "blue";function changeColor(){if(color === "blue"){color = 'red'; } else{color = 'blue';} } changeColor();對這個例子而言,函數(shù)changeColor()的作用域鏈包含兩個對象:一個是它自己的變量對象(就是定義arguments對象的那個),另一個是全局上下文的變量對象。這個函數(shù)內(nèi)部之所以能夠訪問變量color,就是因為可以在作用域鏈中找到它。
局部作用域中定義的變量可用于在局部上下文中替換全局變量。
內(nèi)部上下文可以通過作用域鏈訪問外部上下文中的一切,但外部上下文無法訪問內(nèi)部上下文中的任何東西。上下文之間的連接是線性的、有序的。
注意:函數(shù)參數(shù)被認為i是當前上下文中的變量,因此也跟上下文中的其他變量遵循相同規(guī)則。
PS:上下文還是很好理解的,這里不再多討論。
4.2.1 作用域鏈增強
雖然執(zhí)行上下文主要有全局上下文和函數(shù)上下文兩種(eva1()調(diào)用內(nèi)部存在第三種上下文),但有其他方式來增強作用域鏈。某些語句會導(dǎo)致在作用域鏈前端臨時添加一個上下文,這個上下文在代碼執(zhí)行后會被刪除。通常在兩種情況下會出現(xiàn)這個現(xiàn)象,即代碼執(zhí)行到下面任意一種情況時:
- try/catch 語句的catch塊
- with語句
這兩種情況下,都會在作用域鏈前端添加一個變量對象。對with語句來說,會向作用域鏈前端添加指定的對象;對catch語句而言,則會創(chuàng)建一個新的變量對象,這個變量對象會包含要拋出的錯誤對象的聲明。看下面的例子:
function buildUrl()(let qs = "?debug=true";with(location){let url = href + qs; //這里引用href實際是引用location.href//而引用qs時引用的其實是buildUrl()中定義的qs變量}return url; //這里url沒有定義,因為let聲明被限制在了塊級作用域 }這里,with 語句將 location對象作為上下文,因此 location會被添加到作用域鏈前端。bui1dUrl() 函數(shù)中定義了一個變量qs。當 with 語句中的代碼引用變量href 時,實際上引用的是location.href,也就是自己變量對象的屬性。在引用qs時,引用的則是定義在 bui1dUrl() 中的那個變量,它定義在函數(shù)上下文的變量對象上。而在with語句中使用 var聲明的變量url會成為函數(shù)上下文的一部分,可以作為函數(shù)的值被返回;但像這里使用let聲明的變量ur1,因為被限制在塊級作用域(稍后介紹),所以在with塊之外沒有定義。
4.2.2 變量聲明 *
ES6之后,JavaScript的變量聲明經(jīng)歷了翻天覆地的變化。直到ECMAScript5.1,var都是聲明變量的唯一關(guān)鍵字。ES6不僅增加了1et和const兩個關(guān)鍵字,而且還讓這兩個關(guān)鍵字壓倒性地超越var成為首選。
1)使用 var的函數(shù)作用域聲明
在使用var聲明變量時,變量會被自動添加到最接近的上下文。在函數(shù)中,最接近的上下文就是函數(shù)的局部上下文。在with語句中,最接近的上下文也是函數(shù)上下文。如果變量未經(jīng)聲明就被初始化了,那么它就會自動被添加到全局上下文,如下面的例子所示:
function add(num1,num2){var sum = num1 + num2;return sum; } let result = add(10, 20); //-> 30 console.log(sum); //-> 報錯:sum 在這里不是有效變量 這里訪問不到函數(shù)內(nèi)部var聲明的變量但如果省略上面例子中的關(guān)鍵字var,那么sum 在add()被調(diào)之后就變成可以訪問的了,如下所示:
function add(num1,num2){sum = num1 + num2;return sum; } let result = add(10, 20); //-> 30 console.log(sum); //-> 30 可以訪問,因為sum未經(jīng)聲明就被初始化,它被直接添加到了全局上下文注意:未經(jīng)聲明而初始化變量是Javascript編程中一個常見的錯誤,會導(dǎo)致很多問題。務(wù)必注意。
嚴格模式下,未經(jīng)聲明而初始化變量會報錯。
PS:變量提升這里不再提及。
2)使用let的塊級作用域聲明
塊級作用域由最近的一對花括號界定。換言之,if塊、while塊、function塊,甚至是單獨的塊(也就是僅僅只有兩個花括號)也是let變量聲明的作用域。
let和var的第二個不同是:在同一作用域不能聲明兩次。重復(fù)的var聲明會被忽略,但重復(fù)的let聲明會拋出SyntaxError。
嚴格來說,let在Javascript運行時也會被提升,但是由于“暫時性死區(qū)”的緣故,(上一章提到過)實際上不能在聲明之前使用let變量。
3)使用const的變量聲明
const聲明只應(yīng)用到頂級原語或者對象。換言之,賦值為對象的const變量不能再被重新賦值為其他引用值,但對象的鍵不受限制。如果想讓整個對象都不能修改,可以使用Object.freeze(),這樣再給屬性賦值時雖然不會報錯,但會靜默失敗:
const o1 = {}; o1 = {}; //TypeError:給常量賦值const o2 = {}; o2.name = 'Jake'; alert(o2.name); //-> Jake //對象的鍵不受限制const o3 = Object.freeze({}); o3.name = 'Jake'; //這一步?jīng)]有意義,因為整個對象都不可被修改了 alert(o3.name); //-> undefined由于const聲明暗示變量的值是單一類型且不可修改,JavaScript運行時編譯器可以將其所有實例都替換成實際的值,而不會通過查詢表進行變量查找。谷歌的V8引擎就執(zhí)行這種優(yōu)化。
注意:開發(fā)實踐表明,如果開發(fā)流程并不會因此而受很大影響,就應(yīng)該盡可能地多使用const 聲明,除非確實需要一個將來會重新賦值的變量。這樣可以從根本上保證提前發(fā)現(xiàn)重新賦值導(dǎo)致的bug。
4)標識符查找
當在特定上下文中為讀取或?qū)懭攵靡粋€標識符時,必須通過搜索確定這個標識符表示什么。搜索開始于作用域鏈前端,以給定的名稱搜索對應(yīng)的標識符。如果在局部上下文中找到該標識符,則搜索停止,變量確定;如果沒有找到變量名,則繼續(xù)沿作用域鏈搜索。(注意,作用域鏈中的對象也有一個原型鏈,因此搜索可能涉及每個對象的原型鏈。)這個過程一直持續(xù)到搜索至全局上下文的變量對象。如果仍然沒有找到標識符,則說明其未聲明。
4.3 垃圾回收 `
JavaScript是使用垃圾回收的語言,也就是說執(zhí)行環(huán)境負責在代碼執(zhí)行時管理內(nèi)存。JavaScript通過自動內(nèi)存管理實現(xiàn)內(nèi)存分配和閑置資源回收。基本思路很簡單:確定哪個變量不會使用,然后釋放它占用的內(nèi)存。這個過程是周期性的,即垃圾回收程序每隔一段時間就會自動運行。
瀏覽器發(fā)展史上,用到過兩種標記策略:標記清理和引用技術(shù)。JavaScript最常用的垃圾回收策略是標記清理。
內(nèi)存管理:
將內(nèi)存占用量保持在一個較小的值可以讓頁面性能更好。優(yōu)化內(nèi)存占用的最佳手段就是保證在執(zhí)行代碼時只保存必要的數(shù)據(jù)。如果數(shù)據(jù)不再必要,就把它設(shè)置成null,從而釋放其引用。這也可以叫做釋放引用。這個建議最適合全局變量和全局對象的屬性。局部變量在超出作用域后會被自動解除引用。
不過要注意,解除對一個值得引用并不會導(dǎo)致相關(guān)內(nèi)存被回收。解除引用的關(guān)鍵在于確保相關(guān)的值已經(jīng)不在上下文中了,因此它在下次垃圾回收時會被回收。
1)通過let和const聲明提升性能
因為const和let都以塊(而非函數(shù))為作用域,所以這兩個關(guān)鍵字可能會更早的讓回收程序介入,盡早回收應(yīng)該回收的內(nèi)存。
2)隱藏類和刪除操作
根據(jù)Javascript所在的運行環(huán)境,有時候需要根據(jù)瀏覽器使用的JavaScript引擎來采取不同的性能優(yōu)化策略。截至2017年,Chrome是最流行的瀏覽器,使用V8 JavaScript引擎。V8在將解釋后的JavaScript代碼編譯為實際的機器碼時會利用“隱藏類”。如果你的代碼非常注重性能,那么這一點可能對你很重要。
運行期間,V8 會將創(chuàng)建的對象與隱藏類關(guān)聯(lián)起來,以跟蹤它們的屬性特征。能夠共享相同隱藏類的對象性能會更好,V8會針對這種情況進行優(yōu)化,但不一定總能夠做到。比如下面的代碼:
function Article(){this.title = 'Inauguration Ceremony Features Kazoo Band'; } let a1 = new Article(); let a2 = new Article();V8會在后臺配置,讓這兩個類實例共享相同的隱藏類,因為這兩個實例共享同一個構(gòu)造函數(shù)和原型。假設(shè)之后又添加了下面這行代碼:
a2.author ='Jake';此時兩個Article實例就會對應(yīng)兩個不同的隱藏類。根據(jù)這種操作的頻率和隱藏類的大小,這有可能對性能產(chǎn)生明顯影響。
當然,解決方案就是避免JavaScript的“先創(chuàng)建再補充”(ready-fire-aim)式的動態(tài)屬性賦值,并在構(gòu)造函數(shù)中一次性聲明所有屬性,如下所示:
function Article(opt_author){this.title = 'Inauguration Cerenony Features Kazoo Band';this.author = opt_author; } let al = new Article(); let a2 = new Article('Jake');這樣,兩個實例基本上就一樣了(不考慮hasOwnProperty的返回值),因此可以共享一個隱藏類從而帶來潛在的性能提升。不過要記住,使用delete關(guān)鍵字會導(dǎo)致生成相同的隱藏類片段。看一下這個例子:
function Article(){this.title= 'Inauguration Ceremony Features Kazoo Band';this.author ='Jake'; } let a1 = new Article(): let a2 = new Article(); delete a1.author;在代碼結(jié)束后,即使兩個實例使用了同一個構(gòu)造函數(shù),它們也不再共享一個隱藏類。動態(tài)刪除屬與動態(tài)添加屬性導(dǎo)致的后果一樣。最佳實踐是把不想要的屬性設(shè)置為nu11。這樣可以保持隱藏類不和繼續(xù)共享,同時也能達到刪除引用值供垃圾
3)內(nèi)存泄漏
意外聲明全局變量
也就是局部作用域中聲明變量時缺少聲明操作符,而導(dǎo)致該變量變成了全局變量。加上var / const / let即可。
定時器
定時器的回調(diào)通過閉包引用外部變量時,如:
let name = 'Jake'; setInterval(()=>{console.log(name); },100) //定時器只要一直運行,回調(diào)函數(shù)中引用的name就會一直占用內(nèi)存,垃圾回收程序自然不會清理外部變量使用閉包
let outer = () =>{let name = 'Jake';return function(){return name;}; }; //上述代碼創(chuàng)建了一個內(nèi)部閉包,只要outer函數(shù)存在就不能清理name,因為閉包一直在引用它。如果name的內(nèi)容很大,就會出現(xiàn)很大的問題了4)靜態(tài)分配與對象池
總結(jié)
以上是生活随笔為你收集整理的《JavaScript高级程序设计(第四版)》红宝书学习笔记(2)(第四章:变量、作用域与内存)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: iptables实现访问A的请求重定向到
- 下一篇: C#反射破坏单例