溫馨提示×

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

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

詳解JavaScript作用域

發(fā)布時(shí)間:2021-11-06 14:05:59 來(lái)源:億速云 閱讀:150 作者:iii 欄目:web開(kāi)發(fā)

這篇文章主要介紹“詳解JavaScript作用域”,在日常操作中,相信很多人在詳解JavaScript作用域問(wèn)題上存在疑惑,小編查閱了各式資料,整理出簡(jiǎn)單好用的操作方法,希望對(duì)大家解答”詳解JavaScript作用域”的疑惑有所幫助!接下來(lái),請(qǐng)跟著小編一起來(lái)學(xué)習(xí)吧!

JavaScript編譯過(guò)程

在學(xué)習(xí)作用域之前先簡(jiǎn)單了解一下JavaScript的編譯、執(zhí)行過(guò)程。

JavaScript被稱之為解釋性語(yǔ)言,與Java等這類編譯語(yǔ)言區(qū)別在于:JavaScript代碼寫(xiě)好了就可以直接立即執(zhí)行,Java則需要相對(duì)較長(zhǎng)時(shí)間的編譯過(guò)程才可生成可執(zhí)行的機(jī)器碼。

但其實(shí)JavaScript也是有編譯過(guò)程的,JavaScript使用的是一種即時(shí)編譯的方式(JIT)。 JIT會(huì)把JavaScript代碼中會(huì)多次運(yùn)行的代碼給編譯器編譯,生成編譯后的代碼并保存起來(lái),在下次使用時(shí)使用編譯好的代碼。這其實(shí)是JavaScript運(yùn)行環(huán)境采用的一種優(yōu)化解決方案。 如果不這么做,大量重復(fù)的代碼都會(huì)在運(yùn)行前重復(fù)編譯,這將極大的影響性能與運(yùn)行效率。

JavaScript引擎也會(huì)對(duì)JavaScript代碼在運(yùn)行前進(jìn)去預(yù)編譯,在預(yù)編譯的過(guò)程中會(huì)定義一套規(guī)則用來(lái)存儲(chǔ)變量,對(duì)象,函數(shù)等,方便在之后的運(yùn)行調(diào)用。這套規(guī)則就是作用域。

JavaScript引擎在編譯過(guò)種中要對(duì)代碼進(jìn)行詞法分析、語(yǔ)法分析、代碼生成、性能優(yōu)化等等一系列工作。JIT就是這一過(guò)程中用來(lái)優(yōu)化的一部分。

var a = 1;

var a = 1; 這行代碼在運(yùn)行前編譯器都會(huì)做哪些事情?

編譯器會(huì)把這行代碼分成 var a 和 a = 1 ,兩個(gè)部分。

  1. 首先編譯器會(huì)在相同作用域內(nèi)查詢是否已經(jīng)存在一個(gè)叫 a 的變量,如是存在,編譯器會(huì)忽略聲明a,繼續(xù)下一步編譯;如果不存在,則在當(dāng)前作用域聲明一個(gè)變量,命名為a。

  2. 然后編譯器會(huì)為引擎生成運(yùn)行時(shí)的代碼,這些代碼中包含處理a = 1的部分,引擎在處理a = 1的時(shí)候,同樣也會(huì)查詢作用域中是否存在a變量(會(huì)逐級(jí)向上一個(gè)作用域查找), 存在則賦值為2,不存在則拋出異常(嚴(yán)格模式下,如非嚴(yán)格模式則會(huì)隱式創(chuàng)建一個(gè)全局變量aLHS)。

LHS查詢 & RHS查詢

LHS 和 RHS 的含義是“賦值操作的左側(cè)與右側(cè)”,不過(guò)要注意并不單指“=”和左側(cè)與右側(cè)。 賦值操作還有其它的形式,因此可以理解為:LHS-賦值操作的目標(biāo)是誰(shuí)? RHS-誰(shuí)是賦值操作的源頭。

a = 1; 是對(duì)a LHS查詢,a是賦值操作的目標(biāo),為a賦值為1. 如LHS查詢失敗,非嚴(yán)格模式下會(huì)隱式創(chuàng)建一個(gè)全局變量,嚴(yán)格模式下會(huì)拋出ReferenceError: a is not defined;

console.log(a) 是對(duì)a RHS查詢,a是賦值的源頭;如果在作用域鏈中沒(méi)有查詢到a,同樣也會(huì)拋出ReferenceError: a is not defined;

作用域鏈

作用域是存儲(chǔ)變量的一套規(guī)則,當(dāng)代碼運(yùn)行時(shí)可能并不只是在一個(gè)作用域查詢變量。 當(dāng)一個(gè)作用域中包含另一個(gè)作用域的時(shí)候,就會(huì)存在作用域嵌套的情況。所以當(dāng)內(nèi)部的作用域無(wú)法找到某個(gè)變量的時(shí)候,引擎會(huì)在當(dāng)前作用域的外層嵌套中繼續(xù)查詢;直到查到變量或者達(dá)到最外層的作用域?yàn)橹埂_@就是作用域鏈接。

var name = "rewa"; 
function sayHi(){
    console.log("hello,"+name);
}
sayHi(); // hello,rewa

如上述代碼,sayHi函數(shù)作用域中并沒(méi)有變量name;卻能正常引用。就是因?yàn)橐嬖谏弦粚幼饔糜蛘业讲⑹褂昧俗兞?code>name;

var name = "rewa"; 
function sayHi(){
    var name = "fang"; // 添加的代碼
    console.log("hello,"+name);
}
sayHi(); // hello,fang

當(dāng)sayHi作用域中已經(jīng)找到變量name時(shí),引擎會(huì)停止向上層作用域查找,這叫作“遮蔽效應(yīng)”,內(nèi)部變量遮蔽外部作用域變量。

詞法作用域

作用域有兩種主要的工作模型。一種是最為最為普遍的,被大多數(shù)編程語(yǔ)言采用的詞法作用域; 還有一種叫動(dòng)態(tài)作用域

詞法作用域就是在寫(xiě)代碼時(shí)將變量和塊作用域?qū)懺谀睦镒饔糜蚓驮谀睦?,定義在詞法階段的作用域。JavaScript就是采用的詞法作用域。

詞法:就是組成代碼塊的字符串。比如:

var a = 1;

這行代碼中,var、a、=、2; 還有這中間的空格 都是詞法單元。

編譯器的第一個(gè)工作就是詞法化,會(huì)把代碼分解成一個(gè)一個(gè)詞法單元;具體編譯器在詞法化階段都做了哪些工作遵守哪些規(guī)則,根據(jù)不同編程語(yǔ)言而不同。JavaScript是怎么樣的規(guī)則我特么也不清楚,等我研究清楚了;再來(lái)做一個(gè)筆記。

簡(jiǎn)單的說(shuō),詞法作用域就是你寫(xiě)代碼的時(shí)候,把變量a寫(xiě)在函數(shù)b中,那么編譯器編譯時(shí)b的作用域中就會(huì)包含有a變量,編譯器會(huì)保持詞法作用域不變。(也會(huì)有特殊情況)

如下代碼:

var a = 1;
function foo(){
    var b = a + 2;
    function bar(){
        var c = b + 3;
        console.log(a,b,c)
    }
    bar();
}
foo(); // 1,3,6

這段代碼編譯后的作用域與你編寫(xiě)時(shí)的詞法作用域是一致的。

全局作用域: 變量a, 函數(shù) foo

函數(shù)foo()創(chuàng)建的作用域:變量b,函數(shù)bar

函數(shù)bar()創(chuàng)建的作用域:變量c

代碼寫(xiě)在哪作用域就在哪。

了解詞法作用域需要注意以下幾點(diǎn):

  • 無(wú)論函數(shù)在哪里被調(diào)用,如何被調(diào)用,函數(shù)的詞法作用域都只由函數(shù)被聲明時(shí)所處的位置決定。

  • 詞法作用域查詢只會(huì)查找一級(jí)標(biāo)識(shí)符,比如上述代碼中的變量a,b,c。如果訪問(wèn)foo.bar.baz,詞法作用域只會(huì)查詢foo。找到這個(gè)變量后,再訪問(wèn)屬性bar,再到baz

  • 存在使詞法作用域編譯后不一致的方法,但會(huì)導(dǎo)致性能下降。

修改詞法作用域的方法 eval & with (千萬(wàn)不要這么做)
eval

代碼如下:

var a = 1;
function foo(str){
    eval(str);
    console.log(a);
}
foo('var a = 2;'); // 2

var a = 1; 會(huì)在函數(shù)foo中運(yùn)行,變量a將包含作用域。 eval(...)函數(shù)接受一個(gè)字符串,并將字符串當(dāng)作代碼運(yùn)行;就相當(dāng)于把代碼寫(xiě)在這個(gè)位置。

eval在嚴(yán)格模式下會(huì)拋出異常:

var a = 1;
function foo(str){
    "use strict"
    eval(str);
    console.log(a); // ReferenceError: a is not defined
}
foo('var a = 2;');

默認(rèn)情況下,如果eval()中有包含聲明,就會(huì)對(duì)所處的詞法作用域進(jìn)行修改;在嚴(yán)格模式下,eval()在運(yùn)行時(shí)有其自己的詞法作用域,那么將無(wú)法修改所在的作用域,如上述代碼。

with
var obj = {
    a:1,
    b:2,
    c:3
}
obj.a = 11;
obj.b = 22;
obj.c = 33;
// with 也可以達(dá)到同樣的效果
with(obj){
    a=111;
    b=222;
    c=333;
}
//這樣 obj 被修改為:
{   
    a:111,
    b:222,
    c:333
}

with()接受一個(gè)參數(shù),在這里是obj;此時(shí)with中作用域是obj, 可以訪問(wèn)obj中的屬性。 這種方式賦值就變得簡(jiǎn)潔很多。

with可以為一個(gè)對(duì)象創(chuàng)建一個(gè)作用域,對(duì)象的屬性會(huì)定義為這個(gè)作用域中的變量;不過(guò)with中的通過(guò)var聲明的變量并不會(huì)成為這個(gè)作用域的成員,而是被聲明到with所在的作用域中。這不正常了,代碼使用with會(huì)變得很不容易控制。比如:

with(obj){
    a=111;
    b=222;
    c=333;
    d=444;
}
console.log(obj.d); // undefined
console.log(d); // 444

原來(lái)以為會(huì)添加在obj中的屬性d,卻被添加到了全局作用域中;這就可能與開(kāi)發(fā)編寫(xiě)時(shí)的預(yù)期結(jié)果不符;也不符合詞法作用域的規(guī)則。

所以evalwith都已經(jīng)被禁止了,也不推薦使用。

這種不可預(yù)估詞法作用域的特性,也帶了一個(gè)嚴(yán)重的性能問(wèn)題。 JavaScript引擎在編譯階段會(huì)進(jìn)行性能優(yōu)化。其中有一些優(yōu)化依賴代碼的詞法,對(duì)詞法進(jìn)行靜態(tài)分析,并預(yù)先確定所有變量與函數(shù)的定義位置,才能在執(zhí)行過(guò)程中快速找到變量。

如果引擎在代碼中發(fā)現(xiàn)了evalwith,它無(wú)法在詞法分析階段明確知道eval(...)接生什么代碼;也無(wú)法知道傳遞給with用來(lái)創(chuàng)建新詞法作用域的對(duì)象內(nèi)容是什么。 那么優(yōu)化未知的代碼和詞法作用域是沒(méi)有意義的,引擎將放棄優(yōu)化這一部分。

如果在代碼中頻繁使用evalwith,程序運(yùn)行起來(lái)將會(huì)非常慢。

函數(shù)作用域

函數(shù)內(nèi)部的變量和函數(shù)定義都可以封裝起來(lái),外部無(wú)法訪問(wèn)封裝在函數(shù)內(nèi)部的變量標(biāo)識(shí)符。

如下代碼:

function foo(){
    var a = 1;
    function sayHi(){
        console.log('Hello!')
    }
}
console.log(a); // ReferenceError:a is not defined
sayHi(); //ReferenceError: sayHi is not defined

在函數(shù)外部訪問(wèn)其內(nèi)部的變量與函數(shù)會(huì)拋出異常。

這樣函數(shù)就可以行成一個(gè)相對(duì)獨(dú)立的作用域,可以用函數(shù)來(lái)封裝一個(gè)相對(duì)獨(dú)立的功能。 把業(yè)務(wù)代碼隱藏在函數(shù)內(nèi)部實(shí)現(xiàn),對(duì)外暴露接口;只要傳入不同的參數(shù)就可以輸入對(duì)應(yīng)的結(jié)果。 所以很多情況下函數(shù)可以用來(lái)模擬Java語(yǔ)言中類的實(shí)現(xiàn)。

例如:

function shoot(who,score){
    //這里面可以包含更多邏輯
    function one(){
        console.log(who + '罰籃命中!到得' +score+ '分!');
    }
    function dunk(){
        console.log(who + '扣籃,獲得' +score+ '分!');
    }
    function three(){
        console.log(who + '命中了一個(gè)' +score+ '分球!');
    }
    switch(score){
        case 1:
            one();
            break;
        case 2:
            dunk();
            break;
        case 3:
            three();
            break;
    }
}
shoot('Kobe',3); // Kobe投中了一個(gè)3分球!'
shoot('Lebron',2); // Lebron扣籃,獲得2分!' 
shoot('Shaq',1); // Shaq罰籃命中!到得1分!'

函數(shù)內(nèi)部隱藏變量與函數(shù)的定義可以避免污染全局命名空間;比如當(dāng)全局作用域中也有one dunk three 這些函數(shù),并且內(nèi)部實(shí)現(xiàn)不同;代碼邏輯就會(huì)混亂。 而在上面的代碼中,函數(shù)中定義的函數(shù)會(huì)遮蔽外部作用域的函數(shù)定義,只會(huì)調(diào)用到當(dāng)前函數(shù)作用域中的同名函數(shù)。

但是即使如此,大量的函數(shù)聲明同樣也會(huì)污染全局全名空間。 當(dāng)下流行的模塊化就是解決這一問(wèn)題的方案之一。不過(guò)在模塊化出來(lái)之前,大多數(shù)情況可以使用立即執(zhí)行函數(shù)(IIFE)來(lái)解決。 代碼如下:

(function(){
    var name = 'kobe';
    console.log(name);
})();

當(dāng)函數(shù)執(zhí)行結(jié)束后,name變量會(huì)被垃圾回收; 且不會(huì)與外部的任何作用域產(chǎn)生沖突,因?yàn)檎麄€(gè)函數(shù)都執(zhí)行在一個(gè)立即執(zhí)行函數(shù)中。它是一個(gè)塊作用域,且本身也沒(méi)有在作用域下創(chuàng)建任何標(biāo)識(shí)符。

立即執(zhí)行函數(shù)也可以接受參數(shù),用來(lái)函數(shù)內(nèi)部引用:

(function(name){
    console.log(name);
})('kobe');

JavaScript中除了函數(shù)作用域,還有其它塊作用域。比如with也是塊作用域;上面有過(guò)介紹 with 。 還有一個(gè)容易被忽略的塊作用域 try/catch 。

try{
    undefined(); //拋出異常
}
catch(err){
    console.log(err); // 正常執(zhí)行
}
console.log(err); //ReferenceError: err is not defined

err只能在catch中訪問(wèn),在外部的引用會(huì)拋出異常。

對(duì)于塊作用域,ES6中我們可以用let聲明實(shí)現(xiàn)這種需求。

if(true){
    let a = 1;
    console.log(a); //1
}
console.log(a); //ReferenceError: a is not defined

if(){} 并不是塊作用域,但上述代碼中let可以讓a變量成為僅if(){...}中的變量,外部不可訪問(wèn)。

這是不是像極了try/catch , 可letES6的標(biāo)準(zhǔn);在ES6之前實(shí)現(xiàn)類似塊作用域效果的方法可沒(méi)這么輕松。 現(xiàn)在一般我們?cè)诰帉?xiě)ES6代碼,想要運(yùn)行在所有瀏覽器上需要通過(guò)轉(zhuǎn)譯。而轉(zhuǎn)譯器也會(huì)把類似let的聲明,轉(zhuǎn)為 try/catch的形式。

{
    let a = 1;
    console.log(a); // 1
}
console.log(a); //ReferenceError: a is not defined

轉(zhuǎn)為:

try{
    throw 1;
}catch(a){
    console.log(a); //1
}
console.log(a); //ReferenceError: a is not defined

還有可能轉(zhuǎn)譯為:

{
    let _a = 1;  // 把{}中的 a 轉(zhuǎn)為_(kāi)a 
    console.log(_a); 
}
console.log(a);

到此,關(guān)于“詳解JavaScript作用域”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注億速云網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)?lái)更多實(shí)用的文章!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI