首先,依然回顧《js基礎梳理-究竟什麼是執行上下文棧(執行棧),執行上下文(可執行代碼)?》中的html
3.執行上下文的生命週期
3.1 建立階段
- 生成變量對象(Variable object, VO)
- 創建做用域鏈(Scope chain)
- 肯定this指向
3.2 執行階段
- 變量賦值
- 函數引用
- 執行其餘代碼
咱們已經梳理了在執行上下文中的變量對象是如何生成的以及做用域鏈是如何創建的。本篇文章就繼續梳理下,執行上下文的this指向是如何肯定的。前端
首先,執行上下文分全局執行上下文和函數執行上下文,在瀏覽器環境的全局執行上下文中,this指向全局對象,也就是window(接下來本篇文章都僅只瀏覽器環境)。這也相對簡單,沒有那麼多複雜的狀況須要考慮。java
而在函數執行上下文中,肯定this指向發生在函數執行上下文的建立階段,而函數執行上下文又是在函數被調用後才產生的。所以,不難理解:this的指向,是在函數被調用的時候肯定的。而不是函數聲明的時候肯定的。而肯定this的指向難就難在函數被調用的方式是多種多樣的,因此咱們就須要從函數執行的各類方式分別去分析this的指向。數組
// 1.1 函數體在非嚴格模式下的全局函數執行 function fn () { console.log(this) } fn1() // => window
// 1.2 函數體在嚴格模式下的全局函數執行 'use strict' function fn () { console.log(this) } fn() // => undefined
// 1.3 函數體在非嚴格模式下的函數中的函數執行 function fn1 () { function fn2 () { console.log(this) } fn2() } fn1() // => window
// 1.4 函數體在嚴格模式下的函數中的函數執行 'use strict' function fn1 () { function fn2 () { console.log(this) } fn2() } fn1() // => undefined
// 1.5 函數體在非嚴格模式下,而函數調用在嚴格模式下時, this依然指向window function fn () { console.log(this) } (function () { 'use strict' fn() // => window })()
// 2.1.1函數直接在對象中聲明 var obj = { a: 1, test: function () { console.log(this.a) } } obj.test(); // => 1
// 2.1.2 函數先聲明,再由對象引用 function test () { console.log(this.a) } var obj = { a: 1, test: test } obj.test(); // => 1
// 2.2.1 多層對象引用,this指向離函數調用最近的對象 function test () { console.log(this.a) } var obj2 = { a: 2, test: test } var obj1 = { a: 1, obj2: obj2 } obj1.obj2.test() // => 2
// 2.3.1 將obj.foo看成函數別名賦值給一個變量 function foo () { console.log(this.a) } var obj = { a: 2, foo: foo } var bar = obj.foo // 函數別名 var a = '全局屬性' bar() // => 全局屬性
在2.3.1中,雖然bar是obj.foo的一個引用,可是實際上,它引用的是foo函數自己,所以此時的bar() 實際上是一個不帶任何修飾的普通函數調用。所以也使用默認綁定規則。瀏覽器
// 2.3.2 將obj.foo看成bar的回調函數。 function foo () { console.log(this.a) } function bar (fn) { fn() } var obj = { a: 2, foo: foo } var a = '全局屬性' bar(obj.foo) // => 全局屬性
你們都知道全部的函數的參數都是按值傳遞的,(都是棧內數據的拷貝)。
基本類型傳的是值自己(由於直接把值存在棧內),引用類型傳的是對象在內存裏面的地址(由於複雜對象在堆內,因此在棧裏存對象所在的堆地址)。
所以 bar(obj.foo) 執行時,參數fn實際上引用的是foo。而foo函數執行其實就是一個不帶任何修飾的普通函數調用。因此它也使用默認綁定規則。閉包
由此可擴展到 setInterval, setTimeout,以及匿名函數中的this也是使用的默認綁定規則。即非嚴格模式下,this指向window,嚴格模式下,this指向undefined。app
顯示綁定規則:this指向第一個參數。函數
// 3.1.1 var xw = { name : "小王", gender : "男", age : 24, say : function(school,grade) { console.log(this.name + " , " + this.gender + " ,今年" + this.age + " ,在" + school + "上" + grade); } } var xh = { name : "小紅", gender : "女", age : 12 } xw.say.call(xh, "實驗小學", "六年級") // => 小紅 , 女 ,今年12 ,在實驗小學上六年級
在3.1.1代碼示例中,當調用say時強制把它的this綁定到了xh上。post
// 3.2.1 var xw = { name : "小王", gender : "男", age : 24, say : function(school,grade) { console.log(this.name + " , " + this.gender + " ,今年" + this.age + " ,在" + school + "上" + grade); } } var xh = { name : "小紅", gender : "女", age : 12 } xw.say.apply(xh,["實驗小學","六年級"]) // => 小紅 , 女 ,今年12 ,在實驗小學上六年級
// 3.3.1 var xw = { name : "小王", gender : "男", age : 24, say : function(school,grade) { alert(this.name + " , " + this.gender + " ,今年" + this.age + " ,在" + school + "上" + grade); } } var xh = { name : "小紅", gender : "女", age : 12 } xw.say.bind(xh)("實驗小學","六年級") // => 小紅 , 女 ,今年12 ,在實驗小學上六年級
經過以上這些例子,其實也能夠明顯的看到call,apply,bind的區別。this
以前說到隱式丟失的問題,而顯示綁定的一個變種能夠解決隱式丟失的問題,這種方式被稱之爲硬綁定。
// 2.3.2 將obj.foo看成bar的回調函數。 function foo () { console.log(this.a) } function bar (fn) { fn() } var obj = { a: 2, foo: foo } var a = '全局屬性' bar(obj.foo) // => 全局屬性
將其修改爲
// 3.4.1 利用call方法解決隱式丟失的問題 function foo(){ console.log(this.a); } function bar(fn){ fn.call(obj); } var obj = { a:2, foo:foo } var a = "全局屬性"; bar(obj.foo); // => 2
這裏依舊是建立了 bar()這個函數,可是在其內部手動調用了obj.foo.call(obj),把foo強制綁定到了obj對象,以後不管如何調用bar(),它總會手動在obj上調用foo.
再看看隱式丟失的代碼示例2.3.1
// 2.3.1 將obj.foo看成函數別名賦值給一個變量 function foo () { console.log(this.a) } var obj = { a: 2, foo: foo } var bar = obj.foo // 函數別名 var a = '全局屬性' bar() // => 全局屬性
將其修改爲
function foo(){ console.log(this.a); } var obj = { a:2, foo:foo }; var bar = obj.foo.bind(obj); var a = "global"; bar(); // => 2
總結下「顯示綁定三人組」
共同點:
一、都用於控制this指向;
二、第一個參數都是this須要指向的對象,也就是上下文;
三、均可之後續參數傳遞;
四、沒有任何參數時或者第一個參數是null時,this都指向全局對象window >
區別:
一、call後面的參數與say方法中的參數是一一對應的,而apply的第二個參數是一個數組,數組中的元素是和say方法中一一對應的。因此當傳入的參數數目不肯定時,多使用apply。
二、call、apply綁定後馬上執行,bind是延遲執行。換言之,當你但願改變上下文環境以後並不是當即執行,而是回調執行的時候,就使用bind()方法吧
擴散性思考:
call,apply,bind還有什麼實際使用場景?它們的原理是什麼?如何本身模擬實現這三個方法?
使用new來調用函數,或者說發生構造函數調用時,會自動執行下面的操做:
在這裏主要關心第1,3,4步。
function Foo(a) { this.a = a; } var bar = new Foo(2); console.log(bar.a) // 2
使用new來調用Foo(...)時,會構造一個新對象並把它綁定到foo(...)調用中的this上,new是最後一種能夠影響到函數調用時this綁定行爲的方法,稱之爲new綁定。
// 5.1 箭頭函數外部是普通函數調用時 function fn () { return () => { return () => { console.log(this) } } } fn()()() // => window
在這個例子中,由於包裹箭頭函數的第一個普通函數是 fn,而fn的this指向window, 因此箭頭函數中的 this也是 window。
以上的例子,多數爲單個規則。
這裏推薦一種方法:
在寫這篇文章的時候,原本標題是關於this指向的全面分析,後來想一想,其實也並不全面。真正的宗師級別,會是多個規則夾在一塊兒去分析this,那樣的話,篇幅確定太長了,並且一時半會其實也記不了那麼多。並且就這篇文章所講的,其實也夠你們去理解分析大部分基礎的this指向問題了。有興趣的話,能夠本身去翻閱一下《你不知道javascritp·上卷》這本書,裏面還有一些我我的以爲比較少見而沒有列出來的this指向問題,好比個人文章中提到了硬綁定,那其實也會有軟綁定。
在這裏也貼一篇螞蟻金服前端的博文,是一個多個規則綜合應用判斷this指向的題。不少時候,當咱們覺得本身懂了,而老是會有人更深刻的去挖掘咱們以前沒有想到的知識點,學無止境。