執行環境
var name = 'paper_crane';
function sayName() {
alert('paper_crane');
}
alert(name) // paper_crane
sayName(); // paper_crane
window.sayName(); // paper_crane
alert(window.name); // paper_crane
在上面的例子中,聲明瞭一個全局變量name和一個全局函數sayName,無論是直接調用它們還是當window對象的一個屬性或方法使用都能正確的執行並得到我們預期的結果,所以執行環境裏面定義的所有變量和函數都會被當成其變量對象的屬性和方法,此對象內部的所有的屬性和方法都能訪問到此對象內部的其他屬性和方法。這樣就能用一個具體的變量對象來描述一個抽象的執行環境,window是唯一一個可以通過js獲取的變量對象,即使如此,在非必須的情況下不建議使用這種方式使用自定義的全局變量和全局函數。
作用域和作用域鏈
var name = 'paper_crane';
function sayName() {
alert(name);
var age = 22;
}
sayName(); // paper_crane
alert(age); // throw an error
在這個例子中,變量name是全局變量,變量age是sayName函數局部作用域的變量,在全局作用域裏面使用age會報錯,而在局部作用域裏面使用全局作用域裏面的name變量卻不會報錯。這個結果符合我們的預期,而JavaScript就是通過作用域鏈實現這種變量訪問權限的。
var name = 'paper_crane';
function sayName() {
var name = 'crane';
alert(name);
}
sayName(); // crane
在上面的例子中,sayName函數內部的變量對象和全局變量對象都聲明瞭一個name變量,但是在內部訪問變量name的時候,就會訪問當前變量對象的name變量,接着就是停止向後回溯了。這個不難理解,但是有一種特殊的情況需要注意一下的:
var name = 'paper_crane';
function sayName() {
alert(name); // undefined
var name = 'crane';
alert(name); // crane
}
sayName();
在上面的例子中,第一個alert需要訪問name變量,但是在sayName內部環境裏面還沒有聲明(起碼在我們看來還沒有),所以應該是彈出“paper_crane”纔對,但是實際上卻彈出了“undefined”,爲何?其實JavaScript在聲明變量的時候,會把聲明直接提前到代碼執行前面,所以當第一個alert函數訪問name變量時,會搜索sayName的內部變量對象,而內部變量對象已經聲明瞭name變量,而初始化會則會在代碼設定的位置,這個跟函數聲明提升有點相似,所以第一個alert彈出的是“undefined”。所以在聲明變量的時候,最好在進入此執行環境就把所有的變量都聲明好,不要在邏輯代碼中間隨意的聲明一個變量。
function sayAge() {
if (true) {
var age = 22;
alert(age); // 22
}
alert(age); // 22
}
sayAge();
由於沒有塊級作用域,大量聲明會非常容易造成作用域污染,此時只要使用一個立即執行函數就可以模擬塊級作用域了:
var name = 'paper_crane';
function sayAge() {
(function() {
if (true) {
var age = 22;
alert(age); // 22
}
})();
alert(age); // ReferenceError
}
sayAge();
詞法作用域
var name = 'paper_crane';
function sayName() {
alert(name); // undefined
var name = 'crane';
alert(name); // crane
}
sayName();
這個例子上面舉過,不過上面主要是說明變量聲明會提升。但實際上詞法作用域的本質就是通過變量和函數聲明提升來實現的。上面sayName函數在其作用域內聲明瞭變量name,當函數內使用name變量的時候,在本作用域找到了name變量,所以不會向上層尋找這個變量。
this對象
指向window
var name = 'paper_crane';
function globalFunction() {
var name = 'crane';
alert(this.name); // paper_crane
(function() {
alert(this.name); // paper_crane
alert(name); // crane
})();
function innerFunction() {
alert(this.name); // paper_crane
alert(name); // crane
}
innerFunction();
}
globalFunction();
指向函數所有者
var obj = {
name : 'paper_crane',
showName: function() {
alert(this.name); // paper_crane
}
}
obj.showName();
在上面的例子中聲明瞭一個對象obj,包括一個name屬性和showName方法,sayName方法可以正確的訪問到name屬性,所以this指向的是showName函數的所有者obj。除了這種直接聲明一個對象的情況,在構造類構造函數的時候也指向所有者。例如:
function Student() {
this.name = 'paper_crane';
this.showName = function() {
alert(this.name); // paper_crane
}
}
var crane = new Student();
crane.showName();
在上面的例子中,實現了一個Student類構造函數(實際上ES規範中並沒有類的概念,這只是實現面向對象編程的方法,至於面向對象編程有時間再詳談),類裏面有一個name屬性,有一個showName方法,然後聲明瞭一個Student的實例crane,此時調用crane的方法showName就會發現輸出的是paper_crane,所以this指向的是其所有者crane。
改變this的指向
var name = 'paper_crane';
var thisObj = {
name : 'crane'
}
function showName() {
alert(this.name);
}
showName(); // paper_crane
showName.call(thisObj); // crane
showName.apply(thisObj); // crane
var fun = showName.bind(thisObj);
fun(); // crane
上面的例子演示了使用函數對象的call、apply、bind方法改變函數this對象的指向,至於以上三個方法的區別和使用具體使用方法有時間再詳談。
箭頭函數中的this
var name = 'paper crane';
var obj = {
name: 'crane',
normal: function() {
console.log(this.name);
},
arrow: () => {
console.log(this.name);
}
};
obj.normal(); // crane
obj.arrow(); // paper crane