JS內(nèi)存空間與閉包

一哄尔、JS內(nèi)存空間

JS有7種數(shù)據(jù)類型:基本數(shù)據(jù)類型(Boolean、String侍咱、Number耐床、undefined、null楔脯、Symbol)和引用數(shù)據(jù)類型(Object)撩轰。其中,基本數(shù)據(jù)類型都是按值訪問(wèn)的昧廷,它們被保存在棧數(shù)據(jù)結(jié)構(gòu)中堪嫂,每一個(gè)變量都對(duì)應(yīng)一個(gè)具體的變量值,我們可以直接操作保存在變量中的實(shí)際的值麸粮;而對(duì)于引用數(shù)據(jù)結(jié)構(gòu)溉苛,在棧中保存的值是指向堆中對(duì)象的指針,JavaScript不允許直接訪問(wèn)堆內(nèi)存中的位置弄诲,因此我們不能直接操作對(duì)象的堆內(nèi)存空間愚战,而只能通過(guò)棧中的變量地址來(lái)訪問(wèn)到堆內(nèi)存中的對(duì)象娇唯。

借一張圖來(lái)理解一下:


上圖中,a1,a2,a3都是基本數(shù)據(jù)類型寂玲,在棧內(nèi)存空間中保存著具體的值塔插;而c,d是引用數(shù)據(jù)類型,分別保存的是指向堆內(nèi)存空間中數(shù)組[1,2,3]和對(duì)象{m:20}的地址指針拓哟。因此當(dāng)我們要訪問(wèn)堆內(nèi)存中的引用數(shù)據(jù)類型時(shí)想许,實(shí)際上我們首先是從變量對(duì)象中獲取了該對(duì)象的地址引用(或者地址指針),然后再?gòu)亩褍?nèi)存中取得我們需要的數(shù)據(jù)断序。

深流纹、淺拷貝

當(dāng)我們?cè)诳截惢緮?shù)據(jù)類型的時(shí)候,就相當(dāng)于在棧內(nèi)存中copy了一份變量-值违诗,此時(shí)修改拷貝后的變量是不會(huì)影響到原變量的漱凝;由于對(duì)象保存在堆內(nèi)存中,而棧內(nèi)存保存的是指向這個(gè)對(duì)象的指針诸迟,所以當(dāng)我們拷貝引用數(shù)據(jù)類型的時(shí)候茸炒,只是拷貝了一份這個(gè)對(duì)象的地址,實(shí)際拷貝變量引用的還是原變量所指向的對(duì)象阵苇,當(dāng)我們操作拷貝變量的時(shí)候壁公,就會(huì)修改原變量的內(nèi)容,這就是淺拷貝绅项。



淺拷貝的方法有:

  • ES5方法:Array.prototype.concat() , Array.prototype.slice()
  • ES6方法:擴(kuò)展運(yùn)算符 ...obj , Object.assign({}, obj)

深拷貝的方法有:

  • JSON.parse(JSON.stringify(obj)) JSON序列化紊册,不適用于undefined,null和Infinity
  • 遞歸拷貝快耿,檢測(cè)到數(shù)據(jù)類型為object湿硝,則遞歸拷貝其屬性
  • JQuery中的 $.extend() 方法

二、JS的內(nèi)存空間管理

JS內(nèi)存管理主要針對(duì)的是局部變量润努,例如函數(shù)中聲明和使用的變量关斜。其內(nèi)存生命周期為:

  • 開(kāi)辟所需要的內(nèi)存
  • 使用分配到的內(nèi)存(讀、寫)
  • 不需要時(shí)(函數(shù)結(jié)束)將其釋放铺浇、歸還

JS有自動(dòng)的垃圾回收機(jī)制痢畜,垃圾收集器需要跟蹤每個(gè)變量,找出那些不再繼續(xù)使用的值鳍侣,然后釋放其占用的內(nèi)存丁稀。垃圾收集器會(huì)每隔固定的時(shí)間段就執(zhí)行一次釋放操作。JS的垃圾回收機(jī)制主要有兩種:標(biāo)記清除法倚聚,引用計(jì)數(shù)法

  • 標(biāo)記清除法:是JS最常用的垃圾收集方式线衫。IE/Firefox/Opera/Chrome和Safari使用的都是標(biāo)記清除法,只是垃圾收集的時(shí)間間隔互不相同惑折。標(biāo)記清除法的原理是授账,垃圾收集器在運(yùn)行是會(huì)給存儲(chǔ)在內(nèi)存中的變量都加上標(biāo)記枯跑,當(dāng)變量進(jìn)入環(huán)境時(shí),標(biāo)記會(huì)被去掉白热,在此之后敛助,再一次被打上標(biāo)記的白能量將被視為準(zhǔn)備刪除的變量,最后屋确,垃圾收集器完成內(nèi)存清理工作纳击,銷毀那些帶標(biāo)記的值并回收它們所占用的內(nèi)存空間。

  • 引用計(jì)數(shù)法:Netscape Navigator 3.0最早使用的就是引用技術(shù)策略的瀏覽器攻臀。引用計(jì)數(shù)的含義是跟蹤記錄每個(gè)值被引用的次數(shù)焕数,當(dāng)聲明了一個(gè)變量并將一個(gè)引用類型值賦給該變量時(shí),這個(gè)值的引用次數(shù)+1刨啸,如果包含這個(gè)值引用變量又取得了另外一個(gè)值百匆,那么引用次數(shù)-1.當(dāng)這個(gè)值的引用次數(shù)變?yōu)?時(shí),就會(huì)被當(dāng)成垃圾清理掉呜投。但是引用計(jì)數(shù)法容易造成循環(huán)引用,因?yàn)檠h(huán)引用時(shí)存璃,兩者的變量引用次數(shù)都不會(huì)為0仑荐,這樣垃圾收集器就沒(méi)有辦法判定這個(gè)是否被使用完了,就會(huì)造成內(nèi)存泄漏纵东。為了避免這類問(wèn)題粘招,在引用完變量后要手動(dòng)切斷兩個(gè)對(duì)象的關(guān)聯(lián),為變量賦值為null偎球。

  • V8中的GC算法是分代式垃圾回收機(jī)制洒扎,將內(nèi)存(堆)分為新生代和老生代兩部分。新生代中的對(duì)象一般存活時(shí)間較短衰絮,內(nèi)存空間分為兩部分袍冷,分別為 From 空間和 To 空間。新分配的對(duì)象會(huì)被放入 From 空間中猫牡,當(dāng) From 空間被占滿時(shí)胡诗,新生代 GC 就會(huì)啟動(dòng)了。算法會(huì)檢查 From 空間中存活的對(duì)象并復(fù)制到 To 空間中淌友,如果有失活的對(duì)象就會(huì)銷毀煌恢。當(dāng)復(fù)制完成后將 From 空間和 To 空間互換,這樣 GC 就結(jié)束了震庭。老生代中的對(duì)象一般存活時(shí)間較長(zhǎng)且數(shù)量也多瑰抵,新生代中的對(duì)象如果經(jīng)歷過(guò)GC的話,就會(huì)被移到老生代空間中器联。如果To 空間的對(duì)象占比大小超過(guò) 25 %二汛,會(huì)將對(duì)象從新生代空間移到老生代空間中婿崭。老生代空間中,通常會(huì)先標(biāo)記存活對(duì)象习贫,失活對(duì)象被清理逛球;對(duì)于清除對(duì)象后會(huì)造成堆內(nèi)存出現(xiàn)碎片的情況,當(dāng)碎片超過(guò)一定限制后會(huì)啟動(dòng)壓縮算法苫昌。在壓縮過(guò)程中颤绕,將活的對(duì)象像一端移動(dòng),直到所有對(duì)象都移動(dòng)完成然后清理掉不需要的內(nèi)存祟身。

三奥务、執(zhí)行上下文(Execution Context)

每次當(dāng)控制器轉(zhuǎn)到可執(zhí)行代碼的時(shí)候,就會(huì)進(jìn)入一個(gè)執(zhí)行上下文袜硫。執(zhí)行上下文可以理解為當(dāng)前代碼的執(zhí)行環(huán)境氯葬,它會(huì)形成一個(gè)作用域。JavaScript中的運(yùn)行環(huán)境大概包括三種情況婉陷。

  • 全局環(huán)境:JavaScript代碼運(yùn)行起來(lái)會(huì)首先進(jìn)入該環(huán)境
  • 函數(shù)環(huán)境:當(dāng)函數(shù)被調(diào)用執(zhí)行時(shí)帚称,會(huì)進(jìn)入當(dāng)前函數(shù)中執(zhí)行代碼
  • eval(不建議使用,可忽略)

因此在一個(gè)JavaScript程序中秽澳,必定會(huì)產(chǎn)生多個(gè)執(zhí)行上下文闯睹,JavaScript引擎會(huì)以棧的方式來(lái)處理它們,這個(gè)棧担神,稱為函數(shù)調(diào)用棧(call stack)楼吃。棧底永遠(yuǎn)都是全局上下文,而棧頂就是當(dāng)前正在執(zhí)行的上下文妄讯。

當(dāng)代碼在執(zhí)行過(guò)程中孩锡,遇到以上三種情況,都會(huì)生成一個(gè)執(zhí)行上下文亥贸,放入棧中躬窜,而處于棧頂?shù)纳舷挛膱?zhí)行完畢之后,就會(huì)自動(dòng)出棧炕置。

例如:

var color = 'blue';

function changeColor() {
    var anotherColor = 'red';

    function swapColors() {
        var tempColor = anotherColor;
        anotherColor = color;
        color = tempColor;
    }

    swapColors();
}

changeColor();

那么在這段程序的執(zhí)行過(guò)程中斩披,函數(shù)執(zhí)行棧中的上下文變化情況應(yīng)該為:



全局上下文在瀏覽器窗口關(guān)閉后出棧。

對(duì)于執(zhí)行上下文的總結(jié):

  • 單線程
  • 同步執(zhí)行讹俊,只有棧頂?shù)纳舷挛奶幱趫?zhí)行中垦沉,其他上下文需要等待
  • 全局上下文只有唯一的一個(gè),它在瀏覽器關(guān)閉時(shí)出棧
  • 函數(shù)的執(zhí)行上下文的個(gè)數(shù)沒(méi)有限制
  • 每次某個(gè)函數(shù)被調(diào)用仍劈,就會(huì)有個(gè)新的執(zhí)行上下文為其創(chuàng)建厕倍,即使是調(diào)用的自身函數(shù),也是如此贩疙。

四讹弯、變量對(duì)象與活動(dòng)對(duì)象

js是單線程語(yǔ)言况既,在瀏覽器中一個(gè)頁(yè)面永遠(yuǎn)只有一個(gè)線程在執(zhí)行js腳本代碼。js引擎執(zhí)行過(guò)程主要分為三個(gè)階段组民,分別是語(yǔ)法分析棒仍,預(yù)編譯和執(zhí)行階段:

  • 語(yǔ)法分析: 分別對(duì)加載完成的代碼塊進(jìn)行語(yǔ)法檢驗(yàn),語(yǔ)法正確則進(jìn)入預(yù)編譯階段臭胜;分析該js腳本代碼塊的語(yǔ)法是否正確莫其,如果出現(xiàn)不正確會(huì)向外拋出一個(gè)語(yǔ)法錯(cuò)誤(syntaxError),停止改js代碼的執(zhí)行耸三,然后繼續(xù)查找并加載下一個(gè)代碼塊乱陡;如果語(yǔ)法正確,則進(jìn)入到預(yù)編譯階段仪壮。
  • 預(yù)編譯:通過(guò)語(yǔ)法分析階段后憨颠,進(jìn)入預(yù)編譯階段,則創(chuàng)建變量對(duì)象(創(chuàng)建arguments對(duì)象(函數(shù)運(yùn)行環(huán)境下)积锅,函數(shù)聲明提前解析爽彤,變量聲明提升),確定作用域鏈以及this指向缚陷。
  • 執(zhí)行階段:創(chuàng)建變量對(duì)象完成后适篙,執(zhí)行代碼

當(dāng)調(diào)用一個(gè)函數(shù)時(shí),首先對(duì)函數(shù)進(jìn)行語(yǔ)法分析蹬跃,如果沒(méi)有語(yǔ)法錯(cuò)誤,就進(jìn)入預(yù)編譯階段铆铆,一個(gè)新的執(zhí)行上下文就會(huì)被創(chuàng)建蝶缀,后被壓入函數(shù)調(diào)用棧。而一個(gè)執(zhí)行上下文的生命周期可以分為兩個(gè)階段薄货。

  • 創(chuàng)建階段 在這個(gè)階段中翁都,執(zhí)行上下文會(huì)分別創(chuàng)建變量對(duì)象,建立作用域鏈谅猾,以及確定this的指向柄慰。
  • 代碼執(zhí)行階段 創(chuàng)建完成之后,就會(huì)開(kāi)始執(zhí)行代碼税娜,這個(gè)時(shí)候坐搔,會(huì)完成變量賦值,函數(shù)引用敬矩,以及執(zhí)行其他代碼概行。

什么是變量對(duì)象(VO, Variable Object)

變量對(duì)象的創(chuàng)建,依次經(jīng)歷了以下幾個(gè)過(guò)程弧岳。

  • 建立arguments對(duì)象凳忙。檢查當(dāng)前上下文中的參數(shù)业踏,建立該對(duì)象下的屬性與屬性值。
  • 檢查當(dāng)前上下文的函數(shù)聲明涧卵,也就是使用function關(guān)鍵字聲明的函數(shù)勤家。在變量對(duì)象中以函數(shù)名建立一個(gè)屬性,屬性值為指向該函數(shù)所在內(nèi)存地址的引用柳恐。如果函數(shù)名的屬性已經(jīng)存在伐脖,那么該屬性將會(huì)被新的引用所覆蓋。
  • 檢查當(dāng)前上下文中的變量聲明胎撤,每找到一個(gè)變量聲明晓殊,就在變量對(duì)象中以變量名建立一個(gè)屬性,屬性值為undefined伤提。如果該變量名的屬性已經(jīng)存在巫俺,為了防止同名的函數(shù)被修改為undefined,則會(huì)直接跳過(guò)肿男,原屬性值不會(huì)被修改介汹。

所以所謂的變量提升,就是在變量對(duì)象創(chuàng)建的過(guò)程中舶沛,首先把檢查var嘹承,function聲明的變量,在變量對(duì)象中分別創(chuàng)建以變量名如庭、函數(shù)名為屬性名的屬性叹卷,屬性值為undefined。并且根據(jù)變量對(duì)象創(chuàng)建的過(guò)程來(lái)看坪它,函數(shù)聲明的檢查是處在變量聲明的檢查之前的骤竹,所以function的提升會(huì)優(yōu)先于var的提升。

var往毡、function 與 let蒙揣、const
都知道var,function存在變量提升开瞭,而存在let懒震、const聲明的是塊級(jí)作用域,不存在變量提升嗤详,所以let和const會(huì)存在暫時(shí)性死區(qū)的情況个扰,即在變量聲明之前使用會(huì)拋出 "x is not defined" 的錯(cuò)誤。如下

let x = 'global'
{
  console.log(x) // Uncaught ReferenceError: x is not defined
  let x = 1
}

但是根據(jù)變量對(duì)象創(chuàng)建的過(guò)程葱色,在創(chuàng)建了argument對(duì)象后锨匆,就會(huì)檢查函數(shù)聲明和變量聲明,也就是說(shuō)let是會(huì)變量提升的。那么怎么理解塊級(jí)作用域里暫時(shí)性死區(qū)的存在呢恐锣?知乎專欄:我用了兩個(gè)月的時(shí)間才理解 let 給出了對(duì)于這個(gè)問(wèn)題的思考茅主。

對(duì)于let的變量創(chuàng)建、初始化和賦值過(guò)程土榴,可以理解為以下幾步:

{
  let x = 1
  x = 2
}
  • 找到所有用 let 聲明的變量诀姚,在環(huán)境中「創(chuàng)建」這些變量
  • 開(kāi)始執(zhí)行代碼(注意現(xiàn)在還沒(méi)有初始化)
  • 執(zhí)行 x = 1,將 x 「初始化」為 1(這并不是一次賦值玷禽,如果代碼是 let x赫段,就將 x 初始化為 undefined)
  • 執(zhí)行 x = 2,對(duì) x 進(jìn)行「賦值」

所以說(shuō)在初始化之前矢赁,變量是不能被訪問(wèn)的糯笙,所以一旦被訪問(wèn),就會(huì)拋出錯(cuò)誤撩银。而對(duì)于var聲明的變量给涕,創(chuàng)建過(guò)程中自動(dòng)初始化為undefined,所以是可以訪問(wèn)的额获,function聲明的函數(shù)够庙,在創(chuàng)建過(guò)程中自動(dòng)初始化為為指向函數(shù)的指針。對(duì)于const來(lái)說(shuō)抄邀,由于它是不能被賦值的耘眨,所以只存在創(chuàng)建和初始化兩個(gè)過(guò)程,與let相同的是在環(huán)境中創(chuàng)建了變量后沒(méi)有立即初始化境肾,而是等到函數(shù)執(zhí)行了以后進(jìn)行初始化剔难。

什么是活動(dòng)對(duì)象(AO, Active Object)
變量對(duì)象是在執(zhí)行上下文創(chuàng)建過(guò)程中創(chuàng)建的一個(gè)保存變量聲明的對(duì)象,未進(jìn)入執(zhí)行階段之前奥喻,變量對(duì)象中的屬性都不能訪問(wèn)偶宫,但是進(jìn)入執(zhí)行階段之后,變量對(duì)象轉(zhuǎn)變?yōu)榱嘶顒?dòng)對(duì)象(AO)衫嵌,里面的屬性都能被訪問(wèn)了读宙,然后開(kāi)始進(jìn)行執(zhí)行階段的操作彻秆。

VO和AO的區(qū)別來(lái)看楔绞,它們其實(shí)都是同一個(gè)對(duì)象,只是處于執(zhí)行上下文的不同生命周期唇兑。不過(guò)只有處于函數(shù)調(diào)用棧棧頂?shù)膱?zhí)行上下文中的變量對(duì)象酒朵,才會(huì)變成活動(dòng)對(duì)象。全局上下文中的變量對(duì)象扎附,就是window對(duì)象蔫耽,this也指向window。

五、作用域

在傳統(tǒng)編譯語(yǔ)言的流程中匙铡,程序中的一段源代碼在執(zhí)行之前會(huì)經(jīng)歷三個(gè)步驟图甜,統(tǒng)稱為“編譯”。

  • 分詞/詞法分析(Tokenizing/Lexing):這個(gè)過(guò)程會(huì)將由字符組成的字符串分解成(對(duì)編程語(yǔ)言來(lái)說(shuō))有意義的代碼塊鳖眼,這些代 碼塊被稱為詞法單元(token)黑毅。例如,考慮程序var a = 2;钦讳。這段程序通常會(huì)被分解成 為下面這些詞法單元:var矿瘦、a、=愿卒、2 缚去、;∏砜空格是否會(huì)被當(dāng)作詞法單元易结,取決于空格在 這門語(yǔ)言中是否具有意義。
  • 解析/語(yǔ)法分析(Parsing): 這個(gè)過(guò)程是將詞法單元流(數(shù)組)轉(zhuǎn)換成一個(gè)由元素逐級(jí)嵌套所組成的代表了程序語(yǔ)法結(jié)構(gòu)的樹(shù)稠通。這個(gè)樹(shù)被稱為“抽象語(yǔ)法樹(shù)”(Abstract Syntax Tree衬衬,AST)。
  • 代碼生成:將 AST 轉(zhuǎn)換為可執(zhí)行代碼的過(guò)程稱被稱為代碼生成改橘。這個(gè)過(guò)程與語(yǔ)言滋尉、目標(biāo)平臺(tái)等息息相關(guān)。

但是相比于上述的編譯語(yǔ)言來(lái)說(shuō)飞主,JS引擎要復(fù)雜得多狮惜。例如,在語(yǔ)法分析和代碼生成階段有特定的步驟來(lái)對(duì)運(yùn)行性能進(jìn)行優(yōu)化碌识,包括對(duì)冗余元素進(jìn)行優(yōu)化等碾篡。

什么是作用域

在JavaScript中,我們可以將作用域定義為一套規(guī)則筏餐,這套規(guī)則用來(lái)管理引擎如何在當(dāng)前作用域以及嵌套的子作用域中根據(jù)標(biāo)識(shí)符名稱進(jìn)行變量查找开泽。

JS所采用的的作用域模型是詞法作用域。在編譯步驟中魁瞪,首先進(jìn)行的是詞法分析穆律,詞法作用域就是定義在詞法階段的作用域,也就是說(shuō)导俘,詞法作用域是由在寫代碼時(shí)將變量和塊作用域?qū)懺谀睦飦?lái)決定的峦耘,因此當(dāng)詞法分析器處理代碼時(shí)會(huì)保持作用域不變(大部分情況下是這樣的)。

作用域與執(zhí)行上下文的區(qū)別

  • JavaScript代碼的整個(gè)執(zhí)行過(guò)程旅薄,分為兩個(gè)階段辅髓,代碼編譯階段與代碼執(zhí)行階段。編譯階段由編譯器完成,將代碼翻譯成可執(zhí)行代碼洛口,這個(gè)階段作用域規(guī)則會(huì)確定矫付。
  • 執(zhí)行階段由引擎完成,主要任務(wù)是執(zhí)行可執(zhí)行代碼第焰,執(zhí)行上下文在這個(gè)階段創(chuàng)建技即。

作用域鏈

作用域鏈,是由當(dāng)前環(huán)境與上層環(huán)境的一系列變量對(duì)象組成樟遣,它保證了當(dāng)前執(zhí)行環(huán)境對(duì)符合訪問(wèn)權(quán)限的變量和函數(shù)的有序訪問(wèn)而叼。作用域查找始終從運(yùn)行時(shí)所處的最內(nèi)部作用域開(kāi)始,逐級(jí)向外或者說(shuō)向上進(jìn)行豹悬,直到遇見(jiàn)第一個(gè)匹配的標(biāo)識(shí)符為止葵陵,此時(shí)作用域查找停止。這種作用域查找方式以鏈?zhǔn)匠尸F(xiàn)瞻佛,所以稱為作用域鏈脱篙。

六、閉包

閉包是一種特殊的對(duì)象伤柄。它由兩部分組成绊困。執(zhí)行上下文(代號(hào)A),以及在該執(zhí)行上下文中創(chuàng)建的函數(shù)(代號(hào)B)适刀。B執(zhí)行時(shí)秤朗,如果訪問(wèn)了A中變量對(duì)象中的值,那么閉包就會(huì)產(chǎn)生笔喉。一個(gè)例子:

var fn = null;
function foo() {
    var a = 2;
    function innnerFoo() {
        console.log(a);
    }
    fn = innnerFoo; // 將 innnerFoo的引用取视,賦值給全局變量中的fn
}
function bar() {
    fn(); // 此處的保留的innerFoo的引用
}
foo();
bar(); // 2

如果閉包不存在的話,也就是 innerFoo() 的引用沒(méi)有賦值給fn常挚,在 foo() 執(zhí)行后作谭,foo()的執(zhí)行上下文彈出函數(shù)調(diào)用棧,因?yàn)槠渲械淖兞亢秃瘮?shù)沒(méi)有被引用奄毡,就被垃圾回收器認(rèn)定為垃圾進(jìn)行銷毀折欠。但是在 foo() 中,全局執(zhí)行上下文中的fn獲取了 innerFoo() 的引用吼过,也就是說(shuō)锐秦,innerFoo() 仍然在執(zhí)行環(huán)境中,那么根據(jù)瀏覽器的垃圾回收機(jī)制那先,它就不會(huì)被當(dāng)做垃圾回收农猬。而 innerFoo() 中能夠訪問(wèn)的變量(包括它自己的作用域中的變量和它作用域鏈上的變量)同樣會(huì)被引用赡艰,所以也不會(huì)被回收售淡。所以,通過(guò)閉包,我們可以在其他的執(zhí)行上下文中揖闸,訪問(wèn)到函數(shù)的內(nèi)部變量揍堕。

雖然上述例子中的閉包被保存在了全局變量中,但是閉包的作用域鏈并不會(huì)發(fā)生任何改變汤纸。在閉包中衩茸,能訪問(wèn)到的變量,仍然是作用域鏈上能夠查詢到的變量贮泞。

var fn = null;
function foo() {
    var a = 2;
    function innnerFoo() {
        console.log(c); // 在這里楞慈,試圖訪問(wèn)函數(shù)bar中的c變量,會(huì)拋出錯(cuò)誤
        console.log(a);
    }
    fn = innnerFoo; // 將 innnerFoo的引用啃擦,賦值給全局變量中的fn
}
function bar() {
    var c = 100;
    fn(); // 此處的保留的innerFoo的引用
}
foo();
bar();

innerFoo() 的作用域鏈囊蓝,仍然是innerFoo -> foo -> window,這是在詞法分析的時(shí)候就已經(jīng)確定了令蛉,詞法作用域是靜態(tài)作用域聚霜。

七、this

在執(zhí)行上下文的創(chuàng)建階段珠叔,會(huì)分別生成變量對(duì)象蝎宇,建立作用域鏈,確定this指向祷安。其中變量對(duì)象與作用域鏈我們都已經(jīng)仔細(xì)總結(jié)過(guò)了姥芥,而這里的關(guān)鍵,就是確定this指向汇鞭。

this的指向撇眯,是在函數(shù)被調(diào)用的時(shí)候確定的。并且在函數(shù)執(zhí)行過(guò)程中虱咧,this一旦被確定熊榛,就不可更改了。如果調(diào)用者函數(shù)腕巡,被某一個(gè)對(duì)象所擁有玄坦,那么該函數(shù)在調(diào)用時(shí),內(nèi)部的this指向該對(duì)象绘沉。如果函數(shù)獨(dú)立調(diào)用煎楣,那么該函數(shù)內(nèi)部的this,則指向undefined车伞。但是在非嚴(yán)格模式中择懂,當(dāng)this指向undefined時(shí),它會(huì)被自動(dòng)指向全局對(duì)象

this有四種綁定方式:

  • 默認(rèn)綁定:當(dāng)作為普通函數(shù)調(diào)用時(shí)另玖,this指向全局對(duì)象window困曙,嚴(yán)格模式下this為undefined表伦。
  • 隱式綁定:當(dāng)作為對(duì)象的方法被調(diào)用時(shí),就會(huì)發(fā)生隱式綁定慷丽,this指向調(diào)用該方法的對(duì)象蹦哼。
'use strict'
var a = 20;
var obj = {
    a: 10,
    c: this.a + 20,
    fn: function () {
        return this.a;
    }
}

console.log(obj.c);   // 40
console.log(obj.fn());  //10

因?yàn)閱为?dú)的{}是不會(huì)形成新的作用域的,因此c: this.a + 20這里的this.a要糊,由于并沒(méi)有作用域的限制纲熏,所以它仍然處于全局作用域之中。所以這里的this其實(shí)是指向的window對(duì)象锄俄。而fn中的this由于存在調(diào)用函數(shù)的作用域局劲,this綁定的是obj對(duì)象,所以console.log(obj.fn());的結(jié)果是obj中的a奶赠。

另一個(gè)例子:

'use strict';
var a = 20;
function foo () {
    var a = 1;
    var obj = {
        a: 10,
        c: this.a + 20,
        fn: function () {
            return this.a;
        }
    }
    return obj.c;
}
console.log(foo());    // 報(bào)錯(cuò)
console.log(window.foo());  // 40

在嚴(yán)格模式下容握,函數(shù)獨(dú)立調(diào)用,那么該函數(shù)內(nèi)部的this车柠,則指向undefined剔氏。所以console.log(foo()) 會(huì)報(bào)錯(cuò);而console.log(window.foo()) 中竹祷,foo被window調(diào)用谈跛,所以this綁定的是全局變量 a=20 。如果把 "use strict" 去掉塑陵,則兩個(gè)log打印的都是40感憾。
另外需要注意的是,this只會(huì)綁定離它最近的對(duì)象令花,也就是調(diào)用該方法的對(duì)象阻桅。

var name = "freedom";
var obj = {
  name = "father",
  getName: function() {
      return this.name;
  },
  child = {
      name = "child",
      getName: function() {
          return this.name;
      }
  }
}
console.log(obj.getName());  //father
console.log(obj.child.getName());  //child
  • 顯式綁定:調(diào)用call(), apply(), bind()時(shí),就能指向this要綁定的對(duì)象兼都。顯示綁定的優(yōu)先級(jí)僅次于new綁定
  • 構(gòu)造函數(shù)綁定:當(dāng)用new運(yùn)算符來(lái)調(diào)用構(gòu)造函數(shù)時(shí)嫂沉,會(huì)創(chuàng)建一個(gè)新的對(duì)象,構(gòu)造函數(shù)中的this會(huì)與這個(gè)新的對(duì)象綁定在一起扮碧。

call趟章,apply,bind區(qū)別

  • 三者都是用于顯式綁定this
  • call 和 apply的區(qū)別是慎王,call傳入的是參數(shù)序列蚓土,而apply傳入的是參數(shù)數(shù)組:
    call(obj, ...args), apply(obj, args)
  • call 和 bind傳入的參數(shù)是一樣的,但是bind返回的是函數(shù)赖淤,需要調(diào)用:
    call(obj, ...args), bind(obj, ...args)() 蜀漆。所以一般bind可以用在回調(diào)函數(shù)綁定this中。

new運(yùn)算符做了些什么

var mynew = function(){
        // 獲取參數(shù)上下文的第一個(gè)參數(shù)
        var func = Array.prototype.shift.call(arguments);

        // 判斷第一個(gè)參數(shù)是否是函數(shù)咱旱,不是函數(shù)則拋出錯(cuò)誤
        if (typeof func !== 'function') {
            throw 'the first argument must be function';
        }

        // 創(chuàng)建一個(gè)對(duì)象确丢,綁定func原型
        var obj = Object.create(func.prototype);
        
        // 執(zhí)行構(gòu)造函數(shù),綁定this
        var ret = func.bind(obj,...arguments)();

        // 返回對(duì)象
        return (ret instanceof Object)? ret:obj;
    }

參考文獻(xiàn)

https://yangbo5207.github.io/wutongluo/
《你不知道的JS(上)》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末绷耍,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子蠕嫁,更是在濱河造成了極大的恐慌,老刑警劉巖毯盈,帶你破解...
    沈念sama閱讀 221,406評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件剃毒,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡搂赋,警方通過(guò)查閱死者的電腦和手機(jī)赘阀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,395評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)脑奠,“玉大人基公,你說(shuō)我怎么就攤上這事∷纹郏” “怎么了轰豆?”我有些...
    開(kāi)封第一講書人閱讀 167,815評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)齿诞。 經(jīng)常有香客問(wèn)我酸休,道長(zhǎng),這世上最難降的妖魔是什么祷杈? 我笑而不...
    開(kāi)封第一講書人閱讀 59,537評(píng)論 1 296
  • 正文 為了忘掉前任斑司,我火速辦了婚禮,結(jié)果婚禮上但汞,老公的妹妹穿的比我還像新娘宿刮。我一直安慰自己,他們只是感情好私蕾,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,536評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布僵缺。 她就那樣靜靜地躺著,像睡著了一般踩叭。 火紅的嫁衣襯著肌膚如雪谤饭。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 52,184評(píng)論 1 308
  • 那天懊纳,我揣著相機(jī)與錄音揉抵,去河邊找鬼。 笑死嗤疯,一個(gè)胖子當(dāng)著我的面吹牛冤今,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播茂缚,決...
    沈念sama閱讀 40,776評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼戏罢,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼屋谭!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起龟糕,我...
    開(kāi)封第一講書人閱讀 39,668評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤桐磁,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后讲岁,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體我擂,經(jīng)...
    沈念sama閱讀 46,212評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,299評(píng)論 3 340
  • 正文 我和宋清朗相戀三年缓艳,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了校摩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,438評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡阶淘,死狀恐怖衙吩,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情溪窒,我是刑警寧澤坤塞,帶...
    沈念sama閱讀 36,128評(píng)論 5 349
  • 正文 年R本政府宣布,位于F島的核電站澈蚌,受9級(jí)特大地震影響尺锚,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜惜浅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,807評(píng)論 3 333
  • 文/蒙蒙 一瘫辩、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧坛悉,春花似錦伐厌、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,279評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至轩猩,卻和暖如春卷扮,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背均践。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,395評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工晤锹, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人彤委。 一個(gè)月前我還...
    沈念sama閱讀 48,827評(píng)論 3 376
  • 正文 我出身青樓鞭铆,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親焦影。 傳聞我的和親對(duì)象是個(gè)殘疾皇子车遂,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,446評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容