欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

JS作用域作用鏈及this使用原理詳解

 更新時(shí)間:2022年08月10日 11:06:05   作者:35歲就退休的老狗  
這篇文章主要為大家介紹了JS作用域作用鏈及this使用原理詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

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

變量提升:JavaScript代碼執(zhí)行過(guò)程中 JavaScript引擎把變量的聲明部分和函數(shù)的聲明部分提升到代碼開頭的行為 (變量提升后以undefined設(shè)為默認(rèn)值)

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í)行流程:有些人認(rèn)為 變量提升就是將聲明部分提升到了最前面的位置 其實(shí)這種說(shuō)法是錯(cuò)的 因?yàn)樽兞亢秃瘮?shù)聲明在代碼中的位置是不會(huì)變的 之所以會(huì)變量提升是因?yàn)樵诰幾g階段被JavaScript引擎放入內(nèi)存中(換句話來(lái)說(shuō) js代碼在執(zhí)行前會(huì)先被JavaScript引擎編譯 然后才會(huì)進(jìn)入執(zhí)行階段)流程大致如下圖

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

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

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

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

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

執(zhí)行圖如下

可以看到 結(jié)果編譯后 會(huì)在生成執(zhí)行上下文和可執(zhí)行代碼兩部分內(nèi)容

執(zhí)行上下文:JavaScript代碼執(zhí)行時(shí)的運(yùn)行環(huán)境(比如調(diào)用一個(gè)函數(shù) 就會(huì)進(jìn)入這個(gè)函數(shù)的執(zhí)行上下文 確定函數(shù)執(zhí)行期間的this、變量、對(duì)象等)在執(zhí)行上下文中包含著變量環(huán)境(Viriable Environment)以及詞法環(huán)境(Lexicol Environment) 變量環(huán)境保存著變量提升的內(nèi)容 例如上面的myName 以及callName

那既然變量環(huán)境保存著這些變量提升 那變量環(huán)境對(duì)象時(shí)怎么生成的呢 我們還是用上面的代碼來(lái)舉例子

callName();
function callName() {
	console.log('callName Done!');
}
console.log(personName);
var personName = 'james';
  • 第一、三行不是變量聲明 JavaScript引擎不做任何處理
  • 第二行 發(fā)現(xiàn)了function定義的函數(shù) 將函數(shù)定義儲(chǔ)存在堆中 并在變量環(huán)境中創(chuàng)建一個(gè)callName的屬性 然后將該屬性指向堆中函數(shù)的位置
  • 第四行 發(fā)現(xiàn)var定義 于是在變量環(huán)境中創(chuàng)建一個(gè)personName的屬性 并使用undefined初始化

經(jīng)過(guò)上面的步驟后 變量環(huán)境對(duì)象就生成了 現(xiàn)在已經(jīng)有了執(zhí)行上下文和可執(zhí)行代碼了 接下來(lái)就是代碼執(zhí)行階段了

代碼執(zhí)行階段

總所周知 js執(zhí)行代碼是按照順序一行一行從上往下執(zhí)行的 接下來(lái)還是使用上面的例子來(lái)分析

  • 執(zhí)行到callName()是 JavaScript引擎便在變量環(huán)境中尋找該函數(shù) 由于變量環(huán)境中存在該函數(shù)的引用 于是引擎變開始執(zhí)行該函數(shù) 并輸出"callName Done!"
  • 接下來(lái)執(zhí)行到console.log(personName); 引擎在變量環(huán)境中找到personName變量 但是這時(shí)候它的值是undefined 于是輸出undefined
  • 接下來(lái)執(zhí)行到了var personName = 'james'這一行 在變量環(huán)境中找到personName 并將其值改成james

以上便是一段代碼的編譯和執(zhí)行流程了 相信看到這里你對(duì)JavaScript引擎是如何執(zhí)行代碼的應(yīng)該有了更深的了解

Q:如果代碼中出現(xiàn)了相同的變量或者函數(shù)怎么辦?

A:首先是編譯階段 如果遇到同名變量或者函數(shù) 在變量環(huán)境中后面的同名變量或者函數(shù)會(huì)將之前的覆蓋掉 所以最后只會(huì)剩下一個(gè)定義

function func() {
	console.log('我是第一個(gè)定義的')
}
func();
function func() {
	console.log('我是將你覆蓋掉的')
}
func();
//輸出兩次"我是將你覆蓋掉的"

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

你在日常開發(fā)中有沒(méi)有遇到過(guò)這樣的報(bào)錯(cuò)

根據(jù)報(bào)錯(cuò)我們可以知道是出現(xiàn)了棧溢出的問(wèn)題 那什么是棧溢出呢?為什么會(huì)棧溢出呢?

Q1:什么是棧呢?

A1:一種后進(jìn)先出的數(shù)據(jù)結(jié)構(gòu)隊(duì)列

Q2:什么是調(diào)用棧?

A2:代碼中通常會(huì)有很多函數(shù) 也有函數(shù)中調(diào)用另一個(gè)函數(shù)的情況 調(diào)用棧就是用來(lái)管理調(diào)用關(guān)系的一種數(shù)據(jù)結(jié)構(gòu)

當(dāng)我們?cè)诤瘮?shù)中調(diào)用另一個(gè)函數(shù)(如調(diào)用自身的遞歸)然后處理不當(dāng)?shù)脑?就很容易產(chǎn)生棧溢出 比如下面這段代碼

function stackOverflow(n) {
	if(n == 1) return 1;
	return stackOverflow(n - 2);
}
stackOverflow(10000);//棧溢出

既然知道了什么是調(diào)用棧和棧溢出 那代碼執(zhí)行過(guò)程中調(diào)用棧又是如何工作的呢?我們用下面這個(gè)例子來(lái)舉例

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')

可以看到 我們?cè)?code>findOneDetail中調(diào)用了findName函數(shù) 那么調(diào)用棧是怎么變化的

第一步:創(chuàng)建全局上下文 并將其壓入棧底

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

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

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

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

接下來(lái)執(zhí)行完findName函數(shù)后 將其執(zhí)行上下文彈出調(diào)用棧 接下來(lái)再?gòu)棾?code>findOneDetail的執(zhí)行上下文以及全局執(zhí)行上下文 至此整個(gè)JavaScript的執(zhí)行流程結(jié)束

所以調(diào)用棧是JavaScript引擎追蹤函數(shù)執(zhí)行的一個(gè)機(jī)制 當(dāng)一次有多個(gè)函數(shù)被調(diào)用時(shí) 通過(guò)調(diào)用棧就能追蹤到哪個(gè)函數(shù)正在被執(zhí)行以及各函數(shù)之間的調(diào)用關(guān)系

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

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

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

2.小心棧溢出

當(dāng)我們?cè)趯戇f歸的時(shí)候 很容易發(fā)生棧溢出 可以通過(guò)尾調(diào)用優(yōu)化來(lái)避免棧溢出

塊級(jí)作用域:var、let以及const

作用域

作用域是指在程序中定義變量的區(qū)域,該位置決定了變量的生命周期。通俗地理解,作用域就是變量與函數(shù)的可訪問(wèn)范圍,即作用域控制著變量和函數(shù)的可見(jiàn)性和生命周期

我們都知道 使用var會(huì)產(chǎn)生變量提升 而變量提升會(huì)引發(fā)很多問(wèn)題 比如變量覆蓋 本應(yīng)被銷毀的變量依舊存在等等問(wèn)題 而ES6引入了let 和const兩種聲明方式 讓js有了塊級(jí)作用域 那let和const時(shí)如何實(shí)現(xiàn)塊級(jí)作用域的呢 其實(shí)很簡(jiǎn)單 原來(lái)還是從理解執(zhí)行上下文開始

我們都知道 JavaScript引擎在編譯階段 會(huì)將使用var定義的變量以及function定義的函數(shù)聲明在對(duì)應(yīng)的執(zhí)行上下文中的變量環(huán)境中創(chuàng)建對(duì)應(yīng)的屬性 當(dāng)時(shí)我們發(fā)現(xiàn)執(zhí)行上下文中還有一個(gè)詞法環(huán)境對(duì)象沒(méi)有用到 其實(shí) 詞法環(huán)境對(duì)象便是關(guān)鍵之處 我們還是通過(guò)舉例子來(lái)說(shuō)明一下

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)部通過(guò)var聲明的變量 在編譯階段全都被存放到變量環(huán)境里面了
  • 通過(guò)let聲明的變量 在編譯階段會(huì)被存放到詞法環(huán)境(Lexical Environment)中
  • 在函數(shù)的作用域內(nèi)部 通過(guò)let聲明的變量并沒(méi)有被存放到詞法環(huán)境中
  • 接下來(lái) 第二步繼續(xù)執(zhí)行代碼 當(dāng)執(zhí)行到代碼塊里面時(shí) 變量環(huán)境中a的值已經(jīng)被設(shè)置成了1 詞法環(huán)境中b的值已經(jīng)被設(shè)置成了2

這時(shí)候函數(shù)的執(zhí)行上下文就如下圖所示:

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

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

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

小結(jié)

塊級(jí)作用域就是通過(guò)詞法環(huán)境的棧結(jié)構(gòu)來(lái)實(shí)現(xiàn)的 而變量提升是通過(guò)變量環(huán)境來(lái)實(shí)現(xiàn) 通過(guò)這兩者的結(jié)合 JavaScript引擎也就同時(shí)支持了變量提升和塊級(jí)作用域了。

作用域鏈和閉包

在開始作用域鏈和閉包的學(xué)習(xí)之前 我們先來(lái)看下這部分代碼

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呢 這其實(shí)是和作用域鏈有關(guān)的

在每個(gè)執(zhí)行上下文的變量環(huán)境中 都包含了一個(gè)外部引用 用來(lái)執(zhí)行外部的執(zhí)行上下文 稱之為outer

當(dāng)代碼使用一個(gè)變量時(shí) 會(huì)先從當(dāng)前執(zhí)行上下文中尋找該變量 如果找不到 就會(huì)向outer指向的執(zhí)行上下文查找

可以看到callNamefindName的outer都是指向全局上下文的 所以當(dāng)在callName中找不到personName的時(shí)候 會(huì)去全局找 而不是調(diào)用callNamefindName中找 所以輸出的是curry而不是james

作用域鏈?zhǔn)怯稍~法作用域決定的

詞法作用域就是指作用域是由代碼中函數(shù)聲明的位置來(lái)決定的 所以詞法作用域是靜態(tài)的作用域 通過(guò)它就能夠預(yù)測(cè)代碼在執(zhí)行過(guò)程中如何查找表示符

所以詞法作用域是代碼階段就決定好的 和函數(shù)怎么調(diào)用的沒(méi)有關(guān)系

塊級(jí)作用域中的變量查找

我們來(lái)看下下面這個(gè)例子

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 = " 極客時(shí)間 "
let myAge = 10
let test = 1
foo()

我們知道 如果是let或者const定義的 就會(huì)儲(chǔ)存在詞法環(huán)境中 所以尋找也是從該執(zhí)行上下文的詞法環(huán)境找 如果找不到 就去變量環(huán)境 還是找不到則去outer指向的執(zhí)行上下文尋找 如下圖

閉包

JavaScript 中 根據(jù)詞法作用域的規(guī)則 內(nèi)部函數(shù)總是可以訪問(wèn)其外部函數(shù)中聲明的變量 當(dāng)通過(guò)調(diào)用一個(gè)外部函數(shù)返回一個(gè)內(nèi)部函數(shù)后 即使該外部函數(shù)已經(jīng)執(zhí)行結(jié)束了 但是內(nèi)部函數(shù)引用外部函數(shù)的變量依然保存在內(nèi)存中 我們就把這些變量的集合稱為閉包

舉個(gè)例子

function foo() {
    var myName = " 極客時(shí)間 "
    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())

首先我們看看當(dāng)執(zhí)行到 foo 函數(shù)內(nèi)部的return innerBar這行代碼時(shí)調(diào)用棧的情況 你可以參考下圖:

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

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

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

那如何使用這些閉包呢 可以通過(guò)bar來(lái)使用 當(dāng)調(diào)用了bar.seyName時(shí) 如下圖

可以使用chrome的Clourse查看閉包情況

閉包怎么回收

通常 如果引用閉包的函數(shù)是一個(gè)全局變量 那么閉包會(huì)一直存在直到頁(yè)面關(guān)閉 但如果這個(gè)閉包以后不再使用的話 就會(huì)造成內(nèi)存泄漏

如果引用閉包的函數(shù)是各局部變量 等函數(shù)銷毀后 在下次JavaScript引擎執(zhí)行垃圾回收的時(shí)候 判斷閉包這塊內(nèi)容不再被使用了 就會(huì)回收

所以在使用閉包的時(shí)候 請(qǐng)記住一個(gè)原則:如果該閉包一直使用 可以作為全局變量而存在 如果使用頻率不高且占內(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沒(méi)關(guān)系了(出棧) 所以結(jié)果就是調(diào)用了兩次per.callName 根據(jù)詞法作用域規(guī)則 結(jié)果都是james 也不會(huì)形成閉包

this:從執(zhí)行上下文分析this

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

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

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

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

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

我們通過(guò)一個(gè)例子來(lái)看一下

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

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

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

1.通過(guò)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.通過(guò)對(duì)象調(diào)用

var person = {
	name: 'james';
	callName: function() {
		console.log(this.name)
	}
}
person.callName();//james 

使用對(duì)象來(lái)調(diào)用其內(nèi)部方法 該方法的this指向?qū)ο蟊旧淼?/p>

person.callName() === person.callName.call(person)

這個(gè)時(shí)候我們?nèi)绻v對(duì)象賦給另一個(gè)全局變量 this又會(huì)怎樣變化呢

var person = {
	name: 'james';
	callName: function() {
		this.name = 'curry';
		console.log(this.name);
	}
}
var per1 = person;//this又指向window
  • 在全局環(huán)境中調(diào)用一個(gè)函數(shù) 函數(shù)內(nèi)部的this指向全局變量window
  • 通過(guò)一個(gè)對(duì)象調(diào)用內(nèi)部的方法 該方法的this指向?qū)ο蟊旧?/li>

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

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

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

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

var person = {
	name: 'james',
	callName: function() {
		console.log(this);//指向person
		function innerFunc() {
			console.log(this)//指向window
		}
		innerFunc()
	}
}
person.callName();
//如何解決
1.使用一個(gè)變量保存
let _this = this //保存指向person的this
2.使用箭頭函數(shù)
() => {
    console.log(this)//箭頭函數(shù)不會(huì)創(chuàng)建其自身的執(zhí)行上下文 所以箭頭函數(shù)中的this指向外部函數(shù)
}

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

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

總結(jié)

相信看到這里 大家對(duì)于作用域 作用域鏈 執(zhí)行上下文和this都有了更深的理解 筆者后期還會(huì)更新更多關(guān)于瀏覽器的原理和實(shí)踐 感興趣的小伙伴可以點(diǎn)波關(guān)注一起學(xué)習(xí) 文中錯(cuò)誤之處請(qǐng)?jiān)谠u(píng)論區(qū)指出!

以上就是JS作用域作用鏈及this使用原理詳解的詳細(xì)內(nèi)容,更多關(guān)于JS作用域作用鏈this的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評(píng)論