現在先來做兩道練習題
for(var i=0;i<10;i++){
var a='a'
let b = 'b'
}
console.log(a)
console.log(b)
for(var i=0;i<3;i++){
setTimeout(function(){
console.log(i)
},1000)
}
function(){
console.log(a)
var a =1;
function a(){}
console.log(a)
}
理解js作用域
作用域我自己的理解是變量在某個的范圍內可訪問,那這個范圍就是這個變量的作用域。
在ES5中,js只有兩種形式的作用域:全局作用域和函數作用域。
- 全局作用域:變量在程序中任意地方都可以訪問到
- 函數作用域:變量在函數內部可以訪問到,在函數外部無法訪問
for(var i=0;i<10;i++){
var a='a'
let b = 'b'
}
console.log(a) //'a'
console.log(b) //'b' is not defined
上述代碼中,變量a為全局變量
function test(){
var a = 'a'
}
test()
console.log(a) // 函數外部無法直接訪問函數內部變量,報錯
上述代碼中,變量a為局部變量,控制台打印報錯信息a is not defined
function test(){
a = 'a'
}
test()
console.log(a) //'a'
函數內部未使用var關鍵字定義變量,此時a為全局變量
小結
- ES5中, js的作用域分為全局作用域和函數作用域
- 函數內部可以訪問函數外部的全局變量,函數外部卻無法直接訪問函數內部的局部變量
- 未使用var關鍵字定義的變量是全局變量
現在我們知道函數內部可以訪問函數外部的全局變量,函數外部卻無法直接訪問函數內部的局部變量。但有的時候我們需要讀取函數內部的局部變量,該怎么辦呢?
閉包
舉個例子:
function f1(){
var a = 1;
function f2(){
return a;
}
return f2;
}
console.log(f1()()) //1
上述代碼就創建了一個閉包,我們可以在f1函數外部訪問到f1函數內部的值。
我們看到上述代碼有兩個特點:
- f1函數嵌套了f2函數
- f1函數返回了f2函數
閉包的用處:
1.很多js流行框架都是使用匿名自執行函數來避免變量污染
;(function(){
//todo
})()
2.緩存:閉包可以讓變量的值始終保存在內存中,因此在使用時也要注意不要濫用閉包
function f1(){
var n=999;
nAdd = function(){n+=1}
function f2(){alert(n)}
return f2;
}
var result = f1(); //注意只有f1的返回值被外部引用,才不會被回收
result(); // 999
nAdd();
result(); //1000
3.封裝
var person = function(){
var name = ‘default’
return {
getName:function(){ return name},
setName:function(newName){ name=newName}
}
}()
既然ES5中有定義變量的方法,那為什么ES6中又定義了let,const關鍵字呢?
js中的變量提升
var定義變量存在變量提升:只提升聲明語句,不提升賦值語句
var foo = {n:1};
(function(foo){
console.log(foo.n);
foo.n = 3;
var foo = {n:2};
console.log(foo.n);
})(foo)
console.log(foo.n);
執行上述代碼,我們可以看到控制台中按順序依次打印:1,2,3。這是因為Javascript先編譯后執行。編譯階段,先聲明變量,所以引擎會將上面的代碼理解為以下格式
var foo = {n:1};
(function(foo){
var foo;
console.log(foo.n)
foo.n = 3;
foo = {n:2};
console.log(foo.n)
})(foo);
console.log(foo.n)
說明:
- 函數內部定義變量foo時,因為當前作用域中已經存在名為foo的變量,所以編譯器忽略當前聲明,繼續進行編譯,因此第一次打印的內容為外部變量foo的屬性n值:1
- foo.n=3 改變的是外部變量foo,foo={n:2}將foo指向了內部變量,並重新賦值為{n:2},所以第二次打印的內容為內部重新賦值的變量foo的屬性n值:2
- 第三次打印內容是外部變量foo.n,因為函數內容已經更改了外部變量foo,所以打印結果為:3
js中先提升函數,后提升變量。
思考以下代碼:
(function(){
console.log(a)
var a =1;
function a(){}
console.log(a)
})()
執行上述代碼,我們可以看到控制台中按順序依次打印:a(){},1。按照剛才的理解,js引擎將上面的代碼會理解為下面的格式
(function(){
var a;
console.log(a)
a = 1;
function a(){}
console.log(a)
})()
那打印的結果應該為 undefined , f(){},這是因為我們忽略了一點,js先提升函數,后提升變量。所以正確的格式為
(function(){
function a(){}
var a;
console.log(a)
a = 1;
console.log(a)
})()
說明:
1.定義變量a時,因為已經存在命名為a的函數,所以第一次打印結果為a(){}
2.a=1,將變量a重新賦值,所以第二次打印結果為1
小結
- ES5中,使用var定義變量,變量的作用域有兩種:全局作用域、函數作用域
- var定義變量存在變量提升,此外,先提升函數,后提升變量
但是開發過程中,變量提升往往會對開發造成困擾,幸好ES6中引入了let語法。
let
塊級作用域
我們剛才提到,ES5中,js只用兩種作用域:全局作用域與函數作用域。在ES6中,let關鍵字會隱式地創建一個塊級作用域(通常是{}內部),變量只能在這個作用域中被訪問。例如題目一中
for(var i=0;i<10;i++){
var a='a'
let b = ‘b'
}
console.log(a)
console.log(b)
我們在循環的內部,使用let創建了變量b,在循環外部訪問時報錯,b is not defined.就是這個原因。
塊級作用域的引入大大改善了代碼中由於全局變量而引發的錯誤,比如文章開頭提出的第二題:
for(var i=0;i<3;i++){
setTimeout(function(){
console.log(i)
},1000)
}
上述代碼由於變量i是用var聲明的,所以全局范圍有效 ,當循環體執行完時,i=2,所以定時器中console.log(i)中的i是指向全局變量i的,所以打印結果為2,2,2
如果我們將代碼改為
for(let i=0;i<3;i++){
setTimeout(function(){
console.log(i)
},1000)
}
上述代碼中,變量i使用let定義,所以只在本輪for循環中有效,所以打印結果為0,1,2。
let不存在變量提升,其所聲明的變量一定要在聲明語句之后使用。
例如:
console.log(bar);
let bar = 2;
打印結果報錯:bar is not defined
此外,let 聲明的變量不能重復聲明,例如
let foo = {n:1};
(function(foo){
console.log(foo.n);
foo.n = 3;
let foo = {n:2};
console.log(foo.n);
})(foo)
console.log(foo.n);
函數內部定義變量foo時,因為當前作用域中已經存在命名為foo的變量,所以報錯:’foo’ has already been declared.
const
ES6中新增了let關鍵字的同時,也新增了const關鍵字。
let與const有很多共同點:
- 都支持塊級作用域
- 都不支持變量提升
- 都不支持重復聲明
此外,我們知道var聲明全局變量時,變量是掛在window上的。而let,const聲明變量,卻不是。這樣子便避免了window對象的變量污染問題。
當然,const與let也有區別。const與let的區別在於:
- let聲明變量時無需賦值,const聲明變量時必須賦值
- let聲明變量,變量可重新賦值,const聲明變量,完成初始化后,值不得更改 (基本類型)
剛剛提到const聲明變量后,如果值類型是基本類型,則不得更改,如果是引用類型呢?
如圖所示,可修改。如果我用const定義變量,值為對象,但是想讓對象的屬性無法修改應該怎么做呢?
對象屬性的保護方法
Object.defineProperty
Object.defineProperty()方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性,並返回這個對象。
更多關於Object.defineProperty方法的信息,推薦閱讀 MDN:Object.defineProperty
function setConst(obj) {
Object.keys(obj).forEach(function (t,i) {
Object.defineProperty(obj,t,{
value:obj[t],
writable:false, // 是否可重新賦值
enumerable:false, //是否可枚舉
configurable:false // 是否可刪除,其他設置屬性,是否可修改
})
})
}
如圖所示,雖然實現了需求,對象屬性無法修改,無法刪除。可是控制台無提示,不是很人性化。幸好,ES6中提供了Proxy方法
Proxy對象代理
function setConst(obj) {
return new Proxy(obj,{
set:function (obj,prop,value) {
throw new TypeError('Assignment to constant variable')
},
deleteProperty(target, key) {
throw Error(`Error! ${key} cannot be deleted.`);
}
})
}