1. 程式人生 > >this、apply/call、bind、閉包、函數、變量復制

this、apply/call、bind、閉包、函數、變量復制

報錯 regex 變量提升 blog .get 定義 技術 highlight ini

一、實際場景中抽象出的一個問題

下面this各指向什麽?

          var a = {                    
             b: function() {
                console.log(this);
             },
            
             f: function() {            
              var c = this.b;
              c();
            }
        };
        
        a.b();
        a.f();

第一個this指向a,第二個this指向window。(做對了嗎)

二、JavaScript中變量復制的問題

變量拷貝分為值拷貝和引用類型數據拷貝

一個變量向另一個變量復制基本類型數據值時,另一個變量會在自己所占的內存中保存一份屬於自己的數據值。

一個變量向另一個變量復制引用類型數據值時,實質上復制的是一個指針變量,這個指針指向堆內存中的對象,復制之後這兩個變量指向同一個對象。

Es5中基本數據類型包括string、number、null、undefined、boolean

引用類型包括Array、RegExp、Date、Function等

( 基本包裝類型:String、Boolean、Number; 單體內置對象:Global、Math)

三、函數

創建一個函數的方式: 函數聲明、函數表達式、通過Function構造函數創建,它接受任意數量的參數,最後一個參數始終被看成是函數體。

(例如:new Function(“num1”, “num2”, “return num1+num2”))

函數聲明有聲明提升的過程,解析器在向執行環境中加載數據時會優先讀取函數聲明,保證它在執行任何代碼之前都可用,而函數表達式則是在解析器

執行到它所在的代碼行時才被解析執行。

      var condition = false;
      console.log( afun(100) );//undefined
      if (condition) {
	      function afun(num) {
	      	return num + 100;
	      }
      }
      else {
	      function afun(num) {
	      	return num + 200;
	      }
      }	  

  上面的代碼,函數調用之前還沒有做判斷,所以還沒有afun函數

      var condition = false;    
      if (condition) {
	      function afun(num) {
	      	return num + 100;
	      }
      }
      else {
	      function afun(num) {
	      	return num + 200;
	      }
     }	  
	  console.log( afun(100) );//300
	  

  js中沒有塊級作用域的概念,上面的代碼可以正常調用

js中函數也是一個對象,函數名是一個指針,所以在將一個函數的函數名復制給另一個變量時,這個變量也指向了這個函數

    if (condition) {
	 var bfun = function(num) {
	      return num + 100;
	 }
    }
    else {
         var bfun = function(num) {
	     return num + 200;
         }
    }	
	
       var cfun = bfun;
       bfun = null;
//     console.log( bfun(100) );//報錯
       console.log( cfun(200) );//400
       

驗證函數拷貝之後,被賦值的變量和賦值的變量指向的是同一個函數,函數表達式和函數聲明定義的函數在復制時是一樣的,復制之後兩個變量指向同一函數。

復制之後,添加新的bfun,這個新的bfun覆蓋原來的bfun,cfun同樣也改變了,說明被賦值的變量和賦值的變量指向的是同一個函數。

        function bfun(num) {
	     return num + 200;
        }         
        var cfun = bfun;
	function bfun(num) {
	     return num*1000;
	} ;
       console.log( bfun(100) );//100000
       console.log( cfun(200) );//200000
      

  但是當采用函數表達式的形式(在用函數表達式定義的bfun函數,bfun也是一個全局變量,這裏沒有用var定義變量,規範寫法應該加上var) 再次定義一個同名的函數時,

如下:

       function bfun(num) {
	  return num + 200;
       }         
        var cfun = bfun;    
        bfun = function(num) {
	    return num*1000;
        } ;
       console.log( bfun(100) );//100000
       console.log( cfun(200) );//300
      

函數表達式定義的函數沒有覆蓋函數聲明定義的同名函數,在JavaScript中函數也是一個對象,函數名是一個指向函數的的變量指針,函數名也是一個變量,

在JavaScript中有一條規則是,函數聲明會覆蓋和其函數名同名的變量的聲明,但是不會覆蓋同名的變量的賦值,不管它們定義的順序如何。在用函數表達式定

義的bfun函數中,bfun也是一個變量,只不過它被賦的值是一個匿名函數,一個變量的值可以是任何類型(string、number、boolean、null、undefined、一個

函數、一個復雜類型數據(對象)等)。

驗證:函數聲明會覆蓋和其函數名同名的變量的聲明,但是不會覆蓋同名變量的賦值

       bfun = function(num) {
	     return num*1000;
       } ;
	    
        var cfun = bfun;
        
        function bfun(num) {
	     return num + 200;
        }
	   
       console.log( bfun(100) );//100000
       console.log( cfun(200) );//200000

再看一段代碼,最後結果為多少?

 bfun = function(num) {
return num*1000; } ; var cfun = bfun; bfun = function(num) { return num + 200;
} console.log( bfun(100) ); console.log( cfun(200) );

最後結果為300、2000,後面的bfun覆蓋了前面的bfun,在後面調用bfun時調用的是第二個,而cfun仍然為第一個bfun的值,why?

不是說函數是一個對象,函數名相當於一個指針,指向的是函數,在復制之後兩個變量會指向同一個函數嗎?

當bfun是采用函數聲明的形式定義的時候,後面函數聲明定義一個相同的同名函數之後,bfun改變,cfun會隨之改變。而這裏的bfun是采用函數表達式定義的函數,

又會有什麽不同?

我的理解是,函數表達式定義的函數是沒有函數名的,在復制的時候復制的是一個具體值,這裏復制的就是一個匿名函數,而不是像函數聲明復制一樣復制的是一個

內存地址(指針),

函數表達式拷貝,就等同於基本數據類型變量拷貝,變量與變量之間各保存了一份屬於自己的值,其中一個變量的值改變不會影響另外一個。

函數的內存分配(無論是函數聲明還是函數表達式定義的函數):

技術分享

函數表達式與函數聲明不同的是,bfun再重新被賦值一個匿名函數之後,此時bfun指向一個新的對象(有點類似於原型上采用字面量的方式定義屬性和方法一樣,

此時指向的是一個新對象),cfun還是指向原來的匿名函數。也就是函數聲明方式定義的函數,定義同名的函數,同名函數會覆蓋原來的函數;而函數表達式定義

的函數不會被同名函數覆蓋,定義同名函數之後原來的函數仍然存在。

函數是一個對象,所以可以在函數名上定義屬性和方法:

        bfun = function(num) {
	    return num*1000;
        } ;
	   
        var cfun = bfun;
        bfun.a = 7777;
        console.log(bfun.a);//777
        bfun.b = function(num) {
	     return num + 200;
	}
       console.log(typeof bfun);//function
	   
       console.log(bfun.a);//777
       console.log( bfun(100) );	//10000    
       console.log( bfun.b(100) ); //300

驗證:函數表達式定義的函數不會被同名函數覆蓋,定義同名函數之後原來的函數仍然存在

       bfun = function(num) {
	    return num*1000;
       } ;
	   
        var cfun = bfun;
        bfun.a = 7777;
        console.log(bfun.a);//777
        bfun = function(num) {
	     return num + 200;
        }
        console.log(typeof bfun);//function
	   
       console.log(bfun.a);//undefined
       console.log( bfun(100) );	//300   
       console.log( cfun(200) );//200000
       console.log(cfun.a);//777  

函數是一個對象,所以可以在函數對象上添加屬性和方法,這樣看似乎對象不一定就是存儲在堆內存中了?函數表達式中的匿名函數這樣的函數對象就是存在棧中啊 

函數名是一個指針,指向函數,函數是一個對象,存在堆內存中。

四、閉包 

        var a = {                    
             b: function() {
                console.log(this);
             },
            
             f: function() {            
              var c = this.b;
              c();
            }
        };      
        a.b();
        a.f();

通過上面的分析,c這裏和b指向的是同一個函數,但是為什麽this的指向不同?問題在於調用(直接調用b時this為a,在f中將b復制給c之後,在調用c,this指向window)

函數的方式不一樣,結果執行環境也不同。

這個例子實質上類似於(this===window):

             var object = {
		    _name : "my object",
		    getNameFunc : function() {
		        
		       var that = this;
                       var a = function() {
		           console.log("value:"+this._name);//undefined		
		        };		        
		         a();                
		    }
		};
          console.log(object.getNameFunc());

  同樣也類似於(this===window):

	     var object = {
		    _name : "my object",
		    getNameFunc : function() {
		         
		        return function() {
//		        	this._name = "yyy";
		        	console.log(this);//window
		                return this._name;//undefined		
		        };
                              
		   }
	     };
          console.log(object.getNameFunc()()); 

上面三個例子都是在函數中創建了一個閉包函數

閉包: 一個有權訪問另一個函數中的變量的函數就是閉包,常見的閉包就是一個函數中包含另一個函數

函數沒有利用對象調用時,this指向window;以上 閉包中的this指向window,這又引申到this指向的問題。

五、this的指向問題 (見:https://www.zhihu.com/question/19636194)

調用函數的幾種方式以及當前this的指向問題:

直接通過函數名調用(無論是在哪裏調用),此時this指向window

通過對象點函數名的形式調用,this指向函數的直接調用者

new關鍵字調用構造函數,this指向new出來的這個對象

通過apply或call方法調用,this指向第一個參數中的對象,如果第一個參數為null,this指向window

還可以通過bind()方法來改變函數的作用域,它返回的是一個函數的實例,最終需要通過這個實例來調用函數(apply和call方法是直接調用函數)

通過apply和call方法用來改變函數的作用域,這是每個函數中都包含的兩個非繼承的方法。

 function a(num, num2) {

 } 

當通過函數名直接調用一個函數時a(1,2),這個相當於a.apply(null, arguments),第一個參數是為null,所以this指向window

 var b = {      
       function a(num, num2) {

       }
 }

當通過對象調用一個函數時b.a(1,2),這個相當於a.apply(b, arguments),第一個參數是為b,所以this指向b

當把調用函數的方法換寫成apply或call的形式就很好理解this的指向了

關於直接調用函數和通過new關鍵調用構造函數的區別,如下:

考察不同調用方式this的指向問題和變量提升的問題

                       var a=10;
			function test(){
				a=5;
				alert(a);
				alert(this.a);
				var a;
				alert(this.a);
				alert(a);
			}
			
			test(); 
			new test();
			// 5 10 10 5
			//5 undefined undefined 5

  test()方法,this指向window,test中用var定義了一個a,它將被提升到執行環境的最前端,a=5

new test(),此時test是個構造函數,this.a沒有被定義所以為undefined

總結:

上面的問題的實際場景如下:

實際項目中抽象出來的一部分,用來統一給dom元素添加事件的寫法,大大提升了代碼的可維護性

		var util = {
		     maps: {
			  ‘click #btn‘: ‘clickBtn‘
		     },
		     
		     stop: function() {
		     	console.log("stop....");
		     },
		     
		      clickBtn: function(event) {
				var e = event;
				var target = e.target || e.srcElement;
				console.log(target.id);             
				this.stop();
		       },
			 
		      _scanEventsMap: function(maps, isOn) {
			  	
		        var delegateEventSplitter = /^(\S+)\s*(.*)$/;
                        var bind = isOn ? this._delegate.bind(this) : this._undelegate.bind(this);
//	     	        this._delegate(‘click‘, "#btn", this["clickBtn"]);
//	     	        bind(‘click‘, "#btn", "clickBtn");                   
	                for (var keys in maps) {	            	
	                  if (maps.hasOwnProperty(keys)) {
	                    var matchs = keys.match(delegateEventSplitter);
	                    bind(matchs[1], matchs[2], maps[keys]);
	                  }
	               }
		    },  
		    
	        _delegate: function(name, selector, func) {
	            var ele = document.querySelector(selector),
	                that = this,
	                func = this[func];
	                console.log(func);
		    	ele.addEventListener(name, function(event) {
		              var e = event || window.event;   				    	
		    	      func.apply(that, arguments);
		    	}, false);
	        },
	       
	       _undelegate: function(name, selector, func) {
	           var ele = $(selector);
		     ele.removeEventListener(name, func, false);
               },
          
             _init: function() {
          	 this._scanEventsMap(this.maps, true);
             }
       }		
       util._init();

  

this、apply/call、bind、閉包、函數、變量復制