首頁 >web前端 >js教程 >帶你進一步理解js閉包(詳細)

帶你進一步理解js閉包(詳細)

不言
不言原創
2018-10-18 13:39:182433瀏覽

這篇文章帶給大家的內容是關於帶你進一步理解js閉包(詳細),有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。

譯者:閉包都被討論爛了,不理解閉包都不好意思說自己會js,但我看到這篇文章還是感覺眼前一亮,也讓我對閉包有了一些新的理解,並且涉及了一些類和原型鏈的知識,這是一篇2012年的文章,稍微有點早,內容也略微基礎,但是很明晰,希望能給讀者帶來新的理解。

閉包(Closure) 是javascript這門語言中有些複雜且充滿誤解的特性。簡言之,閉包是一個對象,這個物件包含一個方法(function)和該方法創建時環境的引用(reference to the enviroment)。為了完全理解閉包,我們還需要理解兩個js中的特性,一個是一級方法(first-class function),另一個是內部方法(inner function)。

一級方法/First-Class Functions

在js中,方法是頭等公民,因為它可以輕易轉換成其他資料型別。例如,一級方法可以即時建構並且賦值給一個變數。也可以傳遞給其他方法,或透過其他方法返回。除了滿足這些標準以外,方法也擁有自己的屬性和方法。
透過下述例子,我們來看一級方法的能力。

var foo = function() {
  alert("Hello World!");
};

var bar = function(arg) {
  return arg;
};

bar(foo)();
譯者註:省略原文對程式碼的文字解釋,這裡體現的是一級方法可以回傳參數,參數可以是另一個一級函數,回傳的結果還可以呼叫。

內部方法/Inner Functions

內部方法或說嵌套方法,是指定義在其他方法內部的方法,每當外部方法被喚起,內部方法的實例就被創建。下面的範例反應內部方法的使用,add方法是外部方法,doAdd是內部方法。

function add(value1, value2) {
  function doAdd(operand1, operand2) {
    return operand1 + operand2;
  }

  return doAdd(value1, value2);
}

var foo = add(1, 2);
// foo equals 3

這個例子中,一個重要的特性是,內部方法獲取到了外部方法的作用域,這意味著內部方法能夠使用外部方法的變量,參數等。範例中add()的參數value1,value2傳遞給doAdd()的operand1,operand2參數。然而這並沒有必要,因為doAdd可以直接取得value1,value2。所以上面的例子我們還可以這麼寫:

function add(value1, value2) {
  function doAdd() {
    return value1 + value2;
  }

  return doAdd();
}

var foo = add(1, 2);
// foo equals 3

建立閉包/Creating Closures

內部方法取得外部方法的作用域,便形成了一個閉包。典型的場景是外部函數將其內部方法傳回,內部方法保持了外部環境的引用,並保存了作用域下的所有變數。
一下範例展示閉包如何建立並使用。

function add(value1) {
  return function doAdd(value2) {
    return value1 + value2;
  };
}

var increment = add(1);
var foo = increment(2);
// foo equals 3

說明:

  • add傳回了內部方法doAdd,doAdd呼叫了add的參數,閉包建立。

  • value1是add方法的本機變量,對doAdd來說是非本機變數(非本機變數指變數既不在函數體本身,也不在全域),value2是doAdd的本機變數。

  • 當add(1)被調用,一個閉包被創建並儲存在increment中,在該閉包的引用環境中,value1綁定了1,被綁定的1相當於「封鎖」在這個函數中,這也是「閉包」這個名字的由來。

  • 當increment(2)被調用,進入閉包函數,這意味著攜帶value1為1的doAdd被調用,因此該閉包本質上可以當做如下函數:

function increment(value2) {
  return 1 + value2;
}

何時使用閉包?

閉包可以實現很多功能。例如將回調函數綁定指定參數。我們說兩個讓你的生活和開發變得更簡單的場景。

  1. 配合定時器

閉包結合setTimeout和setInterval非常有用,閉包可讓你向回呼函數傳入指定參數,例如下面的例子,每秒鐘在給指定dom插入字串。

<!DOCTYPE html>
<html lang="en">
<head>
  <title>Closures</title>
  <meta charset="UTF-8" />
  <script>
    window.addEventListener("load", function() {
      window.setInterval(showMessage, 1000, "some message<br />");
    });

    function showMessage(message) {
      document.getElementById("message").innerHTML += message;
    }
  </script>
</head>
<body>
  <span id="message"></span>
</body>
</html>

可惜的是,IE不支援向setInterval的回呼傳參,IE中頁面不會展現「some message」而是「undefined」(無值傳入showMessage()),解決這個問題,可以透過閉包將期望值綁定於回呼函數裡,我們可以改寫如上程式碼:

window.addEventListener("load", function() {
  var showMessage = getClosure("some message<br />");

  window.setInterval(showMessage, 1000);
});

function getClosure(message) {
  function showMessage() {
    document.getElementById("message").innerHTML += message;
  }

  return showMessage;
}

2.模擬私有屬性
絕大多數物件導向的程式語言支援物件的私有屬性,然而js不是純正的物件導向的語言,因此也沒有私有屬性的概念。不過,我們可以透過閉包來模擬私有屬性。回想一下,閉包包含了一份其創建環境的引用,這份引用已經不在當前作用域中了,因此這份引用只能在閉包中訪問,這本質上就是私有屬性。
看如下例子(譯者:省略對程式碼的文字描述):

function Person(name) {
  this._name = name;

  this.getName = function() {
    return this._name;
  };
}

這裡有一個嚴重的問題,因為js不支援私有屬性,所以我們沒辦法阻止別人修改實例的name字段,例如我們建立一個Person實例叫Colin,然後可以將他的名字改成Tom。

var person = new Person("Colin");

person._name = "Tom";
// person.getName() now returns "Tom"

没有人愿意不经同意就被别人改名字,为了阻止这种情况的发生,通过闭包让_name字段变成私有。看如下代码,注意这里的_name是Person构造器的本地变量,而不是对象的属性,闭包形成了,因为外层方法Person对外暴露了一个内部方法getName。

function Person(name) {
  var _name = name;// 注:区别在这里

  this.getName = function() {
    return _name;
  };
}

现在,当getName被调用,能够保证返回的是最初传入类构造器的值。我们依然可以为对象添加新的_name属性,但这并不影响闭包getName最初绑定的值,下面的代码证明,_name字段,事实私有。

var person = new Person("Colin");

person._name = "Tom";
// person._name is "Tom" but person.getName() returns "Colin"

什么时候不要用闭包?

正确理解闭包如何工作何时使用非常重要,而理解什么时候不应该用它也同样重要。过度使用闭包会导致脚本执行变慢并消耗额外内存。由于闭包太容易创建了,所以很容易发生你都不知道怎么回事,就已经创建了闭包的情况。本节我们说几种场景要注意避免闭包的产生。
1.循环中
循环中创建出闭包会导致结果异常。下例中,页面上有三个按钮,分别点击弹出不同的话术。然而实际运行,所有的按钮都弹出button4的话术,这是因为,当按钮被点击时,循环已经执行完毕,而循环中的变量i也已经变成了最终值4.

<!DOCTYPE html>
<html lang="en">
<head>
  <title>Closures</title>
  <meta charset="UTF-8" />
  <script>
    window.addEventListener("load", function() {
      for (var i = 1; i < 4; i++) {
        var button = document.getElementById("button" + i);

        button.addEventListener("click", function() {
          alert("Clicked button " + i);
        });
      }
    });
  </script>
</head>
<body>
  <input type="button" id="button1" value="One" />
  <input type="button" id="button2" value="Two" />
  <input type="button" id="button3" value="Three" />
</body>
</html>

去解决这个问题,必须在循环中去掉闭包(译者:这里的闭包指的是click事件回调函数绑定了外层引用i),我们可以通过调用一个引用新环境的函数来解决。下面的代码中,循环中的变量传递给getHandler函数,getHandler返回一个闭包(译者:这个闭包指的是getHandler返回的内部方法绑定传入的i参数),独立于原来的for循环。

function getHandler(i) {
  return function handler() {
    alert("Clicked button " + i);
  };
}

window.addEventListener("load", function() {
  for (var i = 1; i < 4; i++) {
    var button = document.getElementById("button" + i);

    button.addEventListener("click", getHandler(i));
  }
});

2.构造函数里的非必要使用
类的构造函数里,也是经常会产生闭包的错误使用。我们已经知道如何通过闭包设置类的私有属性,而如果当一个方法不需要调用私有属性,则造成的闭包是浪费的。下面的例子中,Person类增加了sayHello方法,但是它没有使用私有属性。

function Person(name) {
  var _name = name;

  this.getName = function() {
    return _name;
  };

  this.sayHello = function() {
    alert("Hello!");
  };
}

每当Person被实例化,创建sayHello都要消耗时间,想象一下有大量的Person被实例化。更好的实践是将sayHello放入Person的原型链里(prototype),原型链里的方法,会被所有的实例化对象共享,因此节省了为每个实例化对象去创建一个闭包(译者:指sayHello),所以我们有必要做如下修改:

function Person(name) {
  var _name = name;

  this.getName = function() {
    return _name;
  };
}

Person.prototype.sayHello = function() {
  alert("Hello!");
};

需要记得一些事情

闭包包含了一个方法,以及创建它的代码环境引用

闭包会在外部函数包含内部函数的情况下形成

闭包可以轻松的帮助回调函数传入参数

类的私有属性可以通过闭包模拟

类的构造器中使用闭包不是一个好主意,将它们放到原型链中

以上是帶你進一步理解js閉包(詳細)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn