JavaScript閉包使用場景
閉包就是外層函數將內層函數返回出去,並且內層函數執行時帶着外層函數的作用域,可以使用外層函數內部的變量,這些變量始終保存在內存中
本質:閉包相當於橋樑,連接函數內核函數外。
特點:保存函數的誕生環境
使用原因:函數外想要獲取函數內部的變量,通過閉包形式
注意事項:閉包會將作用域保存在內存中,不用時需要將變量設置爲null,防止內存泄漏。
閉包的形式
返回值形式
函數中返回一個函數
// 返回值:直接在函數中返回外層函數的變量
function fn() {
var age = 18;
return function () {
return age;
}
}
var func = fn();
console.log(func()); // 18
函數賦值形式
將內部函數賦值給外部變量
// 函數賦值
var fn2;
function fn() {
var age = 18;
fn2 = function () {
console.log(age);
}
};
fn2(); // 18
函數參數形式
將內層函數當做參數傳入全局定義的函數中
function fnc(fn) {
age = fn();
console.log("已經"+age);
}
function fn() {
var age = 18;
fn2 = function () {
return age;
}
fnc(fn2);
};
fn(); // 已經18
IIFE自執行函數
使用自執行函數,省去調用過程
上文自執行函數介紹
function fnc(fn) {
age = fn();
console.log("已經"+age);
}
(function fn() {
var age = 18;
fn2 = function () {
return age;
}
fnc(fn2);
})();
相關代碼:
循環賦值問題:查看代碼
封裝私有方法:查看代碼
計數器和迭代器:查看代碼
實現緩存機制:查看代碼
閉包用途
計數器
計數器
通過閉包製作計數器
作用:讀取函數內部的變量,這些變量始終保存在內存中
function fn() {
count = 0;
return function () {
++count;
console.log("這是第"+count+"個函數;");
}
}
var ins = fn();
ins(); // 這是第1個函數;
ins(); // 這是第2個函數;
ins = null; // 防止內存泄漏
迭代器
類似於計數器
// 傳入一個數組,返回一個迭代器
function initIter(iter) {
arr = iter;
count = 0;
return {
next:function () {
if(count < arr.length){
return {
value:arr[count++],
done:false
}
}
return {
value:undefined,
done:true
}
}
}
}
var arr = ["apple","banana","orange"];
var it = initIter(arr);
console.log(it.next()); // {done: false,value: "apple"}
console.log(it.next()); // {done: false,value: "banana"}
console.log(it.next()); // {done: false,value: "orange"}
console.log(it.next()); // {done: true,value: undefined}
自定義屬性
通過設置函數屬性,來保存信息
function add() {
return add.count++;
}
add.count = 0
console.log(add()); // 0
console.log(add()); // 1
封裝私有屬性和方法
// 閉包存儲私有屬性和方法
function Person() {
var age;
var setAge = function (n) {
if(Number(n)){
age = n;
}
return age;
};
var getAge = function () {
return age;
};
return {
setAge:setAge,
getAge:getAge
}
}
// 函數只能使用其提供的兩個接口來操作age的值
var p1 = Person();
p1.setAge(18);
console.log(p1.getAge());
p1 = null;
閉包條件:函數嵌套、訪問所在的作用域、在所在的作用域外被調用
自執行函數IIFE
自執行函數介紹
自執行函數,通過自執行函數,可以省去閉包初始化過程。
同樣,自執行函數內部作用域和外部隔開的,不屬於全局作用域,而是函數作用域
// 自執行函數 IIFE
// 方法一:作爲函數表達式
(function () {
})();
// 方法二:作爲匿名函數
(function () {
}());
//注意加分號
例如:封裝私有屬性和方法
// 閉包存儲私有屬性和方法
var p1 = (function Person() {
var age;
var setAge = function (n) {
if(Number(n)){
age = n;
}
return age;
};
var getAge = function () {
return age;
};
return {
setAge:setAge,
getAge:getAge
}
})();
p1.setAge(18);
console.log(p1.getAge());
p1 = null;
緩存機制
緩存機制介紹
沒有緩存機制時,每次使用都會重複調用函數,影響性能
// 沒有緩存機制時
function factorial(n) {
if(n < 0){
throw RangeError("負數沒有階乘");
}
var res = 1;
for(var i = 1;i <= n;i++){
res *= i;
}
return res;
}
console.log(factorial(0));
console.log(factorial(1));
console.log(factorial(2));
添加緩存機制後
function factorial() {
var fac_arr = {}; // 緩存:用來存儲計算結果
// 計算階乘函數
function calculate(n) {
if(n < 0){
throw RangeError("負數沒有階乘");
}
var res = 1;
for(var i = 1;i <= n;i++){
res *= i;
}
return res;
}
return function (n) {
// 判斷之前是否計算過,計算過直接取值,沒有則計算,存儲到緩存中
if(n in fac_arr){
return fac_arr[n];
}else{
var res = calculate(n);
fac_arr[n] = res;
return res;
}
}
}
var fac = factorial()
console.log(fac(0)); // 結果:1 fac_arr值{0: 1}
console.log(fac(10)); // 結果:3628800 fac_arr值{0: 1, 10: 3628800}
console.log(fac(10)); // 結果:3628800 fac_arr值{0: 1, 10: 3628800}
這個階乘函數可以,更進一步,每計算一次,緩存一次
function factorial() {
var fac_arr = [1,]; // 緩存:記錄0-最大階乘之間所有階乘
var max_fac = 0; // 記錄最大階乘
// 計算階乘函數
function calculate(n) {
console.log(fac_arr);
if(n < 0){
return RangeError("負數沒有階乘");
}else if(n <= max_fac){ // 獲取的值在該範圍內,直接返回
return fac_arr[n];
}else{
for(var i = max_fac;i < n;i++){
fac_arr.push(fac_arr[i] * (i+1));
}
max_fac = n;
}
return fac_arr[n];
}
return calculate
}
var fac = factorial()
console.log(fac(20)); // 將1,2,3.。。20階乘全部計算存儲下來
console.log(fac(10)); // 直接從緩存中獲取
console.log(fac(15)); // 直接從緩存中獲取
優點:計算一次大的階乘之後,0-20之間所有階乘都存儲下來。下次想要0-20之間的階乘直接獲取,不用重複計算。
缺點:當階乘過大時,緩存存儲的階乘太多,消耗容量
循環閉包的錯誤結果
循環賦值
當我們在循環中使用閉包時,注意閉包的作用域
<ul id="myList">
<li>你好</li>
<li>你好</li>
<li>你好</li>
<li>你好</li>
<li>你好</li>
</ul>
<script>
//點擊li標籤時,獲取li索引
var myList = document.getElementById("myList");
var lis = myList.children; // 獲取所有的li標籤
// 循環li標籤
for(var i = 0;i < lis.length;i++){
lis[i].onclick = function () {
this.innerHTML = i;
console.log(i);
}
}
console.log(i); // 5
</script>
運行上述代碼時,會發現無論點擊哪個li標籤,返回的都是索引5.
原因:
1.onclick點擊事件設置後不會立即執行,觸發點擊事件時纔會調用事件
2.在事件中查找i變量,因爲js中{}不爲獨立的作用域,每次循環使用同一個i,所以循環結束後i變量值爲5。 即每次循環的i都是同一個i,相當於
// 循環li標籤
var i;
for(i = 0;i < lis.length;i++){
lis[i].onclick = function () {
this.innerHTML = i;
console.log(i);
}
}
console.log(i); // 5
解決辦法一:let變量
使用es6的let創建變量
//點擊li標籤時,獲取li索引
var myList = document.getElementById("myList");
var lis = myList.children; // 獲取所有的li標籤
// 循環li標籤
for(let i = 0;i < lis.length;i++){
lis[i].onclick = function () {
this.innerHTML = i;
console.log(i);
}
}
console.log(i); //Uncaught ReferenceError: i is not defined
let變量會讓{}變爲獨立作用域,即每循環一次,生成一個單獨的作用域,每個作用域中都會創建一個i。
查找變量時,每個i的值不同。循環外部也不能訪問i變量。
解決辦法二:閉包
因爲閉包可以保存數值,我們可以通過閉包來保存i的值
//點擊li標籤時,獲取li索引
var myList = document.getElementById("myList");
var lis = myList.children; // 獲取所有的li標籤
// 循環li標籤
for(let i = 0;i < lis.length;i++){
lis[i].onclick = function () {
return (function (n) {
lis[i].innerHTML = i;
console.log(n);
})(i);
}
}
在js中,{}不能算作獨立作用域,函數內部才能算獨立作用域。(除了es6聲明let、const等)
所以使用自執行函數,並且將i值傳入函數,保存下來。
圖片上報
因爲img圖片對象,在src獲取url後,會下載圖片,但是當前函數執行完畢後,變量會被銷燬,導致丟失圖片數據。
使用閉包解決
function report2() {
var imgs = [];
return function (src) {
var img = new Image();
img.src = src;
imgs.push(img);
return img;
}
}
var rp = report2();
var img2 = rp("../photo.jpg");
App.appendChild(img2);