您好,登錄后才能下訂單哦!
這篇文章主要介紹了JS作用域、作用鏈及this使用原理是什么的相關(guān)知識,內(nèi)容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇JS作用域、作用鏈及this使用原理是什么文章都會有所收獲,下面我們一起來看看吧。
變量提升:JavaScript
代碼執(zhí)行過程中 JavaScript引擎把變量的聲明部分和函數(shù)的聲明部分提升到代碼開頭的行為 (變量提升后以undefined
設(shè)為默認值)
callName(); function callName() { console.log('callName Done!'); } console.log(personName); var personName = 'james'; //變量提升后 類似以下代碼 function callName() { console.log('callName Done!'); }; var personName = undefined; callName();//callName已聲明 所以正常輸出calName Done! console.log(personName);//undefined personName = 'james'; //代碼所作改變: 1.將聲明的變量和函數(shù)移到了代碼頂部 2.去除變量的var 聲明
JavaScript代碼的執(zhí)行流程:有些人認為 變量提升就是將聲明部分提升到了最前面的位置 其實這種說法是錯的 因為變量和函數(shù)聲明在代碼中的位置是不會變的 之所以會變量提升是因為在編譯階段被JavaScript引擎放入內(nèi)存中(換句話來說 js代碼在執(zhí)行前會先被JavaScript引擎編譯 然后才會進入執(zhí)行階段)流程大致如下圖
那么編譯階段究竟是如何做到變量提升的呢 接下來我們一起來看看 我們還是以上面的那段代碼作為例子
function callName() { console.log('callName Done!') } var personName = undefined;
callName(); console.log(personName); personName = 'james'
執(zhí)行圖如下
可以看到 結(jié)果編譯后 會在生成執(zhí)行上下文和可執(zhí)行代碼兩部分內(nèi)容
執(zhí)行上下文:JavaScript代碼執(zhí)行時的運行環(huán)境(比如調(diào)用一個函數(shù) 就會進入這個函數(shù)的執(zhí)行上下文 確定函數(shù)執(zhí)行期間的this
、變量、對象等)在執(zhí)行上下文中包含著變量環(huán)境(Viriable Environment)以及詞法環(huán)境(Lexicol Environment) 變量環(huán)境保存著變量提升的內(nèi)容 例如上面的myName
以及callName
那既然變量環(huán)境保存著這些變量提升 那變量環(huán)境對象時怎么生成的呢 我們還是用上面的代碼來舉例子
callName(); function callName() { console.log('callName Done!'); } console.log(personName); var personName = 'james';
第一、三行不是變量聲明 JavaScript引擎不做任何處理
第二行 發(fā)現(xiàn)了function定義的函數(shù) 將函數(shù)定義儲存在堆中 并在變量環(huán)境中創(chuàng)建一個callName
的屬性 然后將該屬性指向堆中函數(shù)的位置
第四行 發(fā)現(xiàn)var
定義 于是在變量環(huán)境中創(chuàng)建一個personName
的屬性 并使用undefined
初始化
經(jīng)過上面的步驟后 變量環(huán)境對象就生成了 現(xiàn)在已經(jīng)有了執(zhí)行上下文和可執(zhí)行代碼了 接下來就是代碼執(zhí)行階段了
總所周知 js執(zhí)行代碼是按照順序一行一行從上往下執(zhí)行的 接下來還是使用上面的例子來分析
執(zhí)行到callName()
是 JavaScript引擎便在變量環(huán)境中尋找該函數(shù) 由于變量環(huán)境中存在該函數(shù)的引用 于是引擎變開始執(zhí)行該函數(shù) 并輸出"callName Done!"
接下來執(zhí)行到console.log(personName)
; 引擎在變量環(huán)境中找到personName
變量 但是這時候它的值是undefined
于是輸出undefined
接下來執(zhí)行到了var personName = 'james'
這一行 在變量環(huán)境中找到personName
并將其值改成james
以上便是一段代碼的編譯和執(zhí)行流程了 相信看到這里你對JavaScript引擎是如何執(zhí)行代碼的應(yīng)該有了更深的了解
Q:如果代碼中出現(xiàn)了相同的變量或者函數(shù)怎么辦?
A:首先是編譯階段 如果遇到同名變量或者函數(shù) 在變量環(huán)境中后面的同名變量或者函數(shù)會將之前的覆蓋掉 所以最后只會剩下一個定義
function func() { console.log('我是第一個定義的') } func(); function func() { console.log('我是將你覆蓋掉的') } func(); //輸出兩次"我是將你覆蓋掉的"
你在日常開發(fā)中有沒有遇到過這樣的報錯
根據(jù)報錯我們可以知道是出現(xiàn)了棧溢出的問題 那什么是棧溢出呢?為什么會棧溢出呢?
Q1:什么是棧呢?
A1:一種后進先出的數(shù)據(jù)結(jié)構(gòu)隊列
Q2:什么是調(diào)用棧?
A2:代碼中通常會有很多函數(shù) 也有函數(shù)中調(diào)用另一個函數(shù)的情況 調(diào)用棧就是用來管理調(diào)用關(guān)系的一種數(shù)據(jù)結(jié)構(gòu)
當我們在函數(shù)中調(diào)用另一個函數(shù)(如調(diào)用自身的遞歸)然后處理不當?shù)脑?就很容易產(chǎn)生棧溢出 比如下面這段代碼
function stackOverflow(n) { if(n == 1) return 1; return stackOverflow(n - 2); } stackOverflow(10000);//棧溢出
既然知道了什么是調(diào)用棧和棧溢出 那代碼執(zhí)行過程中調(diào)用棧又是如何工作的呢?我們用下面這個例子來舉例
var personName = 'james'; function findName(name, address) { return name + address; } function findOneDetail (name, adress) { var tel = '110'; detail = findName(name, address); return personName + detail + tel }; findOneDetail('james', 'Lakers')
可以看到 我們在findOneDetail
中調(diào)用了findName
函數(shù) 那么調(diào)用棧是怎么變化的
第一步:創(chuàng)建全局上下文 并將其壓入棧底
接下來開始執(zhí)行personName = 'james'
的操作 將變量環(huán)境中的personName
設(shè)置為james
第二步:執(zhí)行findOneDetail
函數(shù) 這個時候JavaScript
會為其創(chuàng)建一個執(zhí)行上下文 最后將其函數(shù)的執(zhí)行上下文壓入棧中
接下來執(zhí)行完tel = ‘110'
后 將變量環(huán)境中的tel
設(shè)置為110
第三步:當執(zhí)行detail = findName()
時 會為findName
創(chuàng)建執(zhí)行上下文并壓入棧中
接下來執(zhí)行完findName
函數(shù)后 將其執(zhí)行上下文彈出調(diào)用棧 接下來再彈出findOneDetail
的執(zhí)行上下文以及全局執(zhí)行上下文 至此整個JavaScript
的執(zhí)行流程結(jié)束
所以調(diào)用棧是JavaScript引擎追蹤函數(shù)執(zhí)行的一個機制 當一次有多個函數(shù)被調(diào)用時 通過調(diào)用棧就能追蹤到哪個函數(shù)正在被執(zhí)行以及各函數(shù)之間的調(diào)用關(guān)系
點擊source并打上斷點刷新后就可以再Call Stack查到調(diào)用棧的信息(也可以通過代碼中輸入console.track()
查看)
當我們在寫遞歸的時候 很容易發(fā)生棧溢出 可以通過尾調(diào)用優(yōu)化來避免棧溢出
作用域是指在程序中定義變量的區(qū)域,該位置決定了變量的生命周期。通俗地理解,作用域就是變量與函數(shù)的可訪問范圍,即作用域控制著變量和函數(shù)的可見性和生命周期
我們都知道 使用var會產(chǎn)生變量提升 而變量提升會引發(fā)很多問題 比如變量覆蓋 本應(yīng)被銷毀的變量依舊存在等等問題 而ES6引入了let 和const兩種聲明方式 讓js有了塊級作用域 那let和const時如何實現(xiàn)塊級作用域的呢 其實很簡單 原來還是從理解執(zhí)行上下文開始
我們都知道 JavaScript引擎在編譯階段 會將使用var定義的變量以及function定義的函數(shù)聲明在對應(yīng)的執(zhí)行上下文中的變量環(huán)境中創(chuàng)建對應(yīng)的屬性 當時我們發(fā)現(xiàn)執(zhí)行上下文中還有一個詞法環(huán)境對象沒有用到 其實 詞法環(huán)境對象便是關(guān)鍵之處 我們還是通過舉例子來說明一下
function foo(){ var a = 1 let b = 2 { let b = 3 var c = 4 let d = 5 console.log(a) console.log(b) } console.log(b) console.log(c) console.log(d) } foo()
第一步:執(zhí)行并創(chuàng)建上下文
函數(shù)內(nèi)部通過var聲明的變量 在編譯階段全都被存放到變量環(huán)境里面了
通過let聲明的變量 在編譯階段會被存放到詞法環(huán)境(Lexical Environment)中
在函數(shù)的作用域內(nèi)部 通過let聲明的變量并沒有被存放到詞法環(huán)境中
接下來 第二步繼續(xù)執(zhí)行代碼 當執(zhí)行到代碼塊里面時 變量環(huán)境中a的值已經(jīng)被設(shè)置成了1 詞法環(huán)境中b的值已經(jīng)被設(shè)置成了2
這時候函數(shù)的執(zhí)行上下文就如下圖所示:
可以看到 當進入函數(shù)的作用域塊是 作用域塊中通過let聲明的變量 會被放到詞法環(huán)境中的一個單獨的區(qū)域中 這個區(qū)域并不郵箱作用域塊外面的變量 (比如聲明了b = undefined
但是不影響外面的b = 2
)
其實 在詞法作用域內(nèi)部 維護了一個小型的棧結(jié)構(gòu) 棧底是函數(shù)最外層的變量 進入一個作用域塊后 便會將過海作用域內(nèi)部耳朵變量壓到棧頂 當作用域執(zhí)行完之后 就會彈出(通過let
和const
聲明的變量)
當作用域塊執(zhí)行完之后 其內(nèi)部定義的變量就會從詞法作用域的棧頂彈出
塊級作用域就是通過詞法環(huán)境的棧結(jié)構(gòu)來實現(xiàn)的 而變量提升是通過變量環(huán)境來實現(xiàn) 通過這兩者的結(jié)合 JavaScript引擎也就同時支持了變量提升和塊級作用域了。
在開始作用域鏈和閉包的學(xué)習(xí)之前 我們先來看下這部分代碼
function callName() { console.log(personName); } function findName() { var personName = 'james'; callName(); } var personName = 'curry'; findName();//curry //你是否以為輸出james 猜想callName不是在findName中調(diào)用的嗎 那findName中已經(jīng)定義了personName = 'james' 那為什么是輸出外面的curry呢 這其實是和作用域鏈有關(guān)的
在每個執(zhí)行上下文的變量環(huán)境中 都包含了一個外部引用 用來執(zhí)行外部的執(zhí)行上下文 稱之為outer
當代碼使用一個變量時 會先從當前執(zhí)行上下文中尋找該變量 如果找不到 就會向outer指向的執(zhí)行上下文查找
可以看到callName
和findName
的outer都是指向全局上下文的 所以當在callName
中找不到personName
的時候 會去全局找 而不是調(diào)用callName
的findName
中找 所以輸出的是curry
而不是james
作用域鏈是由詞法作用域決定的
詞法作用域就是指作用域是由代碼中函數(shù)聲明的位置來決定的 所以詞法作用域是靜態(tài)的作用域 通過它就能夠預(yù)測代碼在執(zhí)行過程中如何查找表示符
所以詞法作用域是代碼階段就決定好的 和函數(shù)怎么調(diào)用的沒有關(guān)系
我們來看下下面這個例子
function bar() { var myName = " 極客世界 " let test1 = 100 if (1) { let myName = "Chrome 瀏覽器 " console.log(test) } } function foo() { var myName = " 極客邦 " let test = 2 { let test = 3 bar() } } var myName = " 極客時間 " let myAge = 10 let test = 1 foo()
我們知道 如果是let
或者const
定義的 就會儲存在詞法環(huán)境中 所以尋找也是從該執(zhí)行上下文的詞法環(huán)境找 如果找不到 就去變量環(huán)境 還是找不到則去outer指向的執(zhí)行上下文尋找 如下圖
JavaScript 中 根據(jù)詞法作用域的規(guī)則 內(nèi)部函數(shù)總是可以訪問其外部函數(shù)中聲明的變量 當通過調(diào)用一個外部函數(shù)返回一個內(nèi)部函數(shù)后 即使該外部函數(shù)已經(jīng)執(zhí)行結(jié)束了 但是內(nèi)部函數(shù)引用外部函數(shù)的變量依然保存在內(nèi)存中 我們就把這些變量的集合稱為閉包
舉個例子
function foo() { var myName = " 極客時間 " let test1 = 1 const test2 = 2 var innerBar = { getName:function(){ console.log(test1) return myName }, setName:function(newName){ myName = newName } } return innerBar } var bar = foo() bar.setName(" 極客邦 ") bar.getName() console.log(bar.getName())
首先我們看看當執(zhí)行到 foo
函數(shù)內(nèi)部的return innerBar
這行代碼時調(diào)用棧的情況 你可以參考下圖:
從上面的代碼可以看出 innerBar
是一個對象 包含了 getName
和setName
的兩個方法 這兩個方法都是內(nèi)部定義的 且都引用了函數(shù)內(nèi)部的變量
根據(jù)詞法作用域的規(guī)則 getName
和setName
總是可以訪問到外部函數(shù)foo
中的變量 所以當foo
執(zhí)行結(jié)束時 getName
和setName
依然可以以后使用變量myName
和test
如下圖所示
可以看出 雖然foo
從棧頂彈出 但是變量依然存在內(nèi)存中 這個時候 除了setName
和getName
其他任何地方都不能訪問到這兩個變量 所以形成了閉包
那如何使用這些閉包呢 可以通過bar來使用 當調(diào)用了bar.seyName
時 如下圖
可以使用chrome的Clourse查看閉包情況
通常 如果引用閉包的函數(shù)是一個全局變量 那么閉包會一直存在直到頁面關(guān)閉 但如果這個閉包以后不再使用的話 就會造成內(nèi)存泄漏
如果引用閉包的函數(shù)是各局部變量 等函數(shù)銷毀后 在下次JavaScript引擎執(zhí)行垃圾回收的時候 判斷閉包這塊內(nèi)容不再被使用了 就會回收
所以在使用閉包的時候 請記住一個原則:如果該閉包一直使用 可以作為全局變量而存在 如果使用頻率不高且占內(nèi)存 考慮改成局部變量
var per = { name: 'curry'; callName: function() { console.log(name); } } function askName(){ let name = 'davic'; return per.callName } let name = 'james'; let _callName = askName() _callName(); per.callName(); //打印兩次james //只需要確定好調(diào)用棧就好 調(diào)用了askName()后 返回的是per.callName 后續(xù)就和askName沒關(guān)系了(出棧) 所以結(jié)果就是調(diào)用了兩次per.callName 根據(jù)詞法作用域規(guī)則 結(jié)果都是james 也不會形成閉包
相信大家都有被this
折磨的時候 而this確實也是比較難理解和令人頭疼的問題 接下來我將從執(zhí)行上下文的角度來分析JavaScript中的this
這里先拋出結(jié)論:this是和執(zhí)行上下文綁定的 每個執(zhí)行上下文都有一個this
接下來 我將帶大家一起理清全局執(zhí)行上下文的this
和函數(shù)執(zhí)行上下文的this
、
全局執(zhí)行上下文的this
和作用域鏈的最底端一樣 都是指向window
對象
我們通過一個例子來看一下
function func() { console.log(this)//window對象 } func();
默認情況下調(diào)用一個函數(shù) 其執(zhí)行上下文的this也是指向window對象
那如何改變執(zhí)行上下文的this
值呢 可以通過apply call 和bind
實現(xiàn) 這里講下如何使用call
來改變
let per = { name: 'james', address: 'Lakers' } function callName() { this.name = 'curry' } callName.call(per); console.log(per)//name: 'curry', address: 'Lakers'
可以看到這里this
的指向已經(jīng)改變了
var person = { name: 'james'; callName: function() { console.log(this.name) } } person.callName();//james
使用對象來調(diào)用其內(nèi)部方法 該方法的this指向?qū)ο蟊旧淼?/p>
person.callName() === person.callName.call(person)
這個時候我們?nèi)绻v對象賦給另一個全局變量 this
又會怎樣變化呢
var person = { name: 'james'; callName: function() { this.name = 'curry'; console.log(this.name); } } var per1 = person;//this又指向window
在全局環(huán)境中調(diào)用一個函數(shù) 函數(shù)內(nèi)部的this
指向全局變量window
通過一個對象調(diào)用內(nèi)部的方法 該方法的this
指向?qū)ο蟊旧?/p>
當使用new
關(guān)鍵字構(gòu)建好了一個新的對象 構(gòu)造函數(shù)的this
其實就是對象本身
var person = { name: 'james', callName: function() { console.log(this);//指向person function innerFunc() { console.log(this)//指向window } innerFunc() } } person.callName(); //如何解決 1.使用一個變量保存 let _this = this //保存指向person的this 2.使用箭頭函數(shù) () => { console.log(this)//箭頭函數(shù)不會創(chuàng)建其自身的執(zhí)行上下文 所以箭頭函數(shù)中的this指向外部函數(shù) }
在默認情況下調(diào)用一個函數(shù) 其指向上下文的this
默認就是指向全局對象window
關(guān)于“JS作用域、作用鏈及this使用原理是什么”這篇文章的內(nèi)容就介紹到這里,感謝各位的閱讀!相信大家對“JS作用域、作用鏈及this使用原理是什么”知識都有一定的了解,大家如果還想學(xué)習(xí)更多知識,歡迎關(guān)注億速云行業(yè)資訊頻道。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。