溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

JS作用域、作用鏈及this使用原理是什么

發(fā)布時間:2022-08-10 14:47:37 來源:億速云 閱讀:140 作者:iii 欄目:開發(fā)技術(shù)

這篇文章主要介紹了JS作用域、作用鏈及this使用原理是什么的相關(guān)知識,內(nèi)容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇JS作用域、作用鏈及this使用原理是什么文章都會有所收獲,下面我們一起來看看吧。

    變量提升的原理:JavaScript的執(zhí)行順序

    變量提升: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í)行階段)流程大致如下圖

    JS作用域、作用鏈及this使用原理是什么

    那么編譯階段究竟是如何做到變量提升的呢 接下來我們一起來看看 我們還是以上面的那段代碼作為例子

    第一部分:變量提升部分的代碼

    function callName() {
    	console.log('callName Done!')
    }
    var personName = undefined;

    第二部分:代碼執(zhí)行部分

    callName();
    console.log(personName);
    personName = 'james'

    執(zhí)行圖如下

    JS作用域、作用鏈及this使用原理是什么

    可以看到 結(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í)行階段了

    代碼執(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();
    //輸出兩次"我是將你覆蓋掉的"

    調(diào)用棧:棧溢出的原理

    你在日常開發(fā)中有沒有遇到過這樣的報錯

    JS作用域、作用鏈及this使用原理是什么

    根據(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)建全局上下文 并將其壓入棧底

    JS作用域、作用鏈及this使用原理是什么

    接下來開始執(zhí)行personName = 'james'的操作 將變量環(huán)境中的personName設(shè)置為james

    第二步:執(zhí)行findOneDetail函數(shù) 這個時候JavaScript會為其創(chuàng)建一個執(zhí)行上下文 最后將其函數(shù)的執(zhí)行上下文壓入棧中

    JS作用域、作用鏈及this使用原理是什么

    接下來執(zhí)行完tel = ‘110'后 將變量環(huán)境中的tel設(shè)置為110

    第三步:當執(zhí)行detail = findName()時 會為findName創(chuàng)建執(zhí)行上下文并壓入棧中

    JS作用域、作用鏈及this使用原理是什么

    接下來執(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)系

    如何利用調(diào)用棧

    1.使用瀏覽器查看調(diào)用棧的信息

    點擊source并打上斷點刷新后就可以再Call Stack查到調(diào)用棧的信息(也可以通過代碼中輸入console.track()查看)

    JS作用域、作用鏈及this使用原理是什么

    2.小心棧溢出

    當我們在寫遞歸的時候 很容易發(fā)生棧溢出 可以通過尾調(diào)用優(yōu)化來避免棧溢出

    塊級作用域:var、let以及const

    作用域

    作用域是指在程序中定義變量的區(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)建上下文

    JS作用域、作用鏈及this使用原理是什么

    • 函數(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í)行上下文就如下圖所示:

    JS作用域、作用鏈及this使用原理是什么

    可以看到 當進入函數(shù)的作用域塊是 作用域塊中通過let聲明的變量 會被放到詞法環(huán)境中的一個單獨的區(qū)域中 這個區(qū)域并不郵箱作用域塊外面的變量 (比如聲明了b = undefined 但是不影響外面的b = 2

    其實 在詞法作用域內(nèi)部 維護了一個小型的棧結(jié)構(gòu) 棧底是函數(shù)最外層的變量 進入一個作用域塊后 便會將過海作用域內(nèi)部耳朵變量壓到棧頂 當作用域執(zhí)行完之后 就會彈出(通過letconst聲明的變量)

    JS作用域、作用鏈及this使用原理是什么

    當作用域塊執(zhí)行完之后 其內(nèi)部定義的變量就會從詞法作用域的棧頂彈出

    JS作用域、作用鏈及this使用原理是什么

    小結(jié)

    塊級作用域就是通過詞法環(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í)行上下文查找

    JS作用域、作用鏈及this使用原理是什么

    可以看到callNamefindName的outer都是指向全局上下文的 所以當在callName中找不到personName的時候 會去全局找 而不是調(diào)用callNamefindName中找 所以輸出的是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í)行上下文尋找 如下圖

    JS作用域、作用鏈及this使用原理是什么

    閉包

    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)用棧的情況 你可以參考下圖:

    JS作用域、作用鏈及this使用原理是什么

    從上面的代碼可以看出 innerBar 是一個對象 包含了 getNamesetName的兩個方法 這兩個方法都是內(nèi)部定義的 且都引用了函數(shù)內(nèi)部的變量

    根據(jù)詞法作用域的規(guī)則 getNamesetName總是可以訪問到外部函數(shù)foo中的變量 所以當foo執(zhí)行結(jié)束時 getNamesetName依然可以以后使用變量myNametest 如下圖所示

    JS作用域、作用鏈及this使用原理是什么

    可以看出 雖然foo從棧頂彈出 但是變量依然存在內(nèi)存中 這個時候 除了setNamegetName 其他任何地方都不能訪問到這兩個變量 所以形成了閉包

    那如何使用這些閉包呢 可以通過bar來使用 當調(diào)用了bar.seyName時 如下圖

    JS作用域、作用鏈及this使用原理是什么

    可以使用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:從執(zhí)行上下文分析this

    相信大家都有被this折磨的時候 而this確實也是比較難理解和令人頭疼的問題 接下來我將從執(zhí)行上下文的角度來分析JavaScript中的this 這里先拋出結(jié)論:this是和執(zhí)行上下文綁定的 每個執(zhí)行上下文都有一個this

    接下來 我將帶大家一起理清全局執(zhí)行上下文的this和函數(shù)執(zhí)行上下文的this

    全局執(zhí)行上下文的this

    全局執(zhí)行上下文的this和作用域鏈的最底端一樣 都是指向window對象

    函數(shù)執(zhí)行上下文的this

    我們通過一個例子來看一下

    function func() {
    	console.log(this)//window對象
    }
    func();

    默認情況下調(diào)用一個函數(shù) 其執(zhí)行上下文的this也是指向window對象

    那如何改變執(zhí)行上下文的this值呢 可以通過apply call 和bind實現(xiàn) 這里講下如何使用call來改變

    1.通過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)改變了

    2.通過對象調(diào)用
    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>

    3.通過構(gòu)造函數(shù)設(shè)置

    當使用new關(guān)鍵字構(gòu)建好了一個新的對象 構(gòu)造函數(shù)的this其實就是對象本身

    this的缺陷以及應(yīng)對方案

    1.嵌套函數(shù)的this不會從外層函數(shù)中繼承

    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ù)
    }

    2.普通函數(shù)中的this指向全局對象window

    在默認情況下調(diào)用一個函數(shù) 其指向上下文的this默認就是指向全局對象window

    關(guān)于“JS作用域、作用鏈及this使用原理是什么”這篇文章的內(nèi)容就介紹到這里,感謝各位的閱讀!相信大家對“JS作用域、作用鏈及this使用原理是什么”知識都有一定的了解,大家如果還想學(xué)習(xí)更多知識,歡迎關(guān)注億速云行業(yè)資訊頻道。

    向AI問一下細節(jié)

    免責(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)容。

    js
    AI