920
技術社區[雲棲]
深入理解Js的This綁定 ( 無需死記硬背,尾部有總結和麵試題解析 )
js 的 this 綁定問題,讓多數新手懵逼,部分老手覺得惡心,這是因為this的綁定 ‘難以捉摸’,出錯的時候還往往不知道為什麼,相當反邏輯。
讓我們考慮下麵代碼:
- var people = {
- name : "海洋餅幹",
- getName : function(){
- console.log(this.name);
- }
- };
- window.onload = function(){
- xxx.onclick = people.getName;
- };
在平時搬磚時比較常見的this綁定問題,大家可能也寫給或者遇到過,當xxx.onclick觸發時,輸出什麼呢 ?
為了方便測試,我將代碼簡化:
- var people = {
- Name: "海洋餅幹",
- getName : function(){
- console.log(this.Name);
- }
- };
- var bar = people.getName;
- bar(); // undefined
通過這個小例子帶大家感受一下this惡心的地方,我最開始遇到這個問題的時候也是一臉懵逼,因為代碼裏的this在創建時指向非常明顯啊,指向自己 people 對象,但是實際上指向 window 對象,這就是我馬上要和大家說的 this 綁定規則。
1 . this
什麼是this ?在討論this綁定前,我們得先搞清楚this代表什麼。
- this是JavaScript的關鍵字之一。它是 對象 自動生成的一個內部對象,隻能在 對象 內部使用。隨著函數使用場合的不同,this的值會發生變化。
- this指向什麼,完全取決於 什麼地方以什麼方式調用,而不是 創建時。(比較多人誤解的地方)(它非常語義化,this在英文中的含義就是 這,這個 ,但這其實起到了一定的誤導作用,因為this並不是一成不變的,並不一定一直指向當前 這個)
2 . this 綁定規則
掌握了下麵介紹的4種綁定的規則,那麼你隻要看到函數調用就可以判斷 this 的指向了。
2 .1 默認綁定
考慮下麵代碼:
- function foo(){
- var a = 1 ;
- console.log(this.a); // 10
- }
- var a = 10;
- foo();
這種就是典型的默認綁定,我們看看foo調用的位置,”光杆司令“,像 這種直接使用而不帶任何修飾的函數調用 ,就 默認且隻能 應用 默認綁定。
那默認綁定到哪呢,一般是window上,嚴格模式下 是undefined。
2 .2 隱性綁定
代碼說話:
- 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();, 上下文取函數的直接上級,即緊挨著的那個,或者說對象鏈的最後一個。
2 .3 顯性綁定
2 .3 .1 隱性綁定的限製
在我們剛剛的 隱性綁定中有一個致命的限製,就是上下文必須包含我們的函數 ,例:var obj = { foo : foo },如果上下文不包含我們的函數用隱性綁定明顯是要出錯的,不可能每個對象都要加這個函數 ,那樣的話擴展,維護性太差了,我們接下來聊的就是直接 給函數強製性綁定this。
2 .3 .2 call apply bind
這裏我們就要用到 js 給我們提供的函數 call 和 apply,它們的作用都是改變函數的this指向,第一個參數都是 設置this對象。
兩個函數的區別:
- call從第二個參數開始所有的參數都是 原函數的參數。
- apply隻接受兩個參數,且第二個參數必須是數組,這個數組代表原函數的參數列表。
例如:
- 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函數非常特別,下次和大家一起討論它的源碼)
2 .3 .2 顯性綁定
開始正題,上代碼,就用上麵隱性綁定的例子 :
- 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硬綁定,軟綁定,箭頭函數綁定 ] 等更多黑科技 的小夥伴歡迎關注我或本文的評論,最近我會單獨做一期放到一起寫一篇文章)(不想看的小夥伴不用擔心,不影響對本文的理解)
2 .4 new 綁定
2 .4 .1 什麼是 new
學過麵向對象的小夥伴對new肯定不陌生,js的new和傳統的麵向對象語言的new的作用都是創建一個新的對象,但是他們的機製完全不同。
創建一個新對象少不了一個概念,那就是構造函數,傳統的麵向對象 構造函數 是類裏的一種特殊函數,要創建對象時使用new 類名()的形式去調用類中的構造函數,而js中就不一樣了。
js中的隻要用new修飾的 函數就是'構造函數',準確來說是 函數的構造調用,因為在js中並不存在所謂的'構造函數'。
那麼用new 做到函數的構造調用後,js幫我們做了什麼工作呢:
- 創建一個新對象。
- 把這個新對象的__proto__屬性指向 原函數的prototype屬性。(即繼承原函數的原型)
- 將這個新對象綁定到 此函數的this上 。
- 返回新對象,如果這個函數沒有返回其他對象。
第三條就是我們下麵要聊的new綁定
2 .4 .2 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); // "搗蛋鬼"
2 .5 this綁定優先級
過程是些無聊的代碼測試,我直接寫出優先級了(想看測試過程可以私信,我幫你寫一份詳細的測試代碼)
new 綁定 > 顯示綁定 > 隱式綁定 > 默認綁定
3 . 總結
1.如果函數被new 修飾
this綁定的是新創建的對象,例:var bar = new foo(); 函數 foo 中的 this 就是一個叫foo的新創建的對象 , 然後將這個對象賦給bar , 這樣的綁定方式叫 new綁定 .
2.如果函數是使用call,apply,bind來調用的
this綁定的是 call,apply,bind 的第一個參數.例: foo.call(obj); , foo 中的 this 就是 obj , 這樣的綁定方式叫 顯性綁定 .
3.如果函數是在某個 上下文對象 下被調用
this綁定的是那個上下文對象,例 : var obj = { foo : foo }; obj.foo(); foo 中的 this 就是 obj . 這樣的綁定方式叫 隱性綁定 .
4.如果都不是,即使用默認綁定
例:function foo(){...} foo() ,foo 中的 this 就是 window.(嚴格模式下默認綁定到undefined).
這樣的綁定方式叫 默認綁定 .
4 . 麵試題解析
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上,這其實是題目的小陷阱,但是追棧的過程絕對精彩
讓我們一步步分析這裏發生了什麼:
- foo(1)執行,應該不難看出是默認綁定吧 , this指向了window,函數裏等價於 window.a = 1,return window;
- var a = foo(1) 等價於 window.a = window , 很多人都忽略了var a 就是window.a ,將剛剛賦值的 1 替換掉了。
- 所以這裏的 a 的值是 window , a.a 也是window , 即window.a = window ; window.a.a = window;
- foo(10) 和第一次一樣,都是默認綁定,這個時候,將window.a 賦值成 10 ,注意這裏是關鍵,原來window.a = window ,現在被賦值成了10,變成了值類型,所以現在 a.a = undefined。(驗證這一點隻需要將var b = foo(10);刪掉,這裏的 a.a 還是window)
- var b = foo(10); 等價於 window.b = window;
本題中所有變量的值,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的對象
5 . 箭頭函數的this綁定 (2017.9.18更新)
箭頭函數,一種特殊的函數,不使用function關鍵字,而是使用=>,學名 胖箭頭(2333),它和普通函數的區別:
- 箭頭函數不使用我們上麵介紹的四種綁定,而是完全根據外部作用域來決定this。(它的父級是使用我們的規則的哦)
- 箭頭函數的this綁定無法被修改 (這個特性非常爽(滑稽))
先看個代碼鞏固一下:
- 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(); // 海洋餅幹
可能會有人不解為什麼在箭頭函數外麵再套一層,直接寫不就行了嗎,搞這麼麻煩幹嘛,其實這也是箭頭函數很多人用不好的地方,來來來,餅幹帶你飛(可把我nb壞了,插會腰):
- 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
- 我們先要搞清楚一點,obj的當前作用域是window,如 obj.that === window。
- 如果不用function(function有自己的函數作用域)將其包裹起來,那麼默認綁定的父級作用域就是window。
- 用function包裹的目的就是將箭頭函數綁定到當前的對象上。函數的作用域是當前這個對象,然後箭頭函數會自動綁定函數所在作用域的this,即obj。
美滋滋,熘了熘了
本文作者:海洋餅幹
來源:51CTO
最後更新:2017-11-02 14:34:17