1.什麼是作用域
作用域是你的程式碼在執行時,某些特定部分中的變數,函式,物件的可訪問性,也就是作用域決定了變數和函式的可訪問範圍,即作用域控制者變數與函式的可見性和宣告週期 作用域的主要功能是:
收集並維護所有聲名的識別符號
依照特定的規則對識別符號進行查詢
確定當前的程式碼對識別符號的訪問許可權
function outFun2() {
var inVariable = "內層變數2";
}
outFun2();//要先執行這個函式,否則根本不知道里面是啥
console.log(inVariable); // Uncaught ReferenceError: inVariable is not defined
複製程式碼
上面的程式碼中 變數inVariable在全域性作用域中沒有被宣告 所以在全域性作用域下取值會報錯 我們可以這樣理解: 作用域就是一個獨立的地盤 ,讓變數不會外洩 暴漏出去也就是說作用域最大的作用就是隔離變數 ,不同作用域下同名變數不會有衝突
ES6之前js沒有塊級作用域 只有區域性作用域和函式作用域 ES6的出現為我們提供了“塊級作用域
可以通過新增的命令 let 和 const 來實現”
2.全域性作用域和函式作用域
所謂全域性作用域 就是在程式碼中的任何地方都能訪問到的物件擁有全域性作用域 一般來說以下情況具有全域性作用域
1.最外層函式和最外層函式外面定義的變數擁有全域性作用域
var globleVariable= 'global'; // 最外層變數
function globalFunc(){ // 最外層函式
var childVariable = 'global_child'; //函式內變數
function childFunc(){ // 內層函式
console.log(childVariable);
}
console.log(globleVariable)
}
console.log(globleVariable); // global
globalFunc(); // global
console.log(childVariable) // childVariable is not defined
console.log(childFunc) // childFunc is not defined
複製程式碼
從上面程式碼中可以看到globleVariable和globalFunc在任何地方都可以訪問到, 反之不具有全域性作用域特性的變數只能在其作用域內使用。
2.未定義變數直接賦值的變數(由於變數提升使之成為全域性變數)
function func1(){
special = 'special_variable';
var normal = 'normal_variable';
}
func1();
console.log(special); //special_variable
console.log(normal) // normal is not defined
複製程式碼
雖然可以在全域性作用域中宣告函式以及變數 使之成為全域性變數但是不建議這麼做 因為這可能會導致和其他變數名衝突 一方面如果我們再使用let 和 const聲名變數 當命名發生衝突時會報錯。
// 變數衝突
var globleVariable = "person";
let globleVariable = "animal"; // Error, thing has already been declared
複製程式碼
另外,如果使用var申明變數,第二個申明的同樣的變數將會覆蓋前面的 這樣會使程式碼很難除錯。如:
var name = 'koala'
var name = 'xiaoxiao'
console.log(name); // xiaoxiao
複製程式碼
2.區域性作用域
區域性作用域一般只在固定程式碼內可以訪問到,最常見的就是函式作用域
2.1函式作用域
定在函式中的變數救在函式作用域中 並且函式在每次呼叫時都有一個不同的作用域,這意味著同名變數可以用在不同的函式中。因為這些變數繫結在不同的函式中,擁有不同的作用域,彼此之間不能訪問
functon test(){
var num = 9;
//內部可以訪問
console.log("test中:" + num);
}
//test外部不能訪問
console.log("test外部" + num);
複製程式碼
注意:
- 如果在函式中定義變數時,如果不新增var關鍵字 造成變數提升 這個變數成為一個全域性變數
function doSomeThing(){
// 在工作中一定避免這樣寫
thing = 'writting';
console.log('內部:'+thing);
}
console.log('外部:'+thing)
複製程式碼
- 任何一對花括號{。。。}中的 語句集都屬於一個塊 在es6之前 在塊語句中定義的便來給你將保留在他已經存在的作用域中:
var name = '程式設計師成長指男';
for(var i=0; i<5; i++){
console.log(i)
}
console.log('{}外部:'+i);
// 0 1 2 3 4 {}外部:5
複製程式碼
我們可以看到變數name和變數i是同級作用域。
2.2在ES6塊級作用域未講解之前注意點
變數提升:
變數提升(hosting) 在程式碼區中任意地方申明變數和在最開始(最上面)申明是一樣的。也就是說,看起來一個變數可以在申明之前被使用 這種行為就是變數提升 看起來就像變數的申明被自動移到了函式或全域性程式碼的最頂上。請看一段程式碼:
var tmp = new Date();
function f() {
console.log(tmp);
if(false) {
var tmp='hello';
}
}
複製程式碼
上面的程式碼會輸出undefined 原因是變數的提升 在這裡申明提升了 定義的內容並不會提升 提升後對應的程式碼如下:
var tmp = new Date();
function f() {
var tmp;
console.log(tmp);
if(false) {
tmp='hello';
}
}
f();
複製程式碼
console在輸出的時候,tmp變數僅僅申明瞭但未定義。所以輸出undefined。雖然能夠輸出,但是並不推薦這種寫法推薦的做法是在申明變數的時候,將所用的變數都寫在作用域(全域性作用域或函式作用域)的最頂上,這樣程式碼看起來就會更清晰,更容易看出來哪個變數是來自函式作用域的,哪個又是來自作用域鏈
重複聲名
// var
var name = 'koloa';
console.log(name); // koala
if(true){
var name = '程式設計師成長指北';
console.log(name); // 程式設計師成長指北
}
console.log(name); // 程式設計師成長指北
複製程式碼
上面的程式碼中 看起來name被宣告瞭兩次 實際上只有最上面的是有用的js的var變數只有在全域性作用域和函式作用域兩種 且申明會被提升 因此name變數只會在最頂上開始的地方申明一次 var name = '程式設計師成長指北'; 此句程式碼的申明將會被忽略 僅僅用於賦值 也就是說上面的程式碼和下面的其實是一致的
// var
var name = 'koloa';
console.log(name); // koala
if(true){
name = '程式設計師成長指北';
console.log(name); // 程式設計師成長指北
}
console.log(name); // 程式設計師成長指北
複製程式碼
變數和函式同時出現的提升
如果有函式和變數同時宣告瞭 會出現什麼情況呢???
看下面的程式碼:
console.log(foo);
var foo ='i am jiangjiejie';
function foo(){}
<!--輸出結果是function foo(){} 也就是函式內容-->
複製程式碼
如果是另一種形式呢?
console.log(foo);
var foo ='i am koala';
var foo=function (){}
複製程式碼
上面程式碼輸出undefined
下面我們對兩種結果進行說明:
- 第一種:函式宣告 就是上面第一種,function foo(){}這種形式
- 另一種:函式表示式,就是上面第二種 var foo=function(){} 這種形式
第二種形式其實就是var變數的聲名定義 因此上面的第二種輸出結果為undefined
而第一種函式聲名的形式 在提升的時候會被整個提升上去 包括函式定義的部分 因此第一種形式和下面的是等價的:
var foo=function (){}
console.log(foo);
var foo ='i am koala';
複製程式碼
可以看到:
- 函式的聲名被提到了最頂上;
- 申明只進行了一次 因此後面 var foo ='i am koala';會被忽略
- 函式申明的優先順序優於變數申明,且函式宣告會連帶定義一起被提升(這裡與變數不同)
2.3塊級作用域
es6新增了let和const命令,可以用來建立塊級作用域變數,使用let命令聲名的變數只在let命令所在的程式碼塊內有效
let 宣告的語法與 var 的語法一致。你基本上可以用 let 來代替 var 進行變數宣告,但會將變數的作用域限制在當前程式碼塊中。塊級作用域有以下幾個特點:
- 1.變數不會提升到程式碼塊頂部 且不允許從外部訪問塊級作用域內部變數
console.log(bar);//丟擲`ReferenceErro`異常: 某變數 `is not defined`
let bar=2;
for (let i =0; i<10;i++){
console.log(i)
}
console.log(i);//丟擲`ReferenceErro`異常: 某變數 `is not defined`
複製程式碼
這個特點帶來了許多好處 開發者需要檢查程式碼的時候可以在作用域外意外但使用某些變數 而且保證了 變數不會被混亂但複用提升了程式碼的可維護性 就像上面程式碼中的例子 一個只在for迴圈內部使用的變數i不會再去汙染整個作用域。
不允許反覆聲名 ES6的let和const不允許反覆聲名 和var不同
// var
function test(){
var name = 'koloa';
var name = '程式設計師成長指北';
console.log(name); // 程式設計師成長指北
}
// let || const
function test2(){
var name ='koloa';
let name= '程式設計師成長指北';
// Uncaught SyntaxError: Identifier 'count' has already been declared
}
複製程式碼
3.作用域鏈
3.1 javascript是如何執行的???
3.1分析階段
JavaScript編譯器編譯完成,生成程式碼後進行分析
- 分析函式引數
- 分析變數聲名
- 分析函式聲名
分析階段的核心就是再分析完成後(也就是接下來函式執行階段的瞬間)會建立一個AO(active Object活動物件)
3.1.2執行階段
分析階段成功後,會把ao給執行階段
- 引擎詢問作用域,作用域中是否有這個叫x的變數
- 如果作用域有x變數 ,引擎會使用這個變數
- 如果作用域中沒有,引擎會自動尋找(向上層作用域)如果到了最後都沒有找到這個變數 引擎會丟擲錯誤。
執行階段的核心就是找,具體怎麼找,後面會講解lhs查詢與RHS查詢
3.1.3 JavaScript執行舉例說明
function a(age) {
console.log(age);
var age = 20
console.log(age);
function age() {
}
console.log(age);
}
a(18);
複製程式碼
首先進入分析階段 前面已經說到 ,函式執行的瞬間 建立一個AO(active object活動物件)
AO = {}
複製程式碼
第一步:分析函式引數
形式引數:AO.age = undefined
實參:AO.age = 18
複製程式碼
第二步:分析變數宣告:
// 第3行程式碼有var age
// 但此前第一步中已有AO.age = 18, 有同名屬性,不做任何事
即AO.age = 18
複製程式碼
第三步:分析函式宣告
// 第5行程式碼有函式age
// 則將function age(){}付給AO.age
AO.age = function age() {}
複製程式碼
函式宣告注意點:AO上如果有與函式名同名的屬性,則會被此函式覆蓋,但是下面這種情況宣告的函式不會覆蓋AO鏈中同名的屬性
var age = function () {
console.log('25');
}
複製程式碼
進入執行階段
分析階段分析成功後,會把給AO(Active Object 活動物件)給執行階段,引擎會詢問作用域,找的過程。所以上面那段程式碼AO鏈中最初應該是
AO.age = function age() {}
//之後
AO.age=20
//之後
AO.age=20
複製程式碼
輸出結果是:
function age(){
}
20
20
複製程式碼
3.2:作用域鏈概念
看了前面一個完整的JavaScript函式執行過程,讓我們來說下作用域鏈的概念吧JavaScript上每個函式執行時,會在自己建立的ao上找對應屬性值,若找不到則往父函式的ao上找,再找不到則再上一層的ao,知道找到最後的全域性作用域,而這一條形成的“ao鏈”就是JavaScript中的作用域鏈
3.3找 過程LHS和RLHS查詢特殊說明
LHS = 變數賦值或寫入記憶體。想象為將文字檔案儲存到硬碟中。 RHS = 變數查詢或從記憶體中讀取。想象為從硬碟開啟文字檔案。
3.3.1LHS和RHS特性
- 都會在所有作用域中查詢
- 嚴格模式下,找不到所需的變數時,引擎都會丟擲ReferenceError 異常
- 非嚴格模式下,LHs會比較特殊,會自動建立一個全域性變數
- 查詢成功時,如果對變數的值進行不合理的操作,比如:對一個非函式型別的值進行函式呼叫,引擎會丟擲Typeerror異常
3.3.2LHS和RHS舉例說明
function foo(a) {
var b = a;
return a + b;
}
var c = foo( 2 );
複製程式碼
直接看引擎在作用域找這個過程:LSH(寫入記憶體):
c=, a=2(隱式變數分配), b=
複製程式碼
RHS(讀取記憶體):
讀foo(2), = a, a ,b
(return a + b 時需要查詢a和b)
複製程式碼
作用域鏈總結