作用域與閉包

http://www.cnblogs.com/wangfupeng1988/p/3986420.html

執(zhí)行上下文

什么是執(zhí)行上下文(也叫做“執(zhí)行上下文環(huán)境”)?暫且不下定義,先看一段代碼:

console.log(a) //ReferenceError: a is not defined

console.log(a) //undefined
var a

console.log(a) //undefined
var a = 100

第一句報錯,a未定義,很正常。第二句、第三句輸出都是undefined,說明瀏覽器在執(zhí)行console.log(a)時,已經(jīng)知道了a是undefined,但卻不知道a是10(第三句中)。

在一段js代碼拿過來真正一句一句運行之前,瀏覽器已經(jīng)做了一些準(zhǔn)備工作,也就是我們所說的變量提升。

不僅如此,需要注意代碼注釋中的兩個名詞——函數(shù)表達(dá)式函數(shù)聲明。雖然兩者都很常用,但是這兩者在“準(zhǔn)備工作”時,卻是兩種待遇。

image.png
console.log(f1)  //function f1(){}
function f1(){} 

上例是函數(shù)聲明,解析文件的過程中是會把聲明先讀取,也就是瀏覽器知道了這個調(diào)用的函數(shù)是存在的

就算是在js文件中函數(shù)調(diào)用在前,函數(shù)聲明在后,這也是沒有問題的

console.log(f2); //undefined
var f2 = function(){}

上例是函數(shù)表達(dá)式,實際上也是一個var一個函數(shù)的過程

解析函數(shù)的時,瀏覽器會把var 的東西先提到最前面

即var f1 = undefined,沒有定義的,你調(diào)用它的函數(shù)有什么作用呢?

所以執(zhí)行肯定出現(xiàn)了錯誤

在script文件中,在文件執(zhí)行或者函數(shù)執(zhí)行的最開始,瀏覽器會最先讀取函數(shù)/變量的聲明,將一些賦值的變量也拿出來,也就是我們說的聲明提升(只有var 聲明的變量才會提升,let,const的不會)

我們要知道,在準(zhǔn)備工作中完成了哪些工作:

  • 變量、函數(shù)表達(dá)式——變量聲明,默認(rèn)賦值為undefined;
  • this——賦值;
  • 函數(shù)聲明——賦值;

這三種數(shù)據(jù)的準(zhǔn)備情況我們稱之為“執(zhí)行上下文”或者“執(zhí)行上下文環(huán)境”。

為證明我們的觀點,在控制臺寫上以下代碼,你會看到

于是我們就可以理解下面兩種不同的情況
情況1

fn('zhangsan');
function fn(name){
   alert(name);
}  //運行后沒有錯誤,并且網(wǎng)頁彈出警告框

情況2

f1('zty');
var f1 = function(name ){
  alert(name)
}
函數(shù).png

注意:函數(shù)聲明就是以function開頭的,函數(shù)賦值則不是
函數(shù)聲明中的函數(shù)可以是匿名的,函數(shù)賦值不可以。

讓我們來看看一道比較經(jīng)典的題:

    function yideng() { 
        console.log(1);
    }
    (function () { 
        if (false) {
            function yideng() {
                console.log(2);
            }
        }
        yideng();
    })();
    //yideng is not a function

在早期的瀏覽器中,彈出來的結(jié)果應(yīng)該是2,(因為變量提升,整個 function yideng() { }會被提升到整個函數(shù)作用域的頂端)但是如果彈出來是2的話,我們寫的if語句就完全沒有意義了。后來瀏覽器為了修正這個錯誤,像這種情況下面的函數(shù)提升就只是 var yideng提升到函數(shù)作用域的頂端,本題中false所以沒進(jìn)入函數(shù)體,所以yideng()就會報錯yideng is not a function。而不是像第一題那樣,整個函數(shù)體都提到前面。
據(jù)說除了if,好像while,switch,for也一樣。

關(guān)于this

http://www.ruanyifeng.com/blog/2018/06/javascript-this.html
上面這個是阮一峰老師的關(guān)于一些this的內(nèi)部機制的理解

在箭頭函數(shù)出來之前,我們只知道this要在函數(shù)執(zhí)行時才能知道指的是什么

主要場景

  1. 作為構(gòu)造函數(shù)執(zhí)行
    function Foo(name){        
        this.name = name;
        alert(name);
        console.log(this.name);
    }
    var f1 = Foo('zhangsan');  //控制臺上面打印出zhangsan
    var f2 = Foo('hello'); //打印出hello

在這里,this的存在就顯得比較有意義了,很多個對象都調(diào)用這個相同的方法,this到時指向很多對象,可以靈活使用,也就不用寫那么多的一樣的函數(shù)了

  1. 作為對象屬性執(zhí)行
    var obj = {
        name:'a',
        fn: function(){
            console.log(this.name);
      }
    }
     obj.fn();   //a

這時候的this指向執(zhí)行這個屬性的對象 ,所以也就是a
想想,如果不是作為obj的屬性來調(diào)用時,會是怎么樣的一種情況?

    var obj = {
        name:'a',
        fn: function(){
            console.log(this.name);  
            console.log('kkk');
      }
    }
    var fn1 =  obj.fn;  
    fn1()//undefined

正如上面的代碼:如果fn函數(shù)被賦值到了另一個變量中,并沒有作為obj的一個屬性被調(diào)用,那么this的值就是window,this.x為undefined。

  1. 作為普通函數(shù)執(zhí)行
    function fn(){
        console.log(this);
    }
    fn(); // this指向全局對象
  1. call bind apply
    function fn1(name,age){
          console.log(name);
          console.log(this);
        }
    fn1.call({x:100},'zhangsan',20);
    //執(zhí)行結(jié)果:zhangsan  {x:100)}
    //也就說call能讓this指向它所指定的東西,在例子中 就是{x:100}
    //常用的就是call.
    apply用法與call相同,但是后面是以數(shù)組形式傳遞的
    fn1.call({x:200},['zhangsan',20]);

    var f2 = function(name){
          console.log(name);
          console.log(this); 
    }.bind({y:100},'lisi')

其實關(guān)于js中的this一直是前端一個經(jīng)久不衰的話題,很多筆試題都在this上面做文章。

this指向的四種情況我們就大概講到這里,來做幾道課后題吧!

var obj = {
    x: 10,
    fn: function(){
        function f(){
            console.log(this);//Window
        }
        f()
    }
};
obj.fn()

什么?答案不是obj嗎,不是說作為對象屬性來執(zhí)行的時候是指向?qū)ο蟮膯幔?/p>


別急,且聽我慢慢道來!

函數(shù)f雖然是在obj.fn內(nèi)部定義的,但是它仍然是一個普通的函數(shù),this仍然指向window。作為對象屬性來執(zhí)行的時候是指向?qū)ο蟮倪@句話在這個函數(shù)里指的是fn而不是f,fn是在obj里面定義的,但f是在obj.fn里面定義的。不知道這個解釋你看懂了嗎?

var x = 3;
var y = 4;
var obj = {
    x: 1,
    y: 6,
    getX: function() {
        var x =5;
        return function() {
            return this.x;
        }();
    },
    getY: function() {
        var y =7;
        return this.y;
    }
}
console.log(obj.getX()) 
console.log(obj.getY())

先給兩分鐘的時間思考一下,這道題的結(jié)果應(yīng)該是:
3 6

image.png

讓我們來探究一下吧!
obj.getX跟我們上面說的題其實差不多,this所在的function其實只是定義在obj.getX中的一個普通函數(shù),this自然是指向Window的,從作用域的角度來看,他會先在第一層尋找自己的this.name,找到了,匿名函數(shù)的執(zhí)行環(huán)境具有全局性,而且這個時候是指向window的,因為這是一個普通的函數(shù)。所以應(yīng)該是3。
obj.getY就很循規(guī)蹈矩了,getY就是obj里面的一個屬性,作為obj的屬性執(zhí)行時指向obj,obj的作用域里面的x就等于6。

我們繼續(xù)。

var length = 10;
function fn(){
console.log(this.length);
}
var obj = {
    length:5,
    method:function(fn){
    fn();
    }
}
obj.method(fn);

答案是10,一樣的道理。function fn其實是指向全局的啦!

this指向也是ES6箭頭函數(shù)的一個重要的特性,下次再單獨用一篇文章來寫。

關(guān)于作用域

說到JS其中一座大山,就是我們大名鼎鼎的閉包了。
不過在說閉包之前,我們要先說說作用域!

首先應(yīng)該明確,Js無塊級作用域

    #include <stdio.h> 
    void main() { 
      int i=2; 
      i--; 
      if(i) { 
        int j=3; 
     } 
      printf("%d/n",j); 
    }

以C語言為例,這個會彈出錯誤,因為j是在if的塊中定義的,如果跳出了這個塊,是訪問不到的。
但是在Js中,不同塊中的還是可以訪問的到的。

if(true){
  var i = 7;
}console.log(i);//7

父級作用域

var a = 100;
    function fn1(){
        var b = 200;
        console.log(a);
        console.log(b);
    }

fn1的父級作用域是window

函數(shù)里面定義的數(shù),在外面是改變不了的,外面是污染不了里面的數(shù)的。看下面的題:

var a = 100;
function f1(){
  var b = 200;
  console.log(a);
  console.log(b);
}
var a = 10000;
var b = 20000;
f1();//10000   200;

javascript除了全局作用域之外,只有函數(shù)可以創(chuàng)建的作用域。

所以,我們在聲明變量時,全局代碼要在代碼前端聲明,函數(shù)中要在函數(shù)體一開始就聲明好。

之前就踩了一個坑!

let person = {
    a:2,
    say:()=>{
        console.log(this.a)
    }
}

因為我把方法寫在了對象里,而對象的括號是不能封閉作用域的。所以此時的this還是指向全局對象,所以不是指向person。

讓我們繼續(xù)說一下作用域:

image.png

如我們上圖:全局,fn,bar都分別有自己的作用域,作用域有上下級的關(guān)系,上下級關(guān)系的確定就看函數(shù)是在哪個作用域下創(chuàng)建的。例如,fn作用域下創(chuàng)建了bar函數(shù),那么“fn作用域”就是“bar作用域”的上級。

作用域的作用就是能隔離變量,不同作用域下同名變量不會有沖突。bar需要a就拿bar下面的a,fn需要a就拿fn下面的a,全局需要就拿全局的a。

ES6的let和我們所謂的塊結(jié)合就能達(dá)到我們所期待的塊級作用域的效果了。

作用域和上下文環(huán)境

閉包

http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html

閉包使用場景:
  1. 函數(shù)作用一個返回值
    function f1(){
        var a = 100;
        return function(){
            console.log(a);
        }
    }

    var f2 = f1(); //f2(){console.log(a)},執(zhí)行時候去父作用域中尋找(定義時候的)
    var a = 200;//這里的a 與上面的a 是兩碼事
    f2(); //100,
  1. 函數(shù)作為一個參數(shù)來傳遞
        function f1(){
        var a = 100;
        return function(){
            console.log(a);
        }
    }

    var f2 = f1();
            function f3(fn){
                var a = 200;
                fn();
      }
      f3(f2); //100
閉包的特殊之處

先看一下這段代碼:

    function compare(value1,value2){
        if(value1<value2){
            return -1;
        }else if(value1>value2){
            return 1;
            }else{
                return 0;
                  }
     }
    var result = compare(5,10)

上面先定義了compare()函數(shù),然后在全局作用域中調(diào)用了他。

當(dāng)?shù)谝淮握{(diào)用compare()的時候,會創(chuàng)建一個包含this,arguments.value1,value2的活動對象,(位于compare()函數(shù)作用域鏈中的第一位),全局執(zhí)行環(huán)境的變量對象(this,result,compare)在compare()函數(shù)作用域的第二位。

后臺的環(huán)境都有一個表示變量的對象——變量對象,全局環(huán)境的變量對象始終存在,像compare()函數(shù)的變量對象,是局部變量的,是只在函數(shù)執(zhí)行過程中存在。創(chuàng)建compare這個函數(shù)的時候,會先創(chuàng)建一個預(yù)先包含全局變量對象的作用域鏈,保存在內(nèi)部的scope屬性中,當(dāng)你執(zhí)行compare這個函數(shù)的時候,會先為函數(shù)創(chuàng)建一個執(zhí)行環(huán)境,然后將scope屬性中的對象復(fù)制進(jìn)行你作用域鏈的構(gòu)建,此后又有一些函數(shù)內(nèi)部的一些變量對象被推進(jìn)到作用域鏈的前端,在compare()這個函數(shù)中,其作用域就包括全局變量和局部變量。作用域鏈本質(zhì)上是一個指向變量對象的指針列表。

一般來講,函數(shù)執(zhí)行完畢,局部變量(活動對象)就會被銷毀。內(nèi)存中僅保存全局作用域鏈,但是閉包的情況有所不同。

出現(xiàn)閉包的情況時,在函數(shù)內(nèi)部定義的函數(shù)會將外部函數(shù)的活動對象添加到它的作用域中,在下面的代碼中,匿名函數(shù)從f1()中被返回的時候,他的作用域鏈也被初始化為f1函數(shù)的活動對象的全局變量對象,匿名函數(shù)可以訪問所有在f1中定義的所有變量,更重要的是,在f1函數(shù)執(zhí)行完畢的時候,他的活動對象也不會被銷毀,因為匿名函數(shù)的作用域鏈仍然在引用這個活動對象,直到匿名函數(shù)被銷毀之后,f1的活動對象才會被銷毀。

  function f1(){
    var a = 100;
    return function(){
    console.log(a);
    }
  }
  var f2 = f1()  //利用上面這個函數(shù)來創(chuàng)建一個新的函數(shù)
  var result = f2()//調(diào)用函數(shù)
  f2 = null //解除對匿名函數(shù)的引用。

相關(guān)面試題目

說一下對于變量提升的理解

Js中函數(shù)和變量的聲明總是會被解析器提到方法體的最頂部。(注意,函數(shù)表達(dá)式不會哦)看上文的關(guān)于作用域這一小節(jié)
最簡單的例子:

console.log(f);
var  f = 100;  //undefined,被提到最前面去的只是var f(l類型是undefined)

var e = 100;
console.log(e);  //100
    
fn('zhangsan');
function fn(name){
  this.name = name;
  alert(this.name);
 }//運行的時候彈zhangsan,因為函數(shù)已經(jīng)被聲明了,function fn以及被提到最前面去了

f5('xiaosan');
var f5=function(name){
  this.name = name;
  alert(this.name);
 } //出現(xiàn)錯誤,因為這是函數(shù)的賦值而不是聲明
閉包塊級作用域

一般會給出這樣的代碼,讓你看看可能的運行結(jié)果

請寫出如下點擊的輸出值,并用三種辦法正確輸出li里面的數(shù)字。

<ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
    <li>6</li>
</ul>
<script type="text/javascript">
    var list_li = document.getElementsByTagName("li"); 
    for (var i = 0; i < list_li.length; i++) {
        list_li[i].onclick = function() { console.log(i);
        }
    }
</script>

真的是很經(jīng)典的一道題啊?。。?!大家可以閉著眼睛說不管點擊哪個li都只會輸出6。(跟Js的單線程和異步隊列有關(guān))

解決方法有三個:

  • ES6的let
    for (let i = 0; i < list_li.length; i++) {
        list_li[i].onclick = function() { 
            console.log(i+1);
        }
    }

關(guān)于let 可以看看暫時性死區(qū)這個知識點。

  • 閉包,立即執(zhí)行函數(shù)
  for(var i = 0; i < list_li.length; i++){
    (function(i){
        list_li[i].onclick = function(){
            console.log(i+1)
        }
    })(i)
  }

閉包保存這個變量。

  • 最后一種方法是佳哥說出來的時候恍然大悟,這才是最in的解決方案啊我靠。這道題考的是this啊,不是閉包也不是作用域。。。
    for (var  i = 0; i < list_li.length; i++) {
        list_li[i].onclick = function() { 
            console.log(this.innerHTML);
        }
    }
補充知識

立即執(zhí)行函數(shù)會為每次循環(huán)創(chuàng)建一個單獨的作用域

立即執(zhí)行的函數(shù)表達(dá)式,只要定義完成,不需要調(diào)用,馬上執(zhí)行
(function(){ })(i)

var a = 2;
(function foo(){
var a = 3;
console.log(a);//3
})();
console.log(a);//2

函數(shù)表達(dá)式后面加上一個括號會立即執(zhí)行。
有一個非常普遍的進(jìn)階用法,就是把他們當(dāng)作函數(shù)調(diào)用并且傳遞函數(shù)進(jìn)去

var a = 2;
(function IIFE(global){
    var a = 3;
    console.log(a);//3
  })(window);
  console.log(a);//2
如何理解作用域
  1. 自由變量

  2. 作用域鏈,即自由變量的查找

  3. 閉包的兩個場景

     var a = 100;
         function f1(){
         console.log(a);//此時在函數(shù)作用域中找不到a,我們把a 稱為自由變量
     }
    

在f1作用域中找不到a ,我們就沿著作用域鏈去上一級尋找,a的父級作用域就是window(看f1是在哪里定義的)然后我們找到了var a = 100;于是開開心心的打印出 100

  1. 場景1:函數(shù)作為參數(shù)被傳遞
    場景2:函數(shù)作為一個返回值。
    一般大家是這么形容閉包的:當(dāng)一個函數(shù)的返回值是另外一個函數(shù),而返回的那個函數(shù)如果調(diào)用了其父函數(shù)內(nèi)部的其他值,如果返回的這個函數(shù)在外部被執(zhí)行,就產(chǎn)生了閉包。
    閉包是指有權(quán)訪問另外一個函數(shù)作用域的變量的函數(shù)。
    創(chuàng)建閉包的常見方式,就是在一個函數(shù)內(nèi)部創(chuàng)建另外一個函數(shù)
    例如下面的例子,外部執(zhí)行了這個函數(shù),是可以訪問的到a 這個變量的,之所以能夠訪問得到這個變量,主要還是因為內(nèi)部函數(shù)的作用域中包含著外面這個函數(shù)f1的作用域。

     function f1(){
         var a = 100;
         return function(){
             console.log(a);
         }
     }
    
     var f2 = f1(); //f2(){console.log(a)},執(zhí)行時候去父作用域中尋找(定義時候的)
     var a = 200;//這里的a 與上面的a 是兩碼事
     f2(); //100
    

本質(zhì)上無論何時何地,如果將函數(shù)當(dāng)作第一級的值類型并且到處傳遞,你就會看到閉包在這些函數(shù)中的應(yīng)用。在定時器,事件監(jiān)聽器,Ajax請求,跨窗口通信,Web Worker或者其他任務(wù)中,只要使用了回調(diào)函數(shù),實際上就是在使用閉包。

下面是一個由于沒有理解閉包和作用域而可能會遇到的坑

    for(var i = 1;i <=5;i++){
        setTimeout(function timer(){
            console.log(i);
        },i*1000)
    }//彈出來了5個6

與我們的預(yù)期不一樣,我們預(yù)期是依次彈出1,2,3,4,5
根據(jù)作用域的工作原理,實際情況是盡管循環(huán)中的五個函數(shù)是在各個迭
代中分別定義的,像上面的代碼中,他們都被封閉在一個共享的全局作用
域中,每次循環(huán),每個函數(shù)都保存著全局作用域中的活動對象,其中包括i
值,也就是說每個函數(shù)的作用域鏈中都保存著全局作用域中的i,當(dāng)主線程
中的i++這些操作結(jié)束之后,i已經(jīng)變成6了,閉包只能取得包含函數(shù)中任何
變量的最后一個值也就是6,在時間執(zhí)行到了timer函數(shù)的時候,因為js解析
的時候遇到setTimeout會把回調(diào)抽出來,放在一個單獨的隊列里面,等到
當(dāng)前處理機空閑了,再去執(zhí)行那個隊列里的東西。處理機先將for循環(huán)全都
處理完畢,再去執(zhí)行SetTimeout這個函數(shù)。此時全局變量的這個i就是6,
因此無法達(dá)到預(yù)期。

解決方法

for(var i = 1;i<=5;i++)
    (function(i){
        setTimeout(function timer(){
            console.log(i);  //i是自由變量,去父級作用域里面找,我們找到了立即函數(shù)傳進(jìn)來的i(每次for循環(huán)后都執(zhí)行一次立即函數(shù))
即函數(shù)為每次循環(huán)都開拓一個單獨的作用域(塊級作用域),在ES6中只要用{  }就是一個塊級作用域了,不用怕會被覆蓋
        },i*1000)
    })(i);

其實將i 前面的var 改成let 也是可以得到我們想要的效果,因為let聲明的也是一個塊級作用域。

上面的區(qū)別在于,我們加了一個自執(zhí)行函數(shù),只要定義,不需要調(diào)用,而且可以立馬執(zhí)行,還會產(chǎn)生一個專門的塊級作用域,(function(i))(j)就是把j的值傳給i,產(chǎn)生了這么一個塊級作用域。
閉包在實際開發(fā)中的作用
模塊化中經(jīng)常需要用到閉包
    function f1(){
        var something = "something";
        return function(){
            console.log(something);
        }
    }

    var foo = f1();
    foo(); //外面的根本觸碰不到函數(shù)里面的變量
實際運用2

用于封裝變量,收斂權(quán)限。

    function firstLoad(){
        var _list = [];//變量前面帶有_表明變量是私有變量
        return function(id){
            if(_list.indexOf(id) >= 0){
                return false;//這個Id對應(yīng)的位置上已經(jīng)有東西
            }else{
                _list.push(id);//這個Id對應(yīng)的位置上壓東西進(jìn)去
                return true;
            }
        }
    }

    var load = firstLoad();
    load(10); //true ,第一次加載.
    load(10); //false,因為上面已經(jīng)加載過了

主要就是只有通過firstload返回的函數(shù)才能去操作__list這個數(shù)組,如果你在外面直接去操作__list是不可能的。因為__list是沒有被直接暴露出來的。

理解了這兩段代碼的結(jié)果,也許你就理解了什么是閉包。

var name = "The Window";
var object = {
name:"my object",
getNameFunc:function(){
return function(){
return this.name;
};
}
};
console.log(object.getNameFunc()());//var name = "The Window";

var name = "The Window";
var object = {
name:"my object",
getNameFunc:function(){
var that = this;
return function(){
return that.name;
};
}
};
object.getNameFunc()();
// "my object"

談?wù)勎易约旱睦斫猓驗槲覀兪紫纫磧蓚€函數(shù)其實都產(chǎn)生了閉包,但是在return function()中return this.name,我們要注意到這其實是一個匿名函數(shù),從作用域的角度來看,他會先在第一層尋找自己的this.name,找到了,匿名函數(shù)的執(zhí)行環(huán)境具有全局性,而且這個時候是指向window的,因為這是一個普通的函數(shù)。

第二段代碼中,將getNameFunc作用域里面的this保存在閉包可以訪問到的變量里,就可以讓閉包訪問這個對象。(argument也是同樣一個道理)

(也就是上面講的作為函數(shù)對象執(zhí)行)

var obj={name:'hhhh',
func:function(){
return this.name;}}
obj.func()//"hhhh"

//至于為什么是getNameFunc()();

var object = {
name:"my object",
getNameFunc:function(){
return function(){
return this.name;
};
}
};
console.log(object.getNameFunc());
// ? (){ return this.name; }

一個括號其實就是得到的return的值是那個function
再加一個括號就是再去執(zhí)行那個function

閉包為什么會造成內(nèi)存泄漏

先談?wù)動嫈?shù)垃圾收集,如果0個引用指向他,那么該對象就可以被垃圾收集了。但是計數(shù)垃圾收集有一個嚴(yán)重的問題,就是循環(huán)引用的對象是不能回收的,IE9之前就是用javascript引擎就是使用標(biāo)記清除策略來實現(xiàn)。
循環(huán)引用(兩個對象的相互引用),在函數(shù)調(diào)用之后他們是無用的可以釋放,但是計數(shù)算法認(rèn)為,由于兩個對象的每一個至少都是被引用一次的,兩者都不能被垃圾回收。例如下面的:

    function problem(){
        var ObjectA = new Object();
        var ObjectB = new Object();

        ObjectA.someOtherObject = ObjectB;
        ObjectB.someOtherObject = ObjectA;
        }

在IE9之前,BOM和DOM對象都是使用C++以COM對象的形式來實現(xiàn)的,COM對象的垃圾收集機制采用的就是計數(shù)策略,所以IE中只要涉及到BOM和DOM對象,就會存在循環(huán)引用的問題。
像下面這樣子:

      var element = document.getElementById("some_element");
      var myObject = new Object();
      myObject.element = element;
      element.someObject = myObject;

閉包實際上很容易造成JavaScript對象和DOM對象的隱蔽循環(huán)引用。也就是說,只要閉包的作用域鏈中保存著一個HTML元素,那么就意味著這個元素將無法被銷毀。

      function assignHandler(){
        var element = document.getElementById("someElement");
        element.onclick = function(){
            alert (element.id);
       }; 
    }

注意,閉包的概念是一個函數(shù)的返回值是另外一個函數(shù),返回的那個函數(shù)如果調(diào)用了其父函數(shù)內(nèi)部的其他值,這是一個element元素事件處理的一個閉包。這個閉包創(chuàng)建了一個循環(huán)引用。

  1. JavaScript對象element引用了一個DOM對象(其id為“someElement”); JS(element) ----> DOM(someElemet)
  2. 該DOM對象的onclick屬性引用了匿名函數(shù)閉包,而閉包可以引用外部函數(shù)assignHandler 的整個活動對象,包括element ; DOM(someElement.onclick) ---->JS(element)
    匿名函數(shù)一直保存著對assginHandler()活動對象的引用,它所占的內(nèi)存永遠(yuǎn)不會被回收。

所以可以稍稍改進(jìn):

        function assignHandler(){
        var element = document.getElementById("someElement");
        var id = element.id;
        element.onclick = function(){
            alert (id);
       }; 
       element = null;
    }

可以首先通過引用一個副本變量消除循環(huán)引用,但是這個閉包包含外部函數(shù)的全部活動對象,所以就算不直接引用,匿名函數(shù)一直都包含著對element的引用。所以最后需要手動設(shè)置element變量為null.

閉包的很大作用是可以讀取函數(shù)內(nèi)部的變量,另一個就是讓這些變量的值始終保持在內(nèi)存中。

只要變量被任何一個閉包使用了,就會被添到詞法環(huán)境中,被該作用域下所有閉包共享。

關(guān)于函數(shù)提升
    alert(a)    1
    a();   2
    var a=3;
    function a(){
        alert(10)
    } 
    alert(a)    3
    a=6;
    a();   4

請寫出彈出值,并解釋為什么?
由于函數(shù)提升,整個式子會變成下面這樣

  var a;
  function a(){
     alert(10)
  } 
  alert(a)    1
  a();   2
  a=3;
  alert(a);   3    
  a=6;
  a();   4

很明顯,會得到下面的結(jié)果
1. f a(){alert(10)} //變量提升,并且函數(shù)優(yōu)先級高于變量,如果相同命名,變量會被覆蓋。
小知識:

    console.log(c)//? c(){console.log('hahha') 
    function c(){
        console.log('hahha')
    }
    var c = 1;

    console.log(c) //? c(){console.log('hahha') 
    var c = 1;
    function c(){
        console.log('hahha')
    }
  1. 10 //執(zhí)行了a(){alert(10)}
  2. 3 //給a賦值了3
  3. a is not a function 此時已經(jīng)給a賦值了6,a已經(jīng)不是函數(shù)了,所以會彈出錯誤
關(guān)于函數(shù)變量提升的一些特殊情況
    function yideng() { 
        console.log(1);
    }
    (function () { 
        if (false) {
            function yideng() {
                console.log(2);
            }
        }
        yideng();
    })();
    //yideng is not a function
在早期的瀏覽器中,彈出來的結(jié)果應(yīng)該是2,(因為變量提升,整個         function yideng() { }會被提升到整個函數(shù)作用域的頂端)但是如果彈出來是2的話,我們寫的if語句就完全沒有意義了。后來瀏覽器為了修正這個錯誤,像這種情況下面的函數(shù)提升就只是 `var yideng`提升到函數(shù)作用域的頂端,本題中false所以沒進(jìn)入函數(shù)體,所以yideng()就會報錯yideng is not a function。而不是像第一題那樣,整個函數(shù)體都提到前面。

據(jù)說除了if,好像while,switch,for也一樣。
補充小知識:

    if(false){
        var a = 1;
    }
    console.log(a)//undefined
關(guān)于this

寫出以下輸出值,解釋為什么?

this.a = 20; 
var test = {
    a: 40,
    init:()=> {
        console.log(this.a);  
        function go() {
            console.log(this.a);
        }
        go.prototype.a = 50; 
        return go;
        }
    };
    new(test.init())();   //20   50 

箭頭函數(shù)的this綁定父級的詞法作用域,所以他的this.a固定了是20。
go本身沒有a 這個屬性,所以new出來的對象也沒有這個屬性,會到原型鏈上面去找。所以是50。

  this.a = 20; 
  var test = {
    a: 40,
    init:()=> {
        console.log(this.a); 
        function go() {
           this.a = 60;
            console.log(this.a);
        }
        go.prototype.a = 50; 
        return go;
        }
    };
    var p = test.init();  //20 
    p();  //  60 
    new(test.init())();   //  60,60

剛開始我以為輸出的結(jié)果會是20 60 20 60
var p = test.init(); 此時test.init()會執(zhí)行一遍。此時的箭頭函數(shù)中的this綁定了最外層的this(test父級作用域的this),也就是this.a = 20
p() 此時相當(dāng)于執(zhí)行g(shù)o(),注意:這個時候是var p = test.init(),所以執(zhí)行p()時的this是指向windows,也就是我們現(xiàn)在看到的最外層的this.a = 20的那個this,此時會執(zhí)行this.a = 60,將windows的this.a 更改成60,執(zhí)行之后打印出來的this.a=60.
new(test.init())():test.init()會取到已經(jīng)綁定了的父級詞法作用域的this,不過此時的this.a已經(jīng)被改為60,所以打印出來60。繼續(xù)執(zhí)行,打印出60。

思考一下,如果是下面這種情況的話,會打印出來什么呢?

image.png

執(zhí)行的時候20
因為這里是var 不是this.a

另外一個小知識:

    function test(){
        console.log(this)
    }
    var obj = {
        f:test
    }
    (obj.f)()  //Cannot read property 'f' of undefined

?????什么鬼??
原來是因為沒有加;在瀏覽器看來
var obj = {f:test}(obj.f)() 是這樣連在一起的。那肯定會報錯??!

加上分號之后,我們再來看一下這段代碼運行的結(jié)果是怎么樣的。

    function test(){
        console.log(this)
    }
    var obj = {
        f:test
    };
    (obj.f)()//{f: ?}

很明顯,指向了obj這個對象,因為是obj調(diào)用了test()。
那如果我們再這樣改一下:

    function test(){
        console.log(this)
    }
    var obj = {
        f:test
    };
    (false || obj.f)()

猜猜結(jié)果是什么?是window
?????這又是什么鬼???
短路語句的結(jié)果不也是obj.f,運算的結(jié)果應(yīng)該也是obj才對啊。
但是()里面是計算,所以里面應(yīng)該是表達(dá)式,也就是說里面相當(dāng)于var xx = obj.f,然后xx(),此時this肯定是指向全局變量window

關(guān)于閉包塊級作用域

請寫出如下點擊的輸出值,并用三種辦法正確輸出li里面的數(shù)字。

<ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
    <li>6</li>
</ul>
<script type="text/javascript">
    var list_li = document.getElementsByTagName("li"); 
    for (var i = 0; i < list_li.length; i++) {
        list_li[i].onclick = function() { console.log(i);
        }
    }
</script>

真的是很經(jīng)典的一道題?。。。?!大家可以閉著眼睛說不管點擊哪個li都只會輸出6。(跟Js的單線程和異步隊列有關(guān))
解決方法有三個:

  • ES6的let

      for (let i = 0; i < list_li.length; i++) {
          list_li[i].onclick = function() { 
              console.log(i+1);
          }
      }
    

關(guān)于let 可以看看暫時性死區(qū)這個知識點。

  • 閉包,立即執(zhí)行函數(shù)

    for(var i = 0; i < list_li.length; i++){
      (function(i){
          list_li[i].onclick = function(){
              console.log(i+1)
          }
      })(i)
    }
    

閉包保存這個變量。怎么去理解閉包保存這個變量呢,也就是說這里每次執(zhí)行立即執(zhí)行函數(shù)的時候,會傳遞一個i(實參)進(jìn)去函數(shù)級的作用域,在這個函數(shù)級作用域里面onclick的回調(diào)函數(shù)使用了這個i,也就是我們常說的閉包了,還要注意的一點是i在這里的按值傳遞的,所以每個i都是獨立的各不影響的。

  • 最后一種方法是佳哥說出來的時候恍然大悟,這才是最in的解決方案啊我靠。這道題考的是this啊,不是閉包也不是作用域。。。

      for (var  i = 0; i < list_li.length; i++) {
          list_li[i].onclick = function() { 
              console.log(this.innerHTML);
          }
      }
    

內(nèi)存泄漏

http://www.ruanyifeng.com/blog/2017/04/memory-leak.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容