this、apply/call、bind、閉包、函數、變量復制
一、實際場景中抽象出的一個問題
下面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、閉包、函數、變量復制