js 的 this 綁定問題,讓多數新手懵逼,部分老手以爲噁心,這是由於this的綁定 ‘難以捉摸’,出錯的時候還每每不知道爲何,至關反邏輯。
讓咱們考慮下面代碼:面試
var people = { name : "海洋餅乾", getName : function(){ console.log(this.name); } }; window.onload = function(){ xxx.onclick = people.getName; };
在平時搬磚時比較常見的this綁定問題,你們可能也寫給或者遇到過,當xxx.onclick觸發時,輸出什麼呢 ?數組
爲了方便測試,我將代碼簡化:app
var people = { Name: "海洋餅乾", getName : function(){ console.log(this.Name); } }; var bar = people.getName; bar(); // undefined
經過這個小例子帶你們感覺一下this
噁心的地方,我最開始遇到這個問題的時候也是一臉懵逼,由於代碼裏的this
在建立時指向很是明顯啊,指向本身 people
對象,可是實際上指向 window
對象,這就是我立刻要和你們說的 this
綁定規則。函數
this
什麼是this
?在討論this
綁定前,咱們得先搞清楚this表明什麼。測試
this
綁定規則掌握了下面介紹的4種綁定的規則,那麼你只要看到函數調用就能夠判斷 this
的指向了。this
考慮下面代碼:prototype
function foo(){ var a = 1 ; console.log(this.a); // 10 } var a = 10; foo();
這種就是典型的默認綁定,咱們看看foo調用的位置,」光桿司令「,像 這種直接使用而不帶任何修飾的函數調用 ,就 默認且只能 應用 默認綁定。code
那默認綁定到哪呢,通常是window
上,嚴格模式下 是undefined
。對象
代碼說話:繼承
function foo(){ console.log(this.a); } var obj = { a : 10, foo : foo } foo(); // ? obj.foo(); // ?
答案 : undefined 10
foo()
的這個寫法熟悉嗎,就是咱們剛剛寫的默認綁定,等價於打印window.a
,故輸出undefined
,
下面obj.foo()
這種你們應該常常寫,這其實就是咱們立刻要討論的 隱性綁定 。
函數foo執行的時候有了上下文對象,即 obj
。這種狀況下,函數裏的this默認綁定爲上下文對象,等價於打印obj.a
,故輸出10
。
若是是鏈性的關係,好比 xx.yy.obj.foo();
, 上下文取函數的直接上級,即緊挨着的那個,或者說對象鏈的最後一個。
在咱們剛剛的 隱性綁定中有一個致命的限制,就是上下文必須包含咱們的函數 ,例:var obj = { foo : foo }
,若是上下文不包含咱們的函數用隱性綁定明顯是要出錯的,不可能每一個對象都要加這個函數 ,那樣的話擴展,維護性太差了,咱們接下來聊的就是直接 給函數強制性綁定this。
這裏咱們就要用到 js 給咱們提供的函數 call 和 apply,它們的做用都是改變函數的this指向,第一個參數都是 設置this對象。
兩個函數的區別:
例如:
function foo(a,b){ console.log(a+b); } foo.call(null,'海洋','餅乾'); // 海洋餅乾 這裏this指向不重要就寫null了 foo.apply(null, ['海洋','餅乾'] ); // 海洋餅乾
除了 call,apply函數之外,還有一個改變this的函數 bind ,它和call,apply都不一樣。
bind只有一個函數,且不會馬上執行,只是將一個值綁定到函數的this上,並將綁定好的函數返回。例:
function foo(){ console.log(this.a); } var obj = { a : 10 }; foo = foo.bind(obj); foo(); // 10
(bind函數很是特別,下次和你們一塊兒討論它的源碼)
開始正題,上代碼,就用上面隱性綁定的例子 :
function foo(){ console.log(this.a); } var obj = { a : 10 //去掉裏面的foo } foo.call(obj); // 10
咱們將隱性綁定例子中的 上下文對象 裏的函數去掉了,顯然如今不能用 上下文.函數
這種形式來調用函數,你們看代碼裏的顯性綁定代碼foo.call(obj)
,看起來很怪,和咱們以前所瞭解的函數調用不同。
其實call 是 foo 上的一個函數,在改變this指向的同時執行這個函數。
(想要深刻理解 [call apply bind this硬綁定,軟綁定,箭頭函數綁定
] 等更多黑科技 的小夥伴歡迎關注我或本文的評論,最近我會單獨作一期放到一塊兒寫一篇文章)(不想看的小夥伴不用擔憂,不影響對本文的理解)
new
學過面向對象的小夥伴對new確定不陌生,js的new和傳統的面嚮對象語言的new的做用都是建立一個新的對象,可是他們的機制徹底不一樣。
建立一個新對象少不了一個概念,那就是構造函數
,傳統的面向對象 構造函數 是類裏的一種特殊函數,要建立對象時使用new 類名()
的形式去調用類中的構造函數,而js中就不同了。
js中的只要用new修飾的 函數就是'構造函數',準確來講是 函數的構造調用
,由於在js中並不存在所謂的'構造函數'。
那麼用new 作到函數的構造調用
後,js幫咱們作了什麼工做呢:
__proto__
屬性指向 原函數的prototype
屬性。(即繼承原函數的原型)第三條就是咱們下面要聊的new綁定
不嗶嗶,看代碼:
function foo(){ this.a = 10; console.log(this); } foo(); // window對象 console.log(window.a); // 10 默認綁定 var obj = new foo(); // foo{ a : 10 } 建立的新對象的默認名爲函數名 // 而後等價於 foo { a : 10 }; var obj = foo; console.log(obj.a); // 10 new綁定
使用new調用函數後,函數會 以本身的名字 命名 和 建立 一個新的對象,並返回。
特別注意 : 若是原函數返回一個對象類型,那麼將沒法返回新對象,你將丟失綁定this的新對象,例:
function foo(){ this.a = 10; return new String("搗蛋鬼"); } var obj = new foo(); console.log(obj.a); // undefined console.log(obj); // "搗蛋鬼"
過程是些無聊的代碼測試,我直接寫出優先級了
new 綁定 > 顯示綁定 > 隱式綁定 > 默認綁定
若是函數被new
修飾
this綁定的是新建立的對象,例:var bar = new foo(); 函數 foo 中的 this 就是一個叫foo的新建立的對象 , 而後將這個對象賦給bar , 這樣的綁定方式叫 new綁定 .
若是函數是使用call,apply,bind
來調用的
this綁定的是 call,apply,bind 的第一個參數.例: foo.call(obj); , foo 中的 this 就是 obj , 這樣的綁定方式叫 顯性綁定 .
若是函數是在某個 上下文對象 下被調用
this綁定的是那個上下文對象,例 : var obj = { foo : foo }; obj.foo(); foo 中的 this 就是 obj . 這樣的綁定方式叫 隱性綁定 .
若是都不是,即便用默認綁定
例:function foo(){...} foo() ,foo 中的 this 就是 window.(嚴格模式下默認綁定到undefined). 這樣的綁定方式叫 默認綁定 .
1.
var x = 10; var obj = { x: 20, f: function(){ console.log(this.x); // ? var foo = function(){ console.log(this.x); } foo(); // ? } }; obj.f();
-----------------------答案---------------------
答案 : 20 10
解析 :考點 1. this默認綁定 2. this隱性綁定
var x = 10; var obj = { x: 20, f: function(){ console.log(this.x); // 20 // 典型的隱性綁定,這裏 f 的this指向上下文 obj ,即輸出 20 function foo(){ console.log(this.x); } foo(); // 10 //有些人在這個地方就想固然的以爲 foo 在函數 f 裏,也在 f 裏執行, //那 this 確定是指向obj 啊 , 仔細看看咱們說的this綁定規則 , 對應一下很容易 //發現這種'光桿司令',是咱們一開始就示範的默認綁定,這裏this綁定的是window } }; obj.f();
2.
function foo(arg){ this.a = arg; return this }; var a = foo(1); var b = foo(10); console.log(a.a); // ? console.log(b.a); // ?
-----------------------答案---------------------
答案 : undefined 10
解析 :考點 1. 全局污染 2. this默認綁定
這道題頗有意思,問題基本上都集中在第一undefined上,這實際上是題目的小陷阱,可是追棧的過程絕對精彩
讓咱們一步步分析這裏發生了什麼:
本題中全部變量的值,a = window.a = 10 , a.a = undefined , b = window , b.a = window.a = 10;
3.
var x = 10; var obj = { x: 20, f: function(){ console.log(this.x); } }; var bar = obj.f; var obj2 = { x: 30, f: obj.f } obj.f(); bar(); obj2.f();
-----------------------答案---------------------
答案:20 10 30
解析:傳說中的送分題,考點,辨別this綁定
var x = 10; var obj = { x: 20, f: function(){ console.log(this.x); } }; var bar = obj.f; var obj2 = { x: 30, f: obj.f } obj.f(); // 20 //有上下文,this爲obj,隱性綁定 bar(); // 10 //'光桿司令' 默認綁定 ( obj.f 只是普通的賦值操做 ) obj2.f(); //30 //無論 f 函數怎麼折騰,this只和 執行位置和方式有關,即咱們所說的綁定規則
4. 壓軸題了
function foo() { getName = function () { console.log (1); }; return this; } foo.getName = function () { console.log(2);}; foo.prototype.getName = function () { console.log(3);}; var getName = function () { console.log(4);}; function getName () { console.log(5);} foo.getName (); // ? getName (); // ? foo().getName (); // ? getName (); // ? new foo.getName (); // ? new foo().getName (); // ? new new foo().getName (); // ?
-----------------------答案---------------------
答案:2 4 1 1 2 3 3
解析:考點 1. new綁定 2.隱性綁定 3. 默認綁定 4.變量污染
function foo() { getName = function () { console.log (1); }; //這裏的getName 將建立到全局window上 return this; } foo.getName = function () { console.log(2);}; //這個getName和上面的不一樣,是直接添加到foo上的 foo.prototype.getName = function () { console.log(3);}; // 這個getName直接添加到foo的原型上,在用new建立新對象時將直接添加到新對象上 var getName = function () { console.log(4);}; // 和foo函數裏的getName同樣, 將建立到全局window上 function getName () { console.log(5);} // 同上,可是這個函數不會被使用,由於函數聲明的提高優先級最高,因此上面的函數表達式將永遠替換 // 這個同名函數,除非在函數表達式賦值前去調用getName(),可是在本題中,函數調用都在函數表達式 // 以後,因此這個函數能夠忽略了 // 經過上面對 getName的分析基本上答案已經出來了 foo.getName (); // 2 // 下面爲了方便,我就使用輸出值來簡稱每一個getName函數 // 這裏有小夥伴疑惑是在 2 和 3 之間,以爲應該是3 , 但其實直接設置 // foo.prototype上的屬性,對當前這個對象的屬性是沒有影響的,若是要使 // 用的話,能夠foo.prototype.getName() 這樣調用 ,這裏須要知道的是 // 3 並不會覆蓋 2,二者不衝突 ( 當你使用new 建立對象時,這裏的 // Prototype 將自動綁定到新對象上,即用new 構造調用的第二個做用) getName (); // 4 // 這裏涉及到函數提高的問題,不知道的小夥伴只須要知道 5 會被 4 覆蓋, // 雖然 5 在 4 的下面,其實 js 並非徹底的自上而下,想要深刻了解的 // 小夥伴能夠看文章最後的連接 foo().getName (); // 1 // 這裏的foo函數執行完成了兩件事, 1. 將window.getName設置爲1, // 2. 返回window , 故等價於 window.getName(); 輸出 1 getName (); // 1 // 剛剛上面的函數剛把window.getName設置爲1,故同上 輸出 1 new foo.getName (); // 2 // new 對一個函數進行構造調用 , 即 foo.getName ,構造調用也是調用啊 // 該執行仍是執行,而後返回一個新對象,輸出 2 (雖然這裏沒有接收新 // 建立的對象可是咱們能夠猜到,是一個函數名爲 foo.getName 的對象 // 且__proto__屬性裏有一個getName函數,是上面設置的 3 函數) new foo().getName (); // 3 // 這裏特別的地方就來了,new 是對一個函數進行構造調用,它直接找到了離它 // 最近的函數,foo(),並返回了應該新對象,等價於 var obj = new foo(); // obj.getName(); 這樣就很清晰了,輸出的是以前綁定到prototype上的 // 那個getName 3 ,由於使用new後會將函數的prototype繼承給 新對象 new new foo().getName (); // 3 // 哈哈,這個看上去很嚇人,讓咱們來分解一下: // var obj = new foo(); // var obj1 = new obj.getName(); // 好了,仔細看看, 這不就是上兩題的合體嗎,obj 有getName 3, 即輸出3 // obj 是一個函數名爲 foo的對象,obj1是一個函數名爲obj.getName的對象
箭頭函數,一種特殊的函數,不使用function
關鍵字,而是使用=>
,學名 胖箭頭
(2333),它和普通函數的區別:
先看個代碼鞏固一下:
function foo(){ return ()=>{ console.log(this.a); } } foo.a = 10; // 1. 箭頭函數關聯父級做用域this var bar = foo(); // foo默認綁定 bar(); // undefined 哈哈,是否是有小夥伴想固然了 var baz = foo.call(foo); // foo 顯性綁定 baz(); // 10 // 2. 箭頭函數this不可修改 //這裏咱們使用上面的已經綁定了foo 的 baz var obj = { a : 999 } baz.call(obj); // 10
來來來,實戰一下,還記得咱們以前第一個例子嗎,將它改爲箭頭函數的形式(能夠完全解決噁心的this綁定問題):
var people = { Name: "海洋餅乾", getName : function(){ console.log(this.Name); } }; var bar = people.getName; bar(); // undefined
====================修改後====================
var people = { Name: "海洋餅乾", getName : function(){ return ()=>{ console.log(this.Name); } } }; var bar = people.getName(); //得到一個永遠指向people的函數,不用想this了,豈不是美滋滋? bar(); // 海洋餅乾
可能會有人不解爲何在箭頭函數外面再套一層,直接寫不就好了嗎,搞這麼麻煩幹嗎,其實這也是箭頭函數不少人用很差的地方
var obj= { that : this, bar : function(){ return ()=>{ console.log(this); } }, baz : ()=>{ console.log(this); } } console.log(obj.that); // window obj.bar()(); // obj obj.baz(); // window
美滋滋,溜了溜了
參考書籍:你不知道的JavaScript<上卷> KYLE SIMPSON 著 (推薦)