1. 程式人生 > 其它 >JavaScript經典面試題之for迴圈click

JavaScript經典面試題之for迴圈click

經典重現

<!DOCTYPE html>

<html lang="en">

<head>

<meta charset="utf-8">

<title></title>

<script type="text/javascript">

function onMyLoad(){

var arr = document.getElementsByTagName("p");

for(var i = 0; i < arr.length; i++){

arr[i].onclick = function(){

alert(i);

}

}

}

</script>

</head>

<body onload="onMyLoad()">

<p>0</p>

<p>1</p>

<p>2</p>

<p>3</p>

<p>4</p>

</body>

</html>

該段程式碼期望實現效果如下:點選p標籤,彈出該p標籤位置序號。請問上述程式碼能否實現該需求,如果不能,應該如何實現?

原題分析

答案顯而易見,不能。點選每個p標籤都會彈出5。 要解決此問題,首先要了解閉包的概念。閉包是JavaScript語言的一個難點,也是它的特色,很多高階應用都要依靠它來實現。

變數作用域

JavaScript變數作用域的特點在於,函式內部可以讀取該函式外部的變數,但函式外部無法讀取該函式內部定義的變數,但是我們可以通過變通的方式獲得。那就是在函式內部再定義一個函式:


function foo(){

    var n=999;

    function fee(){

      alert(n); // 999

    }

  }

在上面的程式碼中,函式fee就被包括在函式foo內部,這時foo內部的所有區域性變數,對fee都是可見的。但是反過來就不行,fee內部的區域性變數,對foo就是不可見的。這就是Javascript語言特有的”鏈式作用域”結構(chain scope),子物件會一級一級地向上尋找所有父物件的變數。所以,父物件的所有變數,對子物件都是可見的,反之則不成立。 既然fee可以讀取foo中的區域性變數,那麼只要把fee作為返回值,我們不就可以在foo外部讀取它的內部變量了嗎!

閉包的概念

簡而言之,閉包就是能夠讀取其他函式內部變數的函式,由於在JavaScript語言中,只有函式內部的子函式才能讀取區域性變數,因此可以把閉包簡單理解成定義在一個函式內部的函式

。所以,在本質上,閉包就是將函式內部和函式外部連線起來的一座橋樑。

閉包的用途

閉包可以用在許多地方。它的最大用處有兩個,一個是前面提到的可以讀取函式內部的變數,另一個就是讓這些變數的值始終保持在記憶體中。

function f1(){

    var n=999;

    nAdd=function(){

        n+=1

        }

    function f2(){

      alert(n);

    }

    return f2;

  }

  var result=f1();

  result(); // 999

  nAdd();

  result(); // 1000

在這段程式碼中,result實際上就是閉包f2函式,它一共運行了兩次,第一次的值是999,第二次的值是1000。這證明了,函式f1中的區域性變數n一直儲存在記憶體中,並沒有在f1呼叫後被自動清除。

為什麼會這樣呢?原因就在於f1是f2的父函式,而f2被賦給了一個全域性變數,這導致f2始終在記憶體中,而f2的存在依賴於f1,因此f1也始終在記憶體中,不會在呼叫結束後,被垃圾回收機制(garbage collection)回收。

這段程式碼中另一個值得注意的地方,就是”nAdd=function(){n+=1}”這一行,首先在nAdd前面沒有使用var關鍵字,因此nAdd是一個全域性變數,而不是區域性變數。其次,nAdd的值是一個匿名函式(anonymous function),而這個匿名函式本身也是一個閉包,所以nAdd相當於是一個setter,可以在函式外部對函式內部的區域性變數進行操作。

使用閉包的注意點

  1. 由於閉包會使得函式中的變數都被儲存在記憶體中,記憶體消耗很大,所以不能濫用閉包,否則會造成網頁的效能問題,在IE中可能導致記憶體洩露。解決方法是,在退出函式之前,將不使用的區域性變數全部刪除。
  2. 閉包會在父函式外部,改變父函式內部變數的值。所以,如果你把父函式當作物件(object)使用,把閉包當作它的公用方法(Public Method),把內部變數當作它的私有屬性(private value),這時一定要小心,不要隨便改變父函式內部變數的值。

思考題

如果你能理解下面兩段程式碼的執行結果,應該就算理解閉包的執行機制了。

程式碼一:


var name = "The Window";

  var object = {

    name : "My Object",

    getNameFunc : function(){

      return function(){

        return this.name;

      };

    }

  };

  alert(object.getNameFunc()())

程式碼二:

var name = "The Window";

  var object = {

    name : "My Object",

    getNameFunc : function(){

      var that = this;

      return function(){

        return that.name;

      };

    }

  };

  alert(object.getNameFunc()());

接下來回到文章開始的題目,解釋一下為什麼打印出來的數字都是5.

arr中的每一項onclick都是一個函式例項,這個函式也產生了一個閉包域,這個閉包域引用了外部閉包域的變數,即i,外部閉包域的私有變數內容發生變化,內部閉包域得到的值自然會發生改變(參照閉包的用途一節中的示例程式碼)

解決方案

方法一:增加若干個對應的閉包域空間(採用匿名函式實現)專門用來儲存原先需要引用的內容(下標值),只限於基本型別(基本型別值傳遞,物件型別引用傳遞)

for(var i = 0; i<arr.length; i++){

(function (arg){//這個函式物件有一個本地私有變數arg(形參),該函式的function scope的closure物件屬性有兩個引用:arr和i。i的值隨外部改變,但是本地的私有變數(形參)arg不會受影響,其值在一開始被呼叫時就決定了

arr[i].onclick = function () {//onclick函式例項的function scope的closure物件屬性有一個引用arg

alert(arg);//只要外部空間的arg不變,這裡的引用值就不會改變

}

})(i);//立即執行匿名函式,傳遞下標i(實參)

}

方法二:將下標作為物件屬性(name:”i”,value:i的值)新增到每個陣列項(p物件)中

for(var i=0; i<arr.length; i++){

//為當前陣列項(當前p物件)新增一個名為i的屬性,值為迴圈體i變數的值

//此時當前p物件的i屬性並不是對迴圈體的i變數的引用,而是一個獨立p物件的屬性,屬性值在宣告的時候就確定了

arr[i].i = i;

arr[i].onclick = function (){

alert(this.i);

}

}

方法三:增加若干個對應的閉包域空間用來儲存下標。新增的匿名閉包空間內完成事件繫結。 //繫結的函式中的function scope中的closure物件的引用arg是指向將其返回的匿名函式的私有變數arg


for(var i = 0; i<arr.length; i++){

arr[i].onclick = (function(arg){

return function () {

alert(arg);

}

})(i);

方法四:使用ES6的let關鍵字

"use strict";

var arr = document.getElementsByTagName("p");

for(var i = 0; i<arr.length; i++){

let j = i;//塊級變數

arr[i].onclick = function () {

alert(j);

}

}