<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
變數提升:JavaScript
程式碼執行過程中 JavaScript引擎把變數的宣告部分和函數的宣告部分提升到程式碼開頭的行為 (變數提升後以undefined
設為預設值)
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.將宣告的變數和函數移到了程式碼頂部 2.去除變數的var 宣告
JavaScript程式碼的執行流程:有些人認為 變數提升就是將宣告部分提升到了最前面的位置 其實這種說法是錯的 因為變數和函數宣告在程式碼中的位置是不會變的 之所以會變數提升是因為在編譯階段被JavaScript引擎放入記憶體中(換句話來說 js程式碼在執行前會先被JavaScript引擎編譯 然後才會進入執行階段)流程大致如下圖
那麼編譯階段究竟是如何做到變數提升的呢 接下來我們一起來看看 我們還是以上面的那段程式碼作為例子
function callName() { console.log('callName Done!') } var personName = undefined;
callName(); console.log(personName); personName = 'james'
執行圖如下
可以看到 結果編譯後 會在生成執行上下文和可執行程式碼兩部分內容
執行上下文:JavaScript程式碼執行時的執行環境(比如呼叫一個函數 就會進入這個函數的執行上下文 確定函數執行期間的this
、變數、物件等)在執行上下文中包含著變數環境(Viriable Environment)以及詞法環境(Lexicol Environment) 變數環境儲存著變數提升的內容 例如上面的myName
以及callName
那既然變數環境儲存著這些變數提升 那變數環境物件時怎麼生成的呢 我們還是用上面的程式碼來舉例子
callName(); function callName() { console.log('callName Done!'); } console.log(personName); var personName = 'james';
callName
的屬性 然後將該屬性指向堆中函數的位置var
定義 於是在變數環境中建立一個personName
的屬性 並使用undefined
初始化經過上面的步驟後 變數環境物件就生成了 現在已經有了執行上下文和可執行程式碼了 接下來就是程式碼執行階段了
總所周知 js執行程式碼是按照順序一行一行從上往下執行的 接下來還是使用上面的例子來分析
callName()
是 JavaScript引擎便在變數環境中尋找該函數 由於變數環境中存在該函數的參照 於是引擎變開始執行該函數 並輸出"callName Done!"
console.log(personName)
; 引擎在變數環境中找到personName
變數 但是這時候它的值是undefined
於是輸出undefined
var personName = 'james'
這一行 在變數環境中找到personName
並將其值改成james
以上便是一段程式碼的編譯和執行流程了 相信看到這裡你對JavaScript引擎是如何執行程式碼的應該有了更深的瞭解
Q:如果程式碼中出現了相同的變數或者函數怎麼辦?
A:首先是編譯階段 如果遇到同名變數或者函數 在變數環境中後面的同名變數或者函數會將之前的覆蓋掉 所以最後只會剩下一個定義
function func() { console.log('我是第一個定義的') } func(); function func() { console.log('我是將你覆蓋掉的') } func(); //輸出兩次"我是將你覆蓋掉的"
你在日常開發中有沒有遇到過這樣的報錯
根據報錯我們可以知道是出現了棧溢位的問題 那什麼是棧溢位呢?為什麼會棧溢位呢?
Q1:什麼是棧呢?
A1:一種後進先出的資料結構佇列
Q2:什麼是呼叫棧?
A2:程式碼中通常會有很多函數 也有函數中呼叫另一個函數的情況 呼叫棧就是用來管理呼叫關係的一種資料結構
當我們在函數中呼叫另一個函數(如呼叫自身的遞迴)然後處理不當的話 就很容易產生棧溢位 比如下面這段程式碼
function stackOverflow(n) { if(n == 1) return 1; return stackOverflow(n - 2); } stackOverflow(10000);//棧溢位
既然知道了什麼是呼叫棧和棧溢位 那程式碼執行過程中呼叫棧又是如何工作的呢?我們用下面這個例子來舉例
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
中呼叫了findName
函數 那麼呼叫棧是怎麼變化的
第一步:建立全域性上下文 並將其壓入棧底
接下來開始執行personName = 'james'
的操作 將變數環境中的personName
設定為james
第二步:執行findOneDetail
函數 這個時候JavaScript
會為其建立一個執行上下文 最後將其函數的執行上下文壓入棧中
接下來執行完tel = ‘110'
後 將變數環境中的tel
設定為110
第三步:當執行detail = findName()
時 會為findName
建立執行上下文並壓入棧中
接下來執行完findName
函數後 將其執行上下文彈出呼叫棧 接下來再彈出findOneDetail
的執行上下文以及全域性執行上下文 至此整個JavaScript
的執行流程結束
所以呼叫棧是JavaScript引擎追蹤函數執行的一個機制 當一次有多個函數被呼叫時 通過呼叫棧就能追蹤到哪個函數正在被執行以及各函數之間的呼叫關係
點選source並打上斷點重新整理後就可以再Call Stack查到呼叫棧的資訊(也可以通過程式碼中輸入console.track()
檢視)
當我們在寫遞迴的時候 很容易發生棧溢位 可以通過尾呼叫優化來避免棧溢位
作用域是指在程式中定義變數的區域,該位置決定了變數的生命週期。通俗地理解,作用域就是變數與函數的可存取範圍,即作用域控制著變數和函數的可見性和生命週期
我們都知道 使用var會產生變數提升 而變數提升會引發很多問題 比如變數覆蓋 本應被銷燬的變數依舊存在等等問題 而ES6引入了let 和const兩種宣告方式 讓js有了塊級作用域 那let和const時如何實現塊級作用域的呢 其實很簡單 原來還是從理解執行上下文開始
我們都知道 JavaScript引擎在編譯階段 會將使用var定義的變數以及function定義的函數宣告在對應的執行上下文中的變數環境中建立對應的屬性 當時我們發現執行上下文中還有一個詞法環境物件沒有用到 其實 詞法環境物件便是關鍵之處 我們還是通過舉例子來說明一下
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()
這時候函數的執行上下文就如下圖所示:
可以看到 當進入函數的作用域塊是 作用域塊中通過let宣告的變數 會被放到詞法環境中的一個單獨的區域中 這個區域並不郵箱作用域塊外面的變數 (比如宣告了b = undefined
但是不影響外面的b = 2
)
其實 在詞法作用域內部 維護了一個小型的棧結構 棧底是函數最外層的變數 進入一個作用域塊後 便會將過海作用域內部耳朵變數壓到棧頂 當作用域執行完之後 就會彈出(通過let
和const
宣告的變數)
當作用域塊執行完之後 其內部定義的變數就會從詞法作用域的棧頂彈出
塊級作用域就是通過詞法環境的棧結構來實現的 而變數提升是通過變數環境來實現 通過這兩者的結合 JavaScript引擎也就同時支援了變數提升和塊級作用域了。
在開始作用域鏈和閉包的學習之前 我們先來看下這部分程式碼
function callName() { console.log(personName); } function findName() { var personName = 'james'; callName(); } var personName = 'curry'; findName();//curry //你是否以為輸出james 猜想callName不是在findName中呼叫的嗎 那findName中已經定義了personName = 'james' 那為什麼是輸出外面的curry呢 這其實是和作用域鏈有關的
在每個執行上下文的變數環境中 都包含了一個外部參照 用來執行外部的執行上下文 稱之為outer
當程式碼使用一個變數時 會先從當前執行上下文中尋找該變數 如果找不到 就會向outer指向的執行上下文查詢
可以看到callName
和findName
的outer都是指向全域性上下文的 所以當在callName
中找不到personName
的時候 會去全域性找 而不是呼叫callName
的findName
中找 所以輸出的是curry
而不是james
作用域鏈是由詞法作用域決定的
詞法作用域就是指作用域是由程式碼中函數宣告的位置來決定的 所以詞法作用域是靜態的作用域 通過它就能夠預測程式碼在執行過程中如何查詢表示符
所以詞法作用域是程式碼階段就決定好的 和函數怎麼呼叫的沒有關係
我們來看下下面這個例子
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
定義的 就會儲存在詞法環境中 所以尋找也是從該執行上下文的詞法環境找 如果找不到 就去變數環境 還是找不到則去outer指向的執行上下文尋找 如下圖
JavaScript 中 根據詞法作用域的規則 內部函數總是可以存取其外部函數中宣告的變數 當通過呼叫一個外部函數返回一個內部函數後 即使該外部函數已經執行結束了 但是內部函數參照外部函數的變數依然儲存在記憶體中 我們就把這些變數的集合稱為閉包
舉個例子
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())
首先我們看看當執行到 foo
函數內部的return innerBar
這行程式碼時呼叫棧的情況 你可以參考下圖:
從上面的程式碼可以看出 innerBar
是一個物件 包含了 getName
和setName
的兩個方法 這兩個方法都是內部定義的 且都參照了函數內部的變數
根據詞法作用域的規則 getName
和setName
總是可以存取到外部函數foo
中的變數 所以當foo
執行結束時 getName
和setName
依然可以以後使用變數myName
和test
如下圖所示
可以看出 雖然foo
從棧頂彈出 但是變數依然存在記憶體中 這個時候 除了setName
和getName
其他任何地方都不能存取到這兩個變數 所以形成了閉包
那如何使用這些閉包呢 可以通過bar來使用 當呼叫了bar.seyName
時 如下圖
可以使用chrome的Clourse檢視閉包情況
通常 如果參照閉包的函數是一個全域性變數 那麼閉包會一直存在直到頁面關閉 但如果這個閉包以後不再使用的話 就會造成記憶體漏失
如果參照閉包的函數是各區域性變數 等函數銷燬後 在下次JavaScript引擎執行垃圾回收的時候 判斷閉包這塊內容不再被使用了 就會回收
所以在使用閉包的時候 請記住一個原則:如果該閉包一直使用 可以作為全域性變數而存在 如果使用頻率不高且佔記憶體 考慮改成區域性變數
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 //只需要確定好呼叫棧就好 呼叫了askName()後 返回的是per.callName 後續就和askName沒關係了(出棧) 所以結果就是呼叫了兩次per.callName 根據詞法作用域規則 結果都是james 也不會形成閉包
相信大家都有被this
折磨的時候 而this確實也是比較難理解和令人頭疼的問題 接下來我將從執行上下文的角度來分析JavaScript中的this
這裡先丟擲結論:this是和執行上下文繫結的 每個執行上下文都有一個this
接下來 我將帶大家一起理清全域性執行上下文的this
和函數執行上下文的this
、
全域性執行上下文的this
和作用域鏈的最底端一樣 都是指向window
物件
我們通過一個例子來看一下
function func() { console.log(this)//window物件 } func();
預設情況下呼叫一個函數 其執行上下文的this也是指向window物件
那如何改變執行上下文的this
值呢 可以通過apply call 和bind
實現 這裡講下如何使用call
來改變
let per = { name: 'james', address: 'Lakers' } function callName() { this.name = 'curry' } callName.call(per); console.log(per)//name: 'curry', address: 'Lakers'
可以看到這裡this
的指向已經改變了
var person = { name: 'james'; callName: function() { console.log(this.name) } } person.callName();//james
使用物件來呼叫其內部方法 該方法的this指向物件本身的
person.callName() === person.callName.call(person)
這個時候我們如果講物件賦給另一個全域性變數 this
又會怎樣變化呢
var person = { name: 'james'; callName: function() { this.name = 'curry'; console.log(this.name); } } var per1 = person;//this又指向window
this
指向全域性變數window
this
指向物件本身當使用new
關鍵字構建好了一個新的物件 建構函式的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.使用箭頭函數 () => { console.log(this)//箭頭函數不會建立其自身的執行上下文 所以箭頭函數中的this指向外部函數 }
在預設情況下呼叫一個函數 其指向上下文的this
預設就是指向全域性物件window
相信看到這裡 大家對於作用域 作用域鏈 執行上下文和this都有了更深的理解 筆者後期還會更新更多關於瀏覽器的原理和實踐 感興趣的小夥伴可以點波關注一起學習 文中錯誤之處請在評論區指出!
以上就是JS作用域作用鏈及this使用原理詳解的詳細內容,更多關於JS作用域作用鏈this的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45