如何理解js中的作用域,閉包,私有變量,this對象概念呢?
就從一道經典的面試題開始吧!
題目:創建10個<a>標簽,點擊時候彈出相應的序號
//先思考一下你會怎么寫,是不是這樣? 可是結果呢,彈出來的都是10,為啥? var i,a for(i=0;i<10;i++){ a=document.createElement('a') a.innerHTML=i+'<br>' a.addEventListener('click',function(event){ var event=event||window.event event.preventDefault() alert(i) }) document.body.appendChild(a) }
這個題目答案最后再說,當然,你就可以直接翻到最后了解,不過我不建議這樣!!
涉及到今天的變量、作用域、閉包等等,在變量、作用域知識中已經說了一些,再重新簡單的說一下。
執行環境:也稱之為執行上下文,兩個,一個是全局環境(window),一個是函數環境(每個函數都是一個函數環境)
作用域鏈:當代碼在某一個環境中執行,會創建變量對象的一個作用域鏈,以保證對執行環境有權訪問的所有變量和函數的有序訪問。外部環境訪問不到內部環境;內部環境通過作用域鏈可以訪問外部環境。
塊級作用域:js沒有塊級作用域,類似循環語句、條件判斷語句內作用域都是全局的;只有兩個執行環境
變量:var操作符判斷是局部變量還是全局變量。
瀏覽器解析:瀏覽器解析時,先將所有的變量提升,變量都存為undefined,當執行到哪一行才給予賦值;函數聲明一開始就是完整的,而函數表達式跟解析變量是一樣的
console.log(a) //undefined var a=100 console.log(a) //100 person("double") //double,20 function person(name){ //聲明函數 age=20 console.log(name,age) var age }
this:引用執行環境的對象;只有在執行時才能確認,定義時無法確認
//普通的構造函數;this就是構造函數的實例 function Foo(name){ //var this this.name=name //return this } var f=new Foo("double") //普通對象;this就是這個對象 var obj={ name:"double", printName:function(){ console.log(this.name) } } obj.printName() //普通函數 this就是window function fn(){ console.log(this) } fn() //call apply bind來設置this的值 function fn1(name,age){ alert(name) console.log(this) } fn1.call({x:100},'double',20) //this 就是{x:100}這個對象 fn1.apply({x:100},["double",20]) //this 就是{x:100}這個對象 var fn2=function (name,age){ alert(name) console.log(this) }.bind('double',20) //this 就是double了 fn2()
閉包
這個概念有些抽象,這樣理解:有權訪問另一個函數作用域中的變量的函數;也就是說一個函數可以訪問另一個函數作用域里的變量
一般兩個地方,一個作為返回值,一個作為傳遞的參數
//閉包 //函數作為返回值 function F1(){ var a=100; //返回一個函數(函數作為返回值) return function(){ console.log(a) } } //f1 得到一個函數 var f1=F1() var a=200 f1() //a=100 執行返回函數,函數在定義函數的作用域查找變量(而非執行函數的作用域),沒有才會往父級作用域查找 //函數作為參數傳遞 function F1(){ var a=100 return function(){ console.log(a) } } var f1=F1() //這是個函數 function F2(fn){ var a=200 fn() } F2(f1) //a=100 給函數傳參給F2
以閉包為返回值為例,詳細說一下
//這是一個閉包以返回值的例子
function create(){ var a=100 return function(){ console.log(a) } } var a=200 var result=create() //得到返回函數 result() //執行返回函數 a=100 函數會在定義函數的作用域中查找,然后再在父級中查找 當某個函數被調用的時候,會創建一個執行環境及相應的作用域鏈,使用arguments和其他參數 來初始化函數的活動對象,但在作用域鏈中,外部函數的活動對象始終處於第二位, 對於create函數內部的匿名函數,其作用域鏈被初始化包含三個的活動對象,一個自身的,一個為create函數的,一個是全局環境的 一般來說,函數執行完畢,其活動對象就已經被銷毀; 但是對於閉包來說,當create函數執行完畢,函數的活動對象依然不會銷毀,因為內部的匿名函數一直引用其中的活動對象
再來看一下這個
//普通對象 的 一個方法 var person={ name:"double", getName:function(){ console.log(this.name) } } person.getName() //double //普通對象方法返回的是匿名函數 var person={ name:"double", getName:function(){ return function(){ console.log(this.name) } } } var name="single" person.getName()() //single //普通對象方法返回的是匿名函數 var person={ name:"double", getName:function(){ var that=this //在定義匿名函數前將this對象賦予給that的變量 this和arguments差不多,也要將其賦值給某個變量 return function(){ console.log(that.name) } } } var name="single" person.getName()() //double
ok,了解閉包、作用域鏈的知識后,回到那道面試題,應該知道其中的原因
正確的寫法
var i for(i=0;i<10;i++){ (function(i){ var a=document.createElement('a') a.innerHTML=i+'<br>' a.addEventListener('click',function(event){ var event=event||window.event event.preventDefault() alert(i) //去父作用域查找 }) document.body.appendChild(a) })(i) } //js沒有塊級作用域,所以alert(i)的i是在全局作用域下的,返回的當然是10, //如何讓i是每個彈出來的呢? //我們知道環境分函數環境和全局環境,只需要將每個i都有自己的執行環境就可以了 //怎么弄? //直接加個自執行函數,讓每個i都有自己的函數環境,也就是構建出私有作用域
內存泄露
由於閉包的存在,外部函數的變量始終被引用着,閉包不結束,則引用始終存在,內存也就不會被回收
//內存泄露 function person(){ var element=document.getElementById("someElement") element.onclick=function(){ alert(element.id) } } //解決內存泄露 function person(){ var element=document.getElementById("someElement") var id=element.id //將所需內容賦值給一個變量 element.onclick=function(){ alert(id) } element=null //閉包會引用包含函數的整個活動對象,其中包含着element }
模仿塊級作用域
都是知道的,js沒有塊級作用域,所以要模仿,怎么模仿呢?
上面的面試題已經告訴了:通過匿名函數來構造函數環境,形成私有作用域
下面解釋有關涉及到匿名函數,函數聲明,函數表達式的東西,說明一下塊級作用域
//變量名只是個值的另一種形式 var count=10 alert(count) alert(10) //既然變量可以,那么函數呢 var person=function(){ alert("hello") } person() var person=function(){ alert("hello") }() function(){ alert("hello") }() //報錯 //為啥?因為js將function關鍵字作為一個函數聲明的開始,而函數聲明是不能加圓括號 (function(){ alert("hello") })() //將函數聲明轉為函數表達式即可,外部加個圓括號即可,也就是形成自己的私有作用域
看了湯姆大叔的博客,關於這個,他是這么說的
//對於函數聲明和函數表達式的轉換 //js中括號()是不包含語句的,和js中的&&,||,逗號等操作符是在函數表達式和函數聲明上消除歧義的 //所以 var i=function(){console.log("hello")}() true &&function(){console.log("hello")}() 0,function(){console.log("hello")}() !function(){console.log("hello")}() ~function(){console.log("hello")}() -function(){console.log("hello")}() +function(){console.log("hello")}() new function(){console.log("hello")} var value=10 new function(){console.log(value)}(value) //括號傳遞參數
關於閉包的知識,就說這么多
