六、閉包的作用
閉包:內部函數保存到外部
當內部函數被保存到外部時,將會生成閉包。 閉包會導致原有作用域鏈不釋放,造成內存泄漏(內存佔用)
一)閉包的作用
- 實現公有變量: eg:函數累加器
- 可以做緩存(存儲結構):eg:eater
- 可以實現封裝,屬性私有化:eg:new Person();
- 模塊化開發,防止污染全局變量
二)閉包作用舉例
1、累加器:
題目:定義一個定時器,計算點擊網頁的次數 這個題目非常簡單,想必大家都能寫出來。
var count = 0;
function addCount() {
count++;
}
document.body.addEventListener("click", addCount);
複製代碼
count作爲一個全局變量,其他地方都可以對它進行操作,如果其他地方對count重新賦值或者重新定義count,那麼這個計時器就被破壞了。這時候,閉包就起作用了。
function addCount() {
var count = 0;
var addCount = function() {
count++;
}
return addCount;
}
document.body.addEventListener("click", addCount);
點擊一次->輸出1
點擊兩次->輸出2
複製代碼
2、緩存:
function eater(){
var food="apple";
var obj={
eat:function (){
if(food!=""){
console.log("i am eating "+ food);
food="";
}else{
console.log("eat emtpy ");
}
}
push:function(myFood){
food = myFood;
}
}
return obj;
}
var eat1= eater();
eat1.eat();->輸出apple
eat1.eat();->輸出empty
eat1.push('banana');
eat1.eat();->輸出banana
複製代碼
看另外一個緩存的例子:
function isFirstLoad(){
var list=[];
return function(option){
if(list.indexOf(option)>=0){
//檢測是否存在於現有數組中,有則說明已存在
console.log('已存在')
}else{
list.push(option);
console.log('首次傳入');
//沒有則返回true,並把這次的數據錄入進去
}
}
}
var ifl=isFirstLoad();
ifl("zhangsan");
ifl("lisi");
ifl("zhangsan");
複製代碼
在瀏覽器控制檯打印如下:
可以看到,如果外界想訪問list變量,只能通過我定義的函數isFirstLoad來進行訪問,我對想訪問list的外界只提供了isFirstLoad這一個接口。至於怎麼操作_list,我已經定義好了,外界能做的就只是使用我的函數,然後傳幾個不同的參數罷了。
最後順便說一下,作用域鏈是在定義的時候就已經確定了,和誰來執行,什麼時候執行均沒有一毛錢關係。
3. 私有化變量:下面例子輸入deng.prepareWife是undefind 形成了私有化變量
function Deng(name,wife){
//正常情況 函數裏的var對象 函數執行完了就會被銷燬,
但是在這裏因爲被this.divorce的函數使用被返回了形成了閉包,所以無法銷燬。
var prepareWife="xiaozhang";
this.name=name;
this.wife=wife;
this.divorce=function(){
this.wife=prepareWife;
}
this.changePrepareWife=function(target){
prepareWife=target;
}
this.sayPraprewife=function(){
console.log(prepareWife);
}
}
var deng=new Deng('deng','xiaoliu');
複製代碼
模擬實現類的私有屬性的例子:
function Boy(name){
this.name = name;
var sex = 'boy';
this.saySex = function(){
console.log("my sex is "+sex)
};
}
var xiaoming = new Boy('xiaoming');
console.log(xiaoming.name);
console.log(xiaoming.sex);
xiaoming.saySex();
VM344:16 xiaoming
VM344:18 undefined
VM344:9 my sex is boy
複製代碼
4. 立即執行函數解決閉包作用域問題:
立即執行函數(執行完立即銷燬) 針對初始化功能的函數 定義:此類函數沒有生命,在一起執行過後釋放。適合做初始化工作。
function test(){
var arr=[];
for(var i=0;i<10;i++){
(function (j){
arr[j]=function(){
document.write(j+" ");//輸爲0,1,2,3,4,5,6,7,8,9
}
}(i));
//用立即執行函數(立即執行函數也可以生成自己的作用域)
i作爲參數傳給j j不會隨着i改變而改變
}
return arr;
}
var myArr=test();
for(var j=0;j<10;j++){
myArr[j]();
}
複製代碼
<ul>
<li id="myli">a</li>
<li id="myli">a</li>
<li id="myli">a</li>
</ul>
<script type="text/javascript">
function test(){
var liList=document.getElementsByTagName("li");
for (i = 0; i < liList.length; i++) {
(function(j){
liList[j].onclick=function(){
console.log(j); //輸出1 2 3 如果不用立即執行函數會報錯
}
}(i))
}
}
test();
複製代碼
解決for循環中點擊事件的問題舉例 我們先來看一段代碼
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
複製代碼
var lis = document.getElementsByTagName("li");
for (var i = 0; i < lis.length; i++) {
lis[i].onclick = function() {
console.log(i);
}
}
複製代碼
運行這段代碼後,大家或許會有疑問,爲什麼點擊任一個li標籤,console臺打印的都是3。 代碼運行結束後,給每個li標籤定義了click函數,但這個函數沒有立即執行,只有當點擊li時,纔會執行該click函數;當點擊li執行函數時,函數中的變量i沒有在函數中定義,根據js的作用域鏈原則,會繼續向上級作用域查詢,因此找到了全局作用域中的i,這時for循環已經執行結束,此時全局作用域中的i已經變爲了3,故打印出來的當然是3了。 要想實現,打印出來的值爲點擊li的順序值,這時,閉包又起到了作用
var lis = document.getElementsByTagName("li");
for (var i = 0; i < lis.length; i++) {
lis[i].onclick = (function(i) {
var clickLi = function() {
console.log(i);
}
return clickLi;
})(i)
}
複製代碼
在for循環執行時,立即將當前的i值作爲形參傳入clickLi中,而形參默認爲函數內的局部變量,函數外部是不能對i進行操作的。所以,當點擊li時,執行clickLi函數時,打印出來的則是li的順序值。
三)閉包的缺點及解決
缺點:函數執行完後,函數內的局部變量沒有釋放,佔用內存時間會變長,容易造成內存泄露。
解決:能不用閉包就不用,及時釋放。比如:
f = null; // 讓內部函數成爲垃圾對象 -->回收閉包
複製代碼
總而言之,你需要它,就是優點;你不需要它,就成了缺點。
七、閉包的注意事項
1. 內存泄漏內存溢出
內存泄漏:佔用的內存沒有及時釋放。內存泄露積累多了就容易導致內存溢出。
常見的內存泄露:
- 意外的全局變量
- 沒有及時清理的計時器或回調函數
- 閉包
情況1舉例:意外的全局變量
function fn() {
a = new Array(10000000);
console.log(a);
}
fn();
複製代碼
情況2舉例:沒有及時清理的計時器或回調函數
var intervalId = setInterval(function () { //啓動循環定時器後不清理
console.log('----')
}, 1000)
// clearInterval(intervalId); //清理定時器
複製代碼
情況3舉例:閉包占用內存
<script type="text/javascript">
function fn1() {
var arr = new Array[100000]; //這個數組佔用了很大的內存空間
function fn2() {
console.log(arr.length)
}
return fn2
}
var f = fn1()
f()
f = null //讓內部函數成爲垃圾對象-->回收閉包
</script>
複製代碼
2. 關於閉包當中的this對象
this對象指的是什麼,這個要看函數所運行的環境。如果函數在全局範圍內調用 ,函數內的this指向的是window對象。對象中的方法,通過閉包如果運行的環境爲window時,則this爲window。因爲閉包並不是該對象的方法。
var color="red";
function fn(){
return this.color;
}
var obj={
color:"yellow",
fn:function(){
return function(){//返回匿名函數
return this.color;
}
}
}
console.log(fn());//red 在外部直接調用this爲window
var b=obj.fn();//b爲window下的變量,獲得的值爲obj對象下的fn方法返回的匿名函數
console.log(b());//red 因爲是在window環境下運行,所以this指缶的是window
//可以通過call或apply改變函數內的this指向
console.log(b.call(obj));//yellow
console.log(b.apply(obj));//yellow
console.log(fn.call(obj));//yellow
複製代碼
通過變量可以獲得上一個作用域中的this指向
var color="red";
function fn(){
return this.color;
}
var obj={
color:"yellow",
fn:function(){
var _this=this;//將this賦值給變量_this
return function(){
return _this.color;//通過_this獲得上一個作用域中的this指向
}
}
}
console.log(fn());//red
var b=obj.fn();
console.log(b());//yellow
複製代碼
可以通過構造方法傳參來訪問私有變量
function Desk(){
var str="";//局部變量str,默認值爲""
this.getStr=function(){
return str;
}
this.setStr=function(value){
str=value;
};
}
var desk=new Desk();
//爲構造函數的局部變量寫入值。
desk.setStr("zhangPeiYue");
//獲取構造函數的局部變量
console.log(desk.getStr());//zhangPeiYue
作者:前端娃娃
鏈接:https://juejin.im/post/5d50d5ddf265da03a715c59c
來源:掘金
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。