作用域也就是變量對象能被其他對象訪問的作用範圍
主要包括以下幾個概念:
1、全局作用域(Global Scope)與局部作用域(Local Scope)
2、靜態(詞法)作用域(Lexical Scope)與動態作用域(Dynamic Scope)
3、塊級作用域(Block Scope)
4、作用域鏈(Scope Chain)
現在來具體舉例說明下這幾個區別:
1、全局作用域,指在全局的範圍都能訪問到。例如:
let a = 1;
(function foo() {
b = 2;
}())
window.c = 3;
console.log(a, b, c);
其中a,b,c能被全局訪問到。a定義在當前全局環境,b沒有關鍵詞聲明也是全局,c定義在window全局對象下。
2、局部作用域,也就是函數作用域,只有在函數內部才能訪問到
var a = 1;
(function foo() {
var a = 2;
var b = 2;
}())
console.log(a, typeof b);
從外部能訪問到a,但是外部的a,值為1,而b就是undefined。
局部作用域能訪問全局的作用域,子函數能訪問父函數的作用域,但父函數不能訪問子函數的作用域。如果要想外部訪問內部作用域,那就得利用閉包了,相當於利用函數複製了一個封閉的空間出來,供外部調用。
3、詞法作用域,也就是作用域在變量定義時決定,範圍在所定義的位置,因為JS解析引擎在提取代碼進行詞法分析生成語法樹時就決定了。動態作用域則是程序在運行階段動態來確定變量的作用域範圍,也就是從調用方來查找,而不是定義的環境。JS的call與apply,bind可以動態地改變執行上下文,可以改變變量的執行上下文this,相當於修改了對象作用域的範圍,但不是修改作用域。
var a = 1;
var a = 2;
function foo() {
console.log(a);
}
function change() {
var a = 3;
foo();
}
change();
變量a在定義時的作用範圍內,所以打印仍是2,foo裡面a作用範圍在定義時的foo局部作用域內。如果是動態作用域則打印3。一般編程語言都是靜態作用域,例如C、Java、Python、JS等,但如shell腳本或velocity模板語言是動態作用域。
4、塊級作用域,也就是變量作用範圍在一個塊內,一般是花括號標識。大多數語言都是塊級作用域域。JS直到ES6推出了後,通過let關鍵字聲明的變量作用域在塊內。
{
var a = 1;
let b = 1;
}
console.log(a, b);
這裡a將打印1,b就會報錯,因為變量未定義而調用。
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 0);
}
這裡會打印5次5,因為沒有塊級作用域,後面的覆蓋了前面的,這裡一般要用閉包才能打印出1到5來。
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 0);
}
而如果換成let,則打印1到5,雖然同樣是超時打印,這是因為變量的作用域不同。至於let與var的其他不同,以及var帶來的變量提升等問題,就不在這裡贅述了。
5、作用域鏈,JS的代碼在執行時會針對執行代碼塊(global, function和eval)創建執行上下文(Execution Context, 包括this, variable object,[[scope]]等),執行上下文中的變量對象會從子函數(是一個執行上下文)逐級往上到父函數(也是一個執行上下文)中查找該變量,一直到全局。函數通過一個[[scope]]屬性供其他的其他對象來鏈接查找。這裡注意不要將變量逐級往上查找與變量作用域本身混淆。變量逐級查找是基於作用域鏈向上查找該變量,而變量作用域是該變量的作用範圍,這個範圍是聲明時所在的位置。同時也不要跟this混淆(this表示對象本身,執行上下文也會創建一個this,用來代表這個執行環境,但並不等於這個上下文),函數的this可以看作是函數的指針,指向函數的調用方,它可以被動態修改。
var a = 'a';
function outer() {
var b = 'b';
function child(b) {
a = '1';
b = '2';
function inner() {
console.log('inner:', 'a=' + a, 'b=' + b);
}
inner(b);
}
child(b);
console.log('outer:', 'a=' + a, 'b=' + b);
}
outer();
輸出:
inner: a=1 b=2
outer: a=1 b=b
inner結果原因:a變量在inner裡面沒有,向上查找child,child裡沒有,再向上找到outer,依然沒有,直到最外層找到a。a在執行child()時被重新重新賦值為1。b是向上在child作用域中找中。
outer結果原因:a在outer作用域下沒有,向上才找到a,但a在執行child()時已經被賦值為1了,b因為傳遞的是值而非引用,因此child()執行,b被修改時並非修改的outer下的b,而是child(b)形參定義的b變量。
以上就把作用域說完了,很簡單吧。好了,來一個例子,如果你能說清原理,那就說明理解了。
function foo(b) {
var a = b;
return {
a: a,
change: function(b) {
console.log(a, b, this.a);
a = b + this.a;
},
output: function() {
console.log(a, this.a, b);
}
}
}
var x = foo(1);
console.log('change before:')
console.log(x.a); // 1;
x.output(); // 1, 1, 1
x.a = 3;
x.change(2); // 1, 2, 3
console.log('after change:');
console.log(x.a); // 3
x.output(); // 5, 3, 1
關於這個答案講解可以放在以後的章節中。