javascript中this到底指向了哪裏?

注意:此篇文章來源於coderwhy,版權爲微信訂閱號作者coderwhy所有。

1.this指向什麼

我們先說一個最簡單的,this在全局作用域下指向什麼?

  • 這個問題非常容易回答,在瀏覽器中測試就是指向window

  • 所以,在全局作用域下,我們可以認爲this就是指向的window

  • console.log(this); // window
    
    var name = "why";
    console.log(this.name); // why
    console.log(window.name); // why

    但是,開發中很少直接在全局作用域下去使用this,通常都是在函數中使用

    所有的函數在被調用時,都會創建一個執行上下文:

  • 這個上下文中記錄着函數的調用棧、函數的調用方式、傳入的參數信息等;

  • this也是其中的一個屬性;

  • 定義一個函數,我們採用三種不同的方式對它進行調用,它產生了三種不同的結果
// 定義一個函數
function foo() {
  console.log(this);
}

// 1.調用方式一: 直接調用
foo(); // window

// 2.調用方式二: 將foo放到一個對象中,再調用
var obj = {
  name: "why",
  foo: foo
}
obj.foo() // obj對象

// 3.調用方式三: 通過call/apply調用
foo.call("abc"); // String {"abc"}對象

上面的案例可以給我們什麼樣的啓示呢?

  • 1.函數在調用時,JavaScript會默認給this綁定一個值;

  • 2.this的綁定和定義的位置(編寫的位置)沒有關係;

  • 3.this的綁定和調用方式以及調用的位置有關係;

  • 4.this是在運行時被綁定的;

那麼this到底是怎麼樣的綁定規則呢?一起來學習一下吧

2.this綁定規則

我們現在已經知道this無非就是在函數調用時被綁定的一個對象,我們就需要知道它在不同的場景下的綁定規則即可。

2.1. 默認綁定

什麼情況下使用默認綁定呢?獨立函數調用。

  • 獨立的函數調用我們可以理解成函數沒有被綁定到某個對象上進行調用;

案例一:普通函數調用

  • 該函數直接被調用,並沒有進行任何的對象關聯;

  • 這種獨立的函數調用會使用默認綁定,通常默認綁定時,函數中的this指向全局對象(window);

function foo() {
  console.log(this); // window
}

foo();

案例二:函數調用鏈(一個函數又調用另外一個函數)

  • 所有的函數調用都沒有被綁定到某個對象上;

// 2.案例二:
function test1() {
  console.log(this); // window
  test2();
}

function test2() {
  console.log(this); // window
  test3()
}

function test3() {
  console.log(this); // window
}
test1();

案例三:將函數作爲參數,傳入到另一個函數中

function foo(func) {
  func()
}

function bar() {
  console.log(this); // window
}

foo(bar);

我們對案例進行一些修改,考慮一下打印結果是否會發生變化:

  • 這裏的結果依然是window,爲什麼呢?

  • 原因非常簡單,在真正函數調用的位置,並沒有進行任何的對象綁定,只是一個獨立函數的調用;

function foo(func) {
  func()
}

var obj = {
  name: "why",
  bar: function() {
    console.log(this); // window
  }
}

foo(obj.bar);

2.2. 隱式綁定

另外一種比較常見的調用方式是通過某個對象進行調用的:

  • 也就是它的調用位置中,是通過某個對象發起的函數調用。

案例一:通過對象調用函數

  • foo的調用位置是obj.foo()方式進行調用的

  • 那麼foo調用時this會隱式的被綁定到obj對象上

function foo() {
  console.log(this); // obj對象
}

var obj = {
  name: "why",
  foo: foo
}

obj.foo();

案例二:案例一的變化

  • 我們通過obj2又引用了obj1對象,再通過obj1對象調用foo函數;

  • 那麼foo調用的位置上其實還是obj1被綁定了this;

function foo() {
  console.log(this); // obj對象
}

var obj1 = {
  name: "obj1",
  foo: foo
}

var obj2 = {
  name: "obj2",
  obj1: obj1
}

obj2.obj1.foo();

案例三:隱式丟失

  • 結果最終是window,爲什麼是window呢?

  • 因爲foo最終被調用的位置是bar,而bar在進行調用時沒有綁定任何的對象,也就沒有形成隱式綁定;

  • 相當於是一種默認綁定;

function foo() {
  console.log(this);
}

var obj1 = {
  name: "obj1",
  foo: foo
}

// 講obj1的foo賦值給bar
var bar = obj1.foo;
bar();

2.3. 顯示綁定

隱式綁定有一個前提條件:

  • 必須在調用的對象內部有一個對函數的引用(比如一個屬性);

  • 如果沒有這樣的引用,在進行調用時,會報找不到該函數的錯誤;

  • 正是通過這個引用,間接的將this綁定到了這個對象上;

如果我們不希望在 對象內部 包含這個函數的引用,同時又希望在這個對象上進行強制調用,該怎麼做呢?

  • JavaScript所有的函數都可以使用call和apply方法(這個和Prototype有關)。

    • 它們兩個的區別這裏不再展開;

    • 其實非常簡單,第一個參數是相同的,後面的參數,apply爲數組,call爲參數列表;

  • 這兩個函數的第一個參數都要求是一個對象,這個對象的作用是什麼呢?就是給this準備的。

  • 在調用這個函數時,會將this綁定到這個傳入的對象上。

因爲上面的過程,我們明確的綁定了this指向的對象,所以稱之爲 顯示綁定

2.3.1. call、apply

通過call或者apply綁定this對象

  • 顯示綁定後,this就會明確的指向綁定的對象

function foo() {
  console.log(this);
}

foo.call(window); // window
foo.call({name: "why"}); // {name: "why"}
foo.call(123); // Number對象,存放時123

2.3.2. bind函數

如果我們希望一個函數總是顯示的綁定到一個對象上,可以怎麼做呢?

方案一:自己手寫一個輔助函數(瞭解)

  • 我們手動寫了一個bind的輔助函數

  • 這個輔助函數的目的是在執行foo時,總是讓它的this綁定到obj對象上

function foo() {
  console.log(this);
}

var obj = {
  name: "why"
}

function bind(func, obj) {
  return function() {
    return func.apply(obj, arguments);
  }
}

var bar = bind(foo, obj);

bar(); // obj對象
bar(); // obj對象
bar(); // obj對象

方案二:使用Function.prototype.bind

function foo() {
  console.log(this);
}

var obj = {
  name: "why"
}

var bar = foo.bind(obj);

bar(); // obj對象
bar(); // obj對象
bar(); // obj對象

2.3.3. 內置函數

有些時候,我們會調用一些JavaScript的內置函數,或者一些第三方庫中的內置函數。

  • 這些內置函數會要求我們傳入另外一個函數;

  • 我們自己並不會顯示的調用這些函數,而且JavaScript內部或者第三方庫內部會幫助我們執行;

  • 這些函數中的this又是如何綁定的呢?

案例一:setTimeout

  • setTimeout中會傳入一個函數,這個函數中的this通常是window

setTimeout(function() {
  console.log(this); // window
}, 1000);

爲什麼這裏是window呢?

  • 這個和setTimeout源碼的內部調用有關;

  • setTimeout內部是通過apply進行綁定的this對象,並且綁定的是全局對象;

案例二:數組的forEach

數組有一個高階函數forEach,用於函數的遍歷:

  • 在forEach中傳入的函數打印的也是Window對象;

  • 這是因爲默認情況下傳入的函數是自動調用函數(默認綁定);

var names = ["abc", "cba", "nba"];
names.forEach(function(item) {
  console.log(this); // 三次window
});

我們是否可以改變該函數的this指向呢?

forEach參數

var names = ["abc", "cba", "nba"];
var obj = {name: "why"};
names.forEach(function(item) {
  console.log(this); // 三次obj對象
}, obj);

案例三:div的點擊

如果我們有一個div元素:

  • 注意:省略了部分代碼

 <style>
    .box {
      width: 200px;
      height: 200px;
      background-color: red;
    }
  </style>

  <div class="box"></div>

獲取元素節點,並且監聽點擊:

  • 在點擊事件的回調中,this指向誰呢?box對象;

  • 這是因爲在發生點擊時,執行傳入的回調函數被調用時,會將box對象綁定到該函數中;

var box = document.querySelector(".box");
box.onclick = function() {
  console.log(this); // box對象
}

所以傳入到內置函數的回調函數this如何確定呢?

  • 某些內置的函數,我們很難確定它內部是如何調用傳入的回調函數;

  • 一方面可以通過分析源碼來確定,另一方面我們可以通過經驗(見多識廣)來確定;

  • 但是無論如何,通常都是我們之前講過的規則來確定的;

2.4. new綁定

JavaScript中的函數可以當做一個類的構造函數來使用,也就是使用new關鍵字。

使用new關鍵字來調用函數時,會執行如下的操作:

  • 1.創建一個全新的對象;

  • 2.這個新對象會被執行Prototype連接;

  • 3.這個新對象會綁定到函數調用的this上(this的綁定在這個步驟完成);

  • 4.如果函數沒有返回其他對象,表達式會返回這個新對象;

// 創建Person
function Person(name) {
  console.log(this); // Person {}
  this.name = name; // Person {name: "why"}
}

var p = new Person("why");
console.log(p);

2.5. 規則優先級

學習了四條規則,接下來開發中我們只需要去查找函數的調用應用了哪條規則即可,但是如果一個函數調用位置應用了多條規則,優先級誰更高呢?

1.默認規則的優先級最低

毫無疑問,默認規則的優先級是最低的,因爲存在其他規則時,就會通過其他規則的方式來綁定this

2.顯示綁定優先級高於隱式綁定

顯示綁定和隱式綁定哪一個優先級更高呢?這個我們可以測試一下:

  • 結果是obj2,說明是顯示綁定生效了

function foo() {
  console.log(this);
}

var obj1 = {
  name: "obj1",
  foo: foo
}

var obj2 = {
  name: "obj2",
  foo: foo
}

// 隱式綁定
obj1.foo(); // obj1
obj2.foo(); // obj2

// 隱式綁定和顯示綁定同時存在
obj1.foo.call(obj2); // obj2, 說明隱式綁定優先級更高

3.new綁定優先級高於隱式綁定

  • 結果是foo,說明是new綁定生效了

function foo() {
  console.log(this);
}

var obj = {
  name: "why",
  foo: foo
}

new obj.foo(); // foo對象, 說明new綁定優先級更高

4.new綁定優先級高於bind

new綁定和call、apply是不允許同時使用的,所以不存在誰的優先級更高

function foo() {
  console.log(this);
}

var obj = {
  name: "obj"
}

var foo = new foo.call(obj);

new和call同時使用

但是new綁定是否可以和bind後的函數同時使用呢?可以

  • 結果顯示爲foo,那麼說明是new綁定生效了

function foo() {
  console.log(this);
}

var obj = {
  name: "obj"
}

// var foo = new foo.call(obj);
var bar = foo.bind(obj);
var foo = new bar(); // 打印foo, 說明使用的是new綁定

優先級總結:

  • new綁定 > 顯示綁定(bind)> 隱式綁定 > 默認綁定

三.this規則之外

我們講到的規則已經足以應付平時的開發,但是總有一些語法,超出了我們的規則之外。(神話故事和動漫中總是有類似這樣的人物)

如果在顯示綁定中,我們傳入一個null或者undefined,那麼這個顯示綁定會被忽略,使用默認規則:

unction foo() {
  console.log(this);
}

var obj = {
  name: "why"
}

foo.call(obj); // obj對象
foo.call(null); // window
foo.call(undefined); // window

var bar = foo.bind(null);
bar(); // window

3.2. 間接函數引用

另外一種情況,創建一個函數的 間接引用,這種情況使用默認綁定規則。

我們先來看下面的案例結果是什麼?

  • (num2 = num1)的結果是num1的值;

var num1 = 100;
var num2 = 0;
var result = (num2 = num1);
console.log(result); // 100

我們來下面的函數賦值結果:

  • 賦值(obj2.foo = obj1.foo)的結果是foo函數;

  • foo函數被直接調用,那麼是默認綁定;

function foo() {
  console.log(this);
}

var obj1 = {
  name: "obj1",
  foo: foo
}; 

var obj2 = {
  name: "obj2"
}

obj1.foo(); // obj1對象
(obj2.foo = obj1.foo)();  // window

3.3. ES6箭頭函數

在ES6中新增一個非常好用的函數類型:箭頭函數

  • 這裏不再具體介紹箭頭函數的用法,可以自行學習。

箭頭函數不使用this的四種標準規則(也就是不綁定this),而是根據外層作用域來決定this。

我們來看一個模擬網絡請求的案例:

  • 這裏我使用setTimeout來模擬網絡請求,請求到數據後如何可以存放到data中呢?

  • 我們需要拿到obj對象,設置data;

  • 但是直接拿到的this是window,我們需要在外層定義:var _this = this

  • 在setTimeout的回調函數中使用_this就代表了obj對象

var obj = {
  data: [],
  getData: function() {
    var _this = this;
    setTimeout(function() {
      // 模擬獲取到的數據
      var res = ["abc", "cba", "nba"];
      _this.data.push(...res);
    }, 1000);
  }
}

obj.getData();

上面的代碼在ES6之前是我們最常用的方式,從ES6開始,我們會使用箭頭函數:

  • 爲什麼在setTimeout的回調函數中可以直接使用this呢?

  • 因爲箭頭函數並不綁定this對象,那麼this引用就會從上層作用域中找到對應的this

var obj = {
  data: [],
  getData: function() {
    setTimeout(() => {
      // 模擬獲取到的數據
      var res = ["abc", "cba", "nba"];
      this.data.push(...res);
    }, 1000);
  }
}

obj.getData();

思考:如果getData也是一個箭頭函數,那麼setTimeout中的回調函數中的this指向誰呢?

  • 答案是window;

  • 依然是不斷的從上層作用域找,那麼找到了全局作用域;

  • 在全局作用域內,this代表的就是window

var obj = {
  data: [],
  getData: () => {
    setTimeout(() => {
      console.log(this); // window
    }, 1000);
  }
}

obj.getData();

四. this面試題

4.1. 面試題一:

var name = "window";
var person = {
  name: "person",
  sayName: function () {
    console.log(this.name);
  }
};
function sayName() {
  var sss = person.sayName;
  sss(); 
  person.sayName(); 
  (person.sayName)(); 
  (b = person.sayName)(); 
}
sayName();

 這道面試題非常簡單,無非就是繞一下,希望把面試者繞暈:

function sayName() {
  var sss = person.sayName;
  // 獨立函數調用,沒有和任何對象關聯
  sss(); // window
  // 關聯
  person.sayName(); // person
  (person.sayName)(); // person
  (b = person.sayName)(); // window
}

4.2. 面試題二:

var name = 'window'
var person1 = {
  name: 'person1',
  foo1: function () {
    console.log(this.name)
  },
  foo2: () => console.log(this.name),
  foo3: function () {
    return function () {
      console.log(this.name)
    }
  },
  foo4: function () {
    return () => {
      console.log(this.name)
    }
  }
}

var person2 = { name: 'person2' }

person1.foo1(); 
person1.foo1.call(person2); 

person1.foo2();
person1.foo2.call(person2);

person1.foo3()();
person1.foo3.call(person2)();
person1.foo3().call(person2);

person1.foo4()();
person1.foo4.call(person2)();
person1.foo4().call(person2);

下面是代碼解析:

// 隱式綁定,肯定是person1
person1.foo1(); // person1
// 隱式綁定和顯示綁定的結合,顯示綁定生效,所以是person2
person1.foo1.call(person2); // person2

// foo2()是一個箭頭函數,不適用所有的規則
person1.foo2() // window
// foo2依然是箭頭函數,不適用於顯示綁定的規則
person1.foo2.call(person2) // window

// 獲取到foo3,但是調用位置是全局作用於下,所以是默認綁定window
person1.foo3()() // window
// foo3顯示綁定到person2中
// 但是拿到的返回函數依然是在全局下調用,所以依然是window
person1.foo3.call(person2)() // window
// 拿到foo3返回的函數,通過顯示綁定到person2中,所以是person2
person1.foo3().call(person2) // person2

// foo4()的函數返回的是一個箭頭函數
// 箭頭函數的執行找上層作用域,是person1
person1.foo4()() // person1
// foo4()顯示綁定到person2中,並且返回一個箭頭函數
// 箭頭函數找上層作用域,是person2
person1.foo4.call(person2)() // person2
// foo4返回的是箭頭函數,箭頭函數只看上層作用域
person1.foo4().call(person2) // person1

4.3. 面試題三:

var name = 'window'
function Person (name) {
  this.name = name
  this.foo1 = function () {
    console.log(this.name)
  },
  this.foo2 = () => console.log(this.name),
  this.foo3 = function () {
    return function () {
      console.log(this.name)
    }
  },
  this.foo4 = function () {
    return () => {
      console.log(this.name)
    }
  }
}
var person1 = new Person('person1')
var person2 = new Person('person2')

person1.foo1()
person1.foo1.call(person2)

person1.foo2()
person1.foo2.call(person2)

person1.foo3()()
person1.foo3.call(person2)()
person1.foo3().call(person2)

person1.foo4()()
person1.foo4.call(person2)()
person1.foo4().call(person2)

下面是代碼解析:

// 隱式綁定
person1.foo1() // peron1
// 顯示綁定優先級大於隱式綁定
person1.foo1.call(person2) // person2

// foo是一個箭頭函數,會找上層作用域中的this,那麼就是person1
person1.foo2() // person1
// foo是一個箭頭函數,使用call調用不會影響this的綁定,和上面一樣向上層查找
person1.foo2.call(person2) // person1

// 調用位置是全局直接調用,所以依然是window(默認綁定)
person1.foo3()() // window
// 最終還是拿到了foo3返回的函數,在全局直接調用(默認綁定)
person1.foo3.call(person2)() // window
// 拿到foo3返回的函數後,通過call綁定到person2中進行了調用
person1.foo3().call(person2) // person2

// foo4返回了箭頭函數,和自身綁定沒有關係,上層找到person1
person1.foo4()() // person1
// foo4調用時綁定了person2,返回的函數是箭頭函數,調用時,找到了上層綁定的person2
person1.foo4.call(person2)() // person2
// foo4調用返回的箭頭函數,和call調用沒有關係,找到上層的person1
person1.foo4().call(person2) // person1

4.4. 面試題四:

var name = 'window'
function Person (name) {
  this.name = name
  this.obj = {
    name: 'obj',
    foo1: function () {
      return function () {
        console.log(this.name)
      }
    },
    foo2: function () {
      return () => {
        console.log(this.name)
      }
    }
  }
}
var person1 = new Person('person1')
var person2 = new Person('person2')

person1.obj.foo1()()
person1.obj.foo1.call(person2)()
person1.obj.foo1().call(person2)

person1.obj.foo2()()
person1.obj.foo2.call(person2)()
person1.obj.foo2().call(person2)

下面是代碼解析:

// obj.foo1()返回一個函數
// 這個函數在全局作用於下直接執行(默認綁定)
person1.obj.foo1()() // window
// 最終還是拿到一個返回的函數(雖然多了一步call的綁定)
// 這個函數在全局作用於下直接執行(默認綁定)
person1.obj.foo1.call(person2)() // window
person1.obj.foo1().call(person2) // person2

// 拿到foo2()的返回值,是一個箭頭函數
// 箭頭函數在執行時找上層作用域下的this,就是obj
person1.obj.foo2()() // obj
// foo2()的返回值,依然是箭頭函數,但是在執行foo2時綁定了person2
// 箭頭函數在執行時找上層作用域下的this,找到的是person2
person1.obj.foo2.call(person2)() // person2
// foo2()的返回值,依然是箭頭函數
// 箭頭函數通過call調用是不會綁定this,所以找上層作用域下的this是obj
person1.obj.foo2().call(person2) // obj

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章