關于JS中的作用域中的問題思考分享
作用域
作用域,也就是我們常說的詞法作用域,說簡單點就是你的程序存放變量、變量值和函數的地方。根據作用范圍不同可以分為全局作用域和局部作用域,簡單說來就是,花括號 {}括起來的代碼共享一塊作用域,里面的變量都對內或者內部級聯的塊級作用域可見,這部分空間就是局部作用域,在 {}之外則是全局作用域。
全局作用域
在JavaScript中,作用域是基于函數來界定的。也就是說屬于一個函數內部的代碼,函數內部以及內部嵌套的代碼都可以訪問函數的變量。
function test(a){
var b = a * 2
function test2(c){
console.log(a ,b, c)
}
test2(b * 3)
}
test(4) // 4 8 24

我們不妨嘗試著來為這套代碼劃分一下作用域,上面定義了一個函數test,里面嵌套了函數 test2。圖中三個不同的顏色,對應三個不同的作用域:
- ①對應著全局
scope,這里只有test2 - ②是
test2界定的作用域,包含a、b、bar - ③是bar界定的作用域,這里只有c這個變量。
在查詢變量并作操作的時候,變量是從當前向外查詢的。就上圖來說,就是③用到了a會依次查詢③、②、①。由于在②里查到了a,因此不會繼續(xù)查①了。這個其實就是作用域鏈的查找方式,詳細內容我們后續(xù)介紹。
作用域中的錯誤
這里順便講講常見的兩種error, ReferenceError和 TypeError。如上圖,如果在test2里使用了d,那么經過查詢③、②、①都沒查到,那么就會報一個ReferenceError;

如果bar里使用了b,但是沒有正確引用,如b.abc(),這會導致TypeError

局部作用域
在局部作用域里面的變量通常是用到 with, let, const
with
對于with第一印象可能就是 with關鍵字的作用在于改變作用域,但并不代表這個關鍵字不好用,至少面試的時候大概率會可以被卷起來,如果你不常用的話。
with語句的原本用意是為逐級的對象訪問提供命名空間式的速寫方式,也就是說在指定的代碼區(qū)域,直接通過節(jié)點名稱調用對象。 with通常被當做重復引用同一個對象中的多個屬性的快捷方式,可以不需要重復引用對象本身。如下面代碼
var obj = {a: 2, b: 2, c: 2};
with (obj) {
a = 5;
b = 5;
c = 5;
}
console.log(obj) // {a: 5, b: 5, c: 5}
我們快速的創(chuàng)建了一個 obj對象,為了能快速改變obj的值我們可以通過 with的方式來進行修改,當然了,我們也可以通過逐行賦值的方式來進行,代碼不夠簡潔就是了。話說回來,在這段代碼中,我們使用了 with語句關聯了 obj對象,這就意味著在 with代碼塊內部,每個變量首先被認為是一個局部變量,如果局部變量與 obj對象的某個屬性同名,則這個局部變量會指向 obj對象屬性。
弊端
在上面的例子中,我們可以看到, with可以很好地幫助我們簡化代碼。但生產環(huán)境中卻很少見到,事實上并不是少見多怪,主要是不推薦使用,為啥嘞?原因如下:
- 數據泄露
- 性能下降
數據泄露
function test3(obj) {
with (obj) {
a = 2;
}
}
var o1 = {
a: 3
};
var o2 = {
b: 3
}
foo(o1);
console.log(o1.a)
foo(o2);
console.log(o2.a);
console.log(a);
在運行的過程中,我們可以看到,對于 o1.a, o2.a的回顯結果都不奇怪,畢竟對于 o1.a來說a是在作用域中定義的,而 o2.a壓根在o2中未定義,對于這個結果顯而易見,但為何 a的值會從未定義到已賦值之間的轉變呢?這個很危險的,畢竟這個時候已然出現數據泄露

首先,我們來分析上面的代碼。例子中創(chuàng)建了 o1和 o2兩個對象。其中一個有 a屬性,另外一個沒有。 test3(obj)函數接受一個 obj的形參,該參數是一個對象引用,并對該對象引用執(zhí)行了 with(obj){...}。在 with 塊內部,對 a有一個詞法引用,實際上是一個 LHS引用,將 2 賦值給了它。
當我們將 o1傳遞進去, a = 2賦值操作找到了 o1.a并將 2 賦值給它。而當 o2 傳遞進去,o2 并沒有 a 的屬性,因此不會創(chuàng)建這個屬性, o2.a保持 undefined。
但為什么對 o2的操作會導致數據的泄漏呢?
要回答這個問題則是需要了解 LHS查詢的機制,后面有機會我們再展開來分享,基于LHS查詢的原理分析,當我們傳遞 o2給 with時, with所聲明的作用域是 o2, 從這個作用域開始對 a 進行 LHS查詢,在 o2 的作用域、foo(…) 的作用域和全局作用域中都沒有找到標識符 a,因此在非嚴格模式下,會自動在全局作用域創(chuàng)建一個全局變量,在嚴格模式下,會拋出 ReferenceError異常。
性能下降
with 會在運行時修改或創(chuàng)建新的作用域,以此來欺騙其他在開發(fā)時定義的詞法作用域。with的使用可以令代碼更具有擴展性,雖然有數據泄漏的可能,但只要稍加注意就可以避免,除此之后,靈活運用難道不可以創(chuàng)造出很好地功能嗎?事實上真的不能,不妨我們考察一下性能特點
function test4() {
console.time("test4");
var obj = {
a: [1, 2, 3]
};
for(var i = 0; i < 100000; i++)
{
var v = obj.a[0];
}
console.timeEnd("test4");
}
test4();
function testWith() {
console.time("testWith");
var obj = {
a: [1, 2, 3]
};
with(obj) {
for(var i = 0; i < 100000; i++) {
var v = a[0];
}
}
console.timeEnd("testWith");
}
testWith();

在處理相同邏輯的代碼中,沒用 with的運行時間僅為 1.94 ms。而用 with的運用時間長達 44.13ms。
這是為什么呢?
原因是 JavaScript引擎會在編譯階段進行數項的性能優(yōu)化。其中有些優(yōu)化依賴于能夠根據代碼的詞法進行靜態(tài)分析,并預先確定所有變量和函數的定義位置,才能在執(zhí)行過程中快速找到標識符。
但如果引擎在代碼中發(fā)現了 with,它只能簡單地假設關于標識符位置的判斷都是無效的,因為無法知道傳遞給 with用來創(chuàng)建新詞法作用域的對象的內容到底是什么。此時引擎的所有的優(yōu)化努力大概率都是無意義的。因此引擎會采取最簡單的做法就是完全不做任何優(yōu)化。這種情況下,設想我們代碼大量使用 with或者 eval(),那么運行起來一定會變得非常慢。無論引擎多聰明,努力將這些悲觀情況的副作用限制在最小范圍內,也無法避免代碼會運行得更慢的事實。┑( ̄Д  ̄)┍
let
在局部作用域中,關鍵字let、const倒是很常見了,先說說說let,其是ES6新增的定義變量的方法,其定義的變量僅存在于最近的{}之內。
var test5 = true;
if (test5) {
let bar = test5 * 2;
console.log( bar );
}
console.log( bar ); // ReferenceError

const
與let一樣,唯一不同的是const定義的變量值不能修改
var test6 = true;
if (test6) {
var a = 2;
const b = 3;
a = 3;
b = 4;
}
console.log( a );
console.log( b );
對于a來說是全局變量,而對于b的作用范圍僅僅是存在與 if的塊內,此外從嘗試對b進行修改的時候也會出錯,提示不能對其進行修改

作用域鏈
在局部作用中,引用一個變量后,系統(tǒng)會自動在當前作用域中尋找var的聲明語句,如果找到則直接使用,否則繼續(xù)向上一級作用域中去尋找var的聲明語句,如未找到,則繼續(xù)向上級作用域中尋找…直到全局作用域中如還未找到var的聲明語句則自動在全局作用域中聲明該變量。我們把這種鏈式的查詢關系就稱之為"作用域鏈"。這個尋找的過程也是可以在局部作用域中可以引用全局變量的答案

代碼中的 testInner2函數中沒有對變量a進行賦值操作,因此由內到外一層層尋找,發(fā)現在 testInner中有 var a的賦值操作,由此返回a的賦值,有興趣的讀者不妨把 testInner里面的賦值操作去掉,可以發(fā)現函數運行返回 a的賦值是 yerik。
其實作用域鏈本質是一個對象列表,其保證了變量對象可以有序的訪問。其開始的地方是當前代碼執(zhí)行環(huán)境的變量對象,常被稱之為“活躍對象”(AO),變量的查找會從第一個鏈的對象開始,如果對象中包含變量屬性,那么就停止查找,如果沒有就會繼續(xù)向上級作用域查找,直到找到全局對象中,如果找不到就會報 ReferenceError。
閉包
簡單的說就是一個函數內嵌套另一個函數,這就會形成一個閉包。請牢記這句話:“無論函數是在哪里調用,也無論函數是如何調用的,其確定的詞法作用域永遠都是在函數被聲明的時候確定下來的”
function test7() {
var a = 2;
function test8() {
console.log( a ); // 2
}
test8();
}
test7();
我們看到上面的函數 test7里嵌套了 test8,這樣 test8就形成了一個閉包。在 test8內可以訪問到任何屬于 test7的作用域內的變量。
function test7() {
var a = 2;
function test8() {
console.log( a ); // 2
}
return test8;
}
var test9 = test7();
test9(); // 2
在第8行,我們執(zhí)行完 test7()后按理說垃圾回收器會釋放test7的詞法作用域里的變量,然而沒有,當我們運行 test9()的時候依然訪問到了 test7中a的值。這是因為,雖然 test7()執(zhí)行完了,但是其返回了 test8并賦給了 test9, test8依然保持著對 test7形成的作用域的引用。這就是依然可以訪問到 test7中a的值的原因。再想想,“無論函數是在哪里調用,也無論函數是如何調用的,其確定的詞法作用域永遠都是在函數被聲明的時候確定下來的”。
我們再來看另一個例子
function createClosure(){
var name = "yerik";
return {
setStr:function(){
name = "naug";
},
getStr:function(){
return name + ":hello";
}
}
}
var builder = new createClosure();
builder.setStr();
console.log(builder.getStr());

上面在函數中返回了兩個閉包,這兩個閉包都維持著對外部作用域的引用,因此不管在哪調用都是能夠訪問外部函數中的變量。在一個函數內部定義的函數,閉包中會將外部函數的自由對象添加到自己的作用域中,所以可以通過內部函數訪問外部函數的屬性,這就是js模擬私有變量的一種方式。
注意:由于閉包會拓展附帶函數的作用域(內部匿名函數攜帶外部函數的作用域),因此,閉包會比其他函數多占用些內存空間,過度使用會導致內存占用增加,這個時候如果要對性能進行優(yōu)化可能會增加一些難度。
閉包對作用域鏈的影響
由于作用域鏈機制的影響,閉包只能取得內部函數的最后一個值,這引起了一個副作用,如果內部函數在一個循環(huán)中,那么變量的值始終為最后一個值。
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
console.log(data[0])
console.log(data[1])
console.log(data[2])

如果我們想要獲取循環(huán)過程的中的結果,應該要怎么做呢?
- 返回匿名函數的賦值或者立即執(zhí)行函數
- 使用es6的let
匿名函數的賦值
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = (function (num) {
return function(){
console.log(num);
}
})(i);
}
console.log(data[0])
console.log(data[1])
console.log(data[2])
無論上是立即執(zhí)行函數還是返回一個匿名函數賦值,原理上都是因為變量的按值傳遞,所以會將變量i的值賦值給實參num,在匿名函數的內部又創(chuàng)建了一個用于訪問num的匿名函數,這樣每一個函數都有一個num的副本,互不影響。

使用let
var data = [];
for (let i = 0; i < 3; i++) {
data[i] = (function (num) {
return function(){
console.log(num);
}
})(i);
}
console.log(data[0])
console.log(data[1])
console.log(data[2])
前面我們介紹到let主要是作用域局部變量,由于其的存在,使for中的i存在于局部作用域中,而不是再全局作用域。

這個函數表執(zhí)行完畢,其中的變量會被銷毀,但是因為這個代碼塊中存在一個閉包,閉包的作用域鏈中引用著局部作用域,所以在閉包被調用之前,這個塊級作用域內部的變量不會被銷毀。
這個循環(huán)本質上就是這樣
var data = [];// 創(chuàng)建一個數組data;
{
// 進入第一次循環(huán)
let i = 0; // 注意:因為使用let使得for循環(huán)為局部作用域
// 此次 let i = 0 在這個局部作用域中,而不是在全局環(huán)境中
data[0] = function() {
console.log(i);
};
}
{
// 進入第二次循環(huán)
let i = 1; // 因為 let i = 1 和上面的 let i = 0
// 在不同的作用域中,所以不會相互影響
data[1] = function(){
console.log(i);
};
}
...
當我們執(zhí)行 data[1]()的時候,相當于是進入了以下的執(zhí)行環(huán)境
{
let i = 1;
data[1] = function(){
console.log(i);
};
}
在上面這個執(zhí)行環(huán)境中,它會首先尋找該執(zhí)行環(huán)境中是否存在i,沒有找到,就沿著作用域鏈繼續(xù)向上找,在其所在的塊級作用域執(zhí)行環(huán)境中,找到i=1,于是輸出1。
到此這篇關于關于JS中的作用域中的問題思考分享的文章就介紹到這了,更多相關JS中的作用域內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
基于Unit PNG Fix.js有時候在ie6下不正常的解決辦法
本篇文章是對Unit PNG Fix.js有時候在ie6下不正常的解決辦法進行了詳細的分析介紹,需要的朋友參考下2013-06-06

