javascript的this原理

九酒發表於2018-11-10

前言

不知道看了多少篇關於this的文章,很難理解,每次看到的文基本都是說明了this的幾個不同環境的指向。然後像記住歷史一樣,記在了腦子裡,但這種記憶隨著時間的變化會越來越分辨不清。

主要麼,也是我沒有真正深入的去了解this。真的去深入了,才發現this涉及的內容真的好多。

this原理

在《高程3》中的p182的第9行寫到:在全域性函式中,this等於window;當函式作為某個物件的方法呼叫時,this等於那個物件;匿名函式的執行環境具有全域性性,所以this等於window

這個概念,應該是一個前端必備的知識點吧?那麼問題來了,為什麼會有這樣一個判定呢?

先了解一個概念:

執行上下文:也叫一個執行環境,有 全域性執行環境函式執行環境eval

每個執行環境中包含這三部分:變數物件/活動物件,作用域鏈,this的值

下面這段程式碼大家肯定都不陌生

var obj = {
    name: 'a',
    say: function(){
        console.log(this.name)
    }
}

var say = obj.say;
obj.say();  //行1 a
say();  //行2 undefined
複製程式碼

以上那幾行程式碼,其實包含了兩個我曾經一度沒有深入理解的內容:

  1. this是在函式執行時基於的函式的執行上下文繫結的。這個時候需要了解一下 執行上下文的建立過程
  1. 物件的值都是存入記憶體的,而this跟記憶體裡面的資料結構有關係,瞭解一下阮大大的 javascript 的 this 原理

物件的記憶體結構

我這裡直接進入物件中的函式的宣告與呼叫,以上文中的obj為例。

在建立執行上下文中,變數的宣告的步驟依次為:建立、初始化、賦值。物件是一種特殊的變數,所以它也存在這3個步驟。

在初始化之後賦值之前,obj的值為undefined,在賦值操作時,javascript引擎會在記憶體裡建立一個下面的物件,然後將這個物件的記憶體地址賦值給 obj 這個變數。

{
    name: 'a',
    say: function(){
        console.log(this.name)
    }
}
複製程式碼

若要讀取 obj.name ,引擎會先拿到obj的記憶體地址,然後到這個記憶體地址中找到name這個屬性的value屬性。

當給物件賦值的是一個函式時,如上面的obj.say的地址,引擎會把函式單獨儲存在記憶體中,然後再將函式的地址賦值給say屬性的value屬性。

{
  name: {
    [[value]]: 'a'
    [[writable]]: true
    [[enumerable]]: true
    [[configurable]]: true
  },
  say: {
    [[value]]: 函式的地址
    [[writable]]: true
    [[enumerable]]: true
    [[configurable]]: true
  }
}
複製程式碼

大概的記憶體結構是這樣的

javascript的this原理

這張圖就可以很清晰的看出來了,因為函式是一個獨立的地址,那麼執行var say = obj.say;時,其實是把函式的地址賦值給了say,那麼say就可以獨立執行了,當執行say時,引擎可以直接拿到函式的地址而不再通過obj的記憶體了。

大概的結構是這樣的

javascript的this原理

環境變數

js允許在函式體內部,引用當前環境的其他變數。

比如下面的程式碼,x代表著當前環境的變數。

var f = function(){
    console.log(x);
}
複製程式碼

那麼問題來了,函式可以在不同的環境裡執行,那麼怎麼獲取到它的當前執行環境呢?所以,this就出現了,它的設計目的就是在函式體內部,指代函式當前的執行環境。

var x = 'a';

var f = function(){
    console.log(this.x);
}

var obj = {
    f:f,
    x: 'b'
}

f();  //a 在全域性環境下單獨執行,this指代全域性環境,全域性的x為a
obj.f(); //b  在obj中執行,this指代obj環境,obj的x 為b

複製程式碼

this的繫結

常見的有兩種,一種是直接在全域性執行,函式內部的this直接繫結到全域性,一個是通過物件呼叫,函式內部的this繫結到呼叫他的物件上。

除了常見的兩種,還有兩種:

1.是從一個環境傳到另一個環境中,需要使用callapply,俗稱改變this的指向。

2.不繫結。箭頭函式獨有,此時的this繼承於作用域鏈的上一層,這裡不免要聊一下作用域鏈上的this了。

call and apply

還是用上面的粒子

var x = 'a';

var f = function(){
    console.log(this.x);
}

var obj = {
    f:f,
    x: 'b'
}

f();  //a 在全域性環境下單獨執行,this指代全域性環境,全域性的x為a
f.call(obj, x); //b  
f.apply(obj, [x]); //b
複製程式碼

callapply都是將this繫結到它第一個引數上。若其第一個引數不是物件,則會使用js的型別強制將第一個引數轉為物件。

bind

bindcallapply不一樣的是,它會生成一個新的函式,且這個新生成的函式的this將永久地被繫結到了bind的第一個引數。具體原因還不清楚,若有知悉的望告知。

繼續上面的那個粒子

var g = f.bind(obj);
var obj2 = {
    x: 'c'
}
var h = g.bind(obj2);
g(); //b
h(); //b g的this指向不會被bind改變
複製程式碼

箭頭函式不繫結this

箭頭函式不繫結this,此時的this繼承於作用域鏈的上一層

var x = 1;
function a(){  //函式作用域上沒有x,向上找,最後找到window
    setTimeout(() => {
        console.log(this.x);
    })
}
function c(){  //函式作用域上沒有x,向上找,最後找到window
    setTimeout(() => {
        console.log(this.x);
    })
}
var obj = {
    x: 2, //物件的原型鏈上有x
    c: c
}
var obj1 = {//物件的原型鏈上沒有x
    c: c
}
a(); //1 執行環境是全域性
obj.c(); //2 執行環境是物件
obj1.c(); //undefined 執行環境是物件
複製程式碼

箭頭函式上的call和apply,bind

由於箭頭函式沒有自己的this指標,通過 call() 或 apply(),bind()方法呼叫一個函式時,只能傳遞引數,不能繫結this,他們的第一個引數會被忽略。

var x = 1;
var a = (b) =>{  //函式作用域上沒有x,向上找,最後找到window
    console.log(this.x + b);
}
var obj = {
    b: 2,
    x: 3
}
a.call(obj, obj.b); //3
複製程式碼

文末

因為寫到這道題,發現自己並不清楚為什麼結果是window,然後翻了下this的原理,算是做一份總結吧。 回顧一下上面的內容,因為js引擎在生成物件的記憶體時,obj這個變數還未被賦值,所以它的值為undefined,在es5中以明確,當bind第一個引數為undefined時,在瀏覽器上將執行window。

var obj = {
        say: function () {
          function _say() {
            console.log(this)
          }
          return _say.bind(obj)
        }()
      }
 obj.say() //window
複製程式碼

若有理解不合理的地方,還望指正。

相關文章