史上最全的 js中this詳解

前言


我如今依然堅信一些js開發者雖然能夠熟練的使用js內置函數和對象,也可以封裝出出色函數,可是 問題來了,大家在使用非箭頭函數的時候this的建立和this判斷是否是模糊不清,哈哈我也是,不過我最近總結了一些this的建立和this判斷的資料,也包含了本身一些觀點,做爲分享,文章比較長,請耐心看看,若是有什麼不足的地方或者理解錯誤的地方請留言一下,到時候我們一塊兒探討😁。

目錄


  • 爲何要用this
  • 對this的誤解
  • this究竟是什麼
  • 全面解析this


爲何要用this


來直接上代碼javascript

function foo(){
     console.log(`我自己屬性a是   ${this.a}`)
 }
 
 var bar ={
     a:2,
     foo:foo
 }
 var baz={
     a:4,
     foo:foo
 }
 bar.foo();//我自己屬性a是  2
 baz.foo()://我自己屬性a是  4
複製代碼

小夥伴們是否是已經在這個簡單的代碼中發現了 剛發foo只定義了一次,去能夠被不一樣的對象引用,實現了代碼共享java


對this的誤解


通常對this的誤解分爲兩個方面

  • 1 this是指向當前函數的自己
  • 2 this 指向的是當前函數的 做用域

接下來我們看看代碼中的是調試es6

this是指向當前函數的自己


下面代碼中你們要理解函數的多面性,多個身份
  • 普通的函數
  • 普通的對象
  • 構造函數

接下來說用到函數的是兩個身份普通函數、普通對象, 看代碼()數組

function foo(){
    this.count++
}
var count=0;
foo.count=0;
for(var i=0;i<5;i++){
    
    foo()
}
console.log(foo.count)//0
console.log(count)//5
複製代碼

從打印的結果上來看顯然,this指向的不是自己函數,固然我們通常看到這類的問題我們就會繞道而行,看代碼安全

function foo(){
    this.count++
}
var bar={
    count:0
}
foo.count=0;
for(var i=0;i<5;i++){
    
    foo.call(bar)
}
console.log(bar.count)//5
console.log(count)//0
複製代碼

雖然這種解決方案很好,也會有其餘的解決方案,可是咱們仍是不理解this的問題,內心仍是有種不安之感
bash

this 指向的是當前函數的 做用域


接下來說用到函數的是兩個身份普通函數、普通對象, 看代碼()

function foo(){
     var num=2;
     console.log(this.num)
 }
 var num=0;
 foo()//0
複製代碼

我們看到代碼的執行結果後,發現this指向的並非該函數的做用域。markdown


this究竟是什麼


this是在函數調用的時候綁定,不是在函數定義的時候綁定。它的上下文取決於函數調用時的各類條件,函數執行的時候會建立一個活動記錄,這個記錄裏面包含了該函數中定義的參數和參數,包含函數在哪裏被調用(調用棧)...,this就是其中的一個屬性。 來看圖 閉包

圖中我們看到this是在函數執行的時候建立的。app


全面解析this


前面幾步我們已經肯定的this的建立和this的指向的誤區,接下啦我們要看看this的綁定的規則,分爲4個規則。函數

  • 默認綁定
  • 隱式綁定(上下文綁定)
  • 顯式綁定
  • new 綁定


默認綁定


默認綁定的字面意思就是,不知足其餘的綁定方式,而執行的綁定規則。默認綁定會把this綁定到全局對象(是一個危險的操做,文章後面會說爲何) 看代碼

function foo(){
     var num=2;
     this.num++
     console.log(this.num)
 }
 var num=0;
 foo()//1
複製代碼

上面代碼中就實現了默認綁定,在foo方法的代碼塊中操做的是window.num++。


隱式綁定(上下文綁定)


定義:
函數被調用的位置有上下文,或者是該函數的引用地址是否是被某個對象的屬性引用,並經過對象的屬性直接運行該函數。若是出現上述的狀況,就會觸發this的隱式綁定,this就會被綁定成當前對象 看代碼

function foo(){
    console.log(this.name)
}
var bar={
    name:'shiny',
    foo:foo
}
bar.foo()//shiny
複製代碼

要須要補充一點,無論你的對象嵌套多深,this只會綁定爲直接引用該函數的地址屬性的對象,看代碼

function foo(){
    console.log(this.name)
}
var shiny={
    name:'shiny',
    foo:foo
}
var red={
    name:'red',
    obj:shiny
    
}
red.obj.foo()//shiny
複製代碼


隱式綁定的丟失


先看代碼

function foo(){
    console.log(this.name)
}
var shiny={
    name:'shiny',
    foo:foo
}
function doFoo(fn){
    fn()
}
doFoo(shiny.foo)//undefind
複製代碼

你們知道函數參數在函數執行的時候,其實有一個賦值的操做,我來解釋一下上面的,當函數doFoo執行的時候會開闢一個新的棧並被推入到全局棧中執行,在執行的過程當中會建立一個活動對象,這個活動對象會被賦值傳入的參數以及在函數中定義的變量函數,在函數執行時用到的變量和函數直接從該活動對象上面取值使用。 看圖 doFoo的執行棧

fn的執行棧

看下面原理和上面同樣經過賦值,致使隱式綁定的丟失,看代碼

function foo(){
    console.log(this.name)
}
var shiny={
    name:'shiny',
    foo:foo
}
var bar = shiny.foo
bar()//undefined
複製代碼

你們是否是已經明白了爲何是undefined,來解釋一波,其實shiny的foo屬性是引用了foo函數的引用內存地址,那麼有把foo的引用地址賦值給了 bar 那麼如今的bar的引用地址個shiny.foo的引用地址是一個,那麼執行bar的時候也會觸發默認綁定規則由於沒有其餘規則能夠匹配,bar函數執行時,函數內部的this綁定的是全局變量。

看下滿的引用地址賦值是出現的,奇葩 隱式綁定丟失,看代碼

function foo(){
    console.log(this.name)
}
var shiny={
    name:'shiny',
    foo:foo
}
var red={
    name:'red'
}
(red.foo=shiny.foo)()//undefined
複製代碼

賦值表達式 p.foo = o.foo 的返回值是目標函數的引用,所以調用位置是 foo() 而不是 p.foo() 或者 o.foo()。根據咱們以前說過的,這裏會應用默認綁定。


顯式綁定


call、apply綁定


javascript,在Function的porpertype上提供了3個方法來強行修改this,分別是 call、apply、bind,你們常常用的莫過於call和apply了,這兩個函數的第一個參數,都是須要執行函數綁定的this,對於apply只有連個參數,第二個參數是一個數組,這個數組是要傳入執行函數的參數,而call能夠跟不少參數,從第二個參數起都會被傳入到要執行函數的參數中

看代碼

function foo(){
   console.log(this.age)
}
var shiny={
   age:20
}
foo.call(shiny)//20

function bar(){
console.log(this.age)
}
var red={
age:18
}
bar.apply(red)//18
複製代碼

這兩個方法都是顯式的綁定了tihs

硬綁定:


相似與 bind方法行爲,是顯式綁定的一種方式

function foo(b){
  return this.a+b
}
var obj={
  a:2
}
function bind(fn,obj){
  return function(){
     return fn.apply(obj,arguments)
  }
}
bind(foo,obj)(3)//5
複製代碼

語言解釋: 經過apply + 閉包機制 實現bind方法,實現強行綁定規則

API調用的「上下文」 第三方庫或者寄生在環境,以及js內置的一些方法都提供了一下 content 上下文參數,他的做用和 bind同樣,就是確保回調函數的this被綁定

function foo (el){
  console.log(el,this.id)
}
var obj ={
 id:'some one'
};
[1,2,4].forEach(foo,obj)
// 1 some one 2 some one 4 some one
複製代碼


new 綁定


說道new 你們都會想到js的構造函數,我們想不用着急new 綁定this的問題,我們先看看我們對js的構造函數的誤解,傳統面向類的語言中的構函數和js的構造函數時不同

  • 傳統面向類的語言中的構函數,是在使用new操做符實例化類的時候,會調用類中的一些特殊方法(構造函數)

  • 不少人認爲js中的new操做符和傳統面向類語言的構造函數是同樣的,其實有很大的差異

  • 重新認識一下js中的構造函數,js中的構造函數 在被new操做符調用時,這個構造函數不屬於每一個類,也不會創造一個類,它就是一個函數,只是被new操做符調用。

  • 使用new操做符調用 構造函數時會執行4步

    • 建立一個全新的對象
    • 對全新的對象的__proto__屬性地址進行修改爲構造函數的原型(prototype)的引用地址
    • 構造函數的this被綁定爲這個全新的對象
    • 若是構造函數有返回值而且這個返回值是一個對象,則返回該對象,不然返回當前新對象

我們瞭解了js new 操做符調用構造函數時都作了些什麼,哪麼我們就知道構造函數裏面的this是誰了

代碼實現

function Foo(a){
  this.a=a
}
var F = new Foo(2)
console.log(F.a)//2
複製代碼


綁定規則的順序


我們在上面瞭解this綁定的4大規則,那麼我們就看看這4大綁定規則的優先級。

默認綁定

我們根據字面意思,都能理解只有其他的3個綁定規則沒法觸發的時候就會觸發默認綁定,沒有比較意義


顯式綁定 VS 隱式綁定


看代碼

function foo(){
    console.log(this.name)
}
var  shiny={
    name:'shiny',
    foo:foo
}
var red={
    name:'red'
}

shiny.foo()//shiny
shiny.foo.call(red)// red
shiny.foo.apply(red)// red
shiny.foo.bind(red)()//red
複製代碼

顯然在這場綁定this比賽中,顯式綁定贏了隱式綁定


隱式綁定 VS new 操做符綁定


看代碼

function  foo(name){
    this.name=name
}
var shiny={
    foo:foo
}
shiny.foo('shiny')
console.log(shiny.name)//shiny

var red = new shiny.foo('red')
console.log(red.name)//red
複製代碼

顯然在這場綁定this比賽中new 操做符綁定贏了隱式綁定


顯式綁定(硬綁定) VS new 操做符綁定


使用call、apply方法不能結合new操做符會報錯誤

可是我們能夠是bind綁定this來比較 顯式綁定和new操做符的綁定this優先級。 看代碼

function foo(){
    console.log(this.name)
}
var shiny={
    name:'shiny'
}

var bar = foo.bind(shiny)
var obj = new bar();
console.log(obj.name)// undefind
複製代碼

顯然 new操做符綁定 打敗了 顯式綁定


this的判斷


我們在上面已經瞭解 4個綁定this的優先級。我們能夠列舉出來

  • 1 判斷該函數是否是被new操做符調用,有的話 this就是 構造函數運行時建立的新對象 var f = new foo()
  • 2 判斷 函數是否是使用顯式綁定 call、apply、bind,若是有,那麼該函數的this就是 這個三個方法的第一個參數 foo.call(window)
  • 3 判斷該函數是否是被一個對象的屬性引用了地址,該函數有上下文(隱式綁定),在函數執行的時候是經過該對象屬性的引用觸發,這個函數的this就是當前對象的。 obj.foo();
  • 4 上面的三種都沒有的話,就是默認綁定,該函數的this就是全局對象或undefined(嚴格模式下)


綁定例外


😁 規則老是會有意外的,this綁定也是會有的,某些場面的綁定也是會出乎意料的,有可能觸發了默認綁定 看代碼

function foo(){
    console.log(name)
}
var name ='shiny'
foo.call(null)//shiny
foo.call(undefined)//shiny
var bar = foo.bind(null)
var baz = foo.bind(undefined)
bar()//siny
baz()//siny
複製代碼

把 null、undefined經過 apply、call、bind 顯式綁定,雖然實現可默認綁定,可是建議這麼作由於在非嚴格的模式下會給全局對象添加屬性,有時候會形成不可必要的bug。


更安全的this


我們從上面知道在非嚴格模式下 默認綁定是並操做this的話會該全局對象添加屬性,這樣的操做是有風險性的

function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 咱們的空對象
var ø = Object.create( null );
// 把數組展開成參數
foo.apply( ø, [2, 3] ); // a:2, b:3
// 使用 bind(..) 進行柯里化
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2, b:3

複製代碼


es6中的this


在es5及一下版本,咱們被this深深的困惑,可是看完了上面的文章,應該判斷this沒有關係,可是 重點來了 es6的this能夠經過箭頭函數直接綁定在該函數的執行的做用域上。 看代碼

function foo(){
     return ()=>{
          console.log(this.name)
     }
 }
 var obj ={
     name:'obj'
 }
  var shiny ={
     name:'shiny'
 }
 var bar = foo.call(obj);
 bar.call(shiny)// foo
 

複製代碼

咱們看到箭頭函數的this被綁定到該函數執行的做用域上。

我們在看看 js內部提供內置函數使用箭頭函數

function foo() {
    setTimeout(() => {
    // 這裏的 this 在此法上繼承自 foo()
    console.log( this.a );
    },100);
}
var obj = {
    a:2
};
foo.call( obj ); // 2
複製代碼

箭頭函數能夠像 bind(..) 同樣確保函數的 this 被綁定到指定對象,此外,其重要性還體 如今它用更常見的詞法做用域取代了傳統的 this 機制。實際上,在 ES6 以前咱們就已經 在使用一種幾乎和箭頭函數徹底同樣的模式。

function foo() {
var self = this; // lexical capture of this
setTimeout( function(){
    console.log( self.a );
    }, 100 );
}
var obj = {
    a: 2
};
foo.call( obj ); // 2
複製代碼

雖然 self = this 和箭頭函數看起來均可以取代 bind(..),可是從本質上來講,它們想替 代的是 this 機制。 若是你常常編寫 this 風格的代碼,可是絕大部分時候都會使用 self = this 或者箭頭函數。 若是徹底採用 this 風格,在必要時使用 bind(..),儘可能避免使用 self = this 和箭頭函數。

若有不足,在評論中提出,我們一塊兒學習

相關文章
相關標籤/搜索