JavaScript中的this

this 是什麼

在 JavaScript 中並無 OOP 編程的概念,咱們談到的 this 不一樣於一些 OOP 編程裏指向的實例化對象,它指的是運行時的上下文。所謂上下文,就是運行時所在的環境對象,好比你在公司,可能你的領導是你的部門經理,在家就是你媳婦兒同樣,不一樣的場合上下文是不同的。編程

this 的應用場景

在 JavaScript 中函數具備定義時上下文、運行時上下文以及上下文可改變的特色,也就是說函數中的 this 在不一樣的場合對應不一樣的值。在變量對象與做用域鏈一文中咱們談到 this 的肯定是在執行環境的建立階段完成的,也就是說 this 在運行時是基於執行環境綁定的,在全局執行函數,this 就指向全局(瀏覽器中爲 window),若是函數做爲一個對象的方法調用時,this 就指向這個對象。數組

全局調用瀏覽器

來看下面的的例子。bash

例 1:閉包

以下,函數 getName 在全局調用,this 指向全局對象app

var name = 'lily';
function getName() {
  var name = 'lucy';
  console.log('name:', this.name);
};
//非嚴格模式下等同於window.getName()
getName();
=> name: lily
複製代碼

例 2:函數

以下,儘管函數 getNameFunc 爲 boy 對象的方法,但因其在全局調用,this 一樣指向全局對象。post

var name = 'lily';
var boy = {
  name: 'lucy',
  getName: function() {
    console.log('name:', this.name);
  },
};
var getNameFunc = boy.getName;
getNameFunc();
=> name: lily
複製代碼

例 3:this

以下,boy.getName()返回一個匿名函數,假如這個匿名函數叫作 f,則(boy.getName())()等同於f(),等同於在全局中調用,所以 this 一樣指向全局對象。spa

var name = 'lily';
var boy = {
  name: 'lucy',
  getName: function() {
    var name = 'snow';
    return function() {
      console.log('name:', this.name);
    }
  },
};

(boy.getName())();

=> name: lily
複製代碼

爲了保持 this 能夠經過閉包實現,以下,執行 boy.getName()時,this 指向當前執行環境 boy,所以 that 指向 boy,屬性 name 爲 lily,匿名函數執行console.log('name:', that.name);時,因爲做用域鏈的關係,能夠訪問到上級做用域的 that 對象,指向 boy,所以 that.name 爲 lucy

var name = 'lily';
var boy = {
  name: 'lucy',
  getName: function() {
    var name = 'snow';
    var that = this;
    return function() {
      console.log('name:', that.name);
    }
  }
};

(boy.getName())();

=> name: lucy
複製代碼

對象調用

以下,對象 boy 調用本身的方法 getName,this 則指向 boy。

var boy = {
  name: 'lucy',
  getName: function() {
    console.log('name:', this.name);
  }
};

boy.getName();
=> name: lucy
複製代碼

構造函數調用

想要知道調用構造函數 this 如何指向,須要知道 new 操做符究竟作了什麼。 以下爲 new 的模擬實現:

function mockNew(f) {
  // 1.
  var newObj, returnObj, proto;

  // 2.
  proto = Object(f.prototype) === f.prototype ? f.prototype : Object.prototype;

  // 3.
  newObj = Object.create(proto);

  // 4.
  /*
    arguments爲類數組對象須要經過Array.prototype.slice.call將其轉換爲數組;
    Array.prototype.slice.call(arguments, 1)中的1是爲了去掉arguments的第一個參數(函數f),而沒有從0開始;
    經過f.apply調用,則將this指向了newObj。
  */
  returnObj = f.apply(newObj, Array.prototype.slice.call(arguments, 1));

  // 5.
  // 檢查returnObj是否爲Object類型
  if (Object(returnObj) === returnObj) {
    return returnObj;
  }
  return newObj;
}
複製代碼

經過 new 操做符調用構造函數(利用內置[[Construct]]方法),會經歷如下幾個階段:

    1. 初始化內部變量
    1. 給內置屬性[[prototype]](proto)賦值

      若是 f 的prototype爲原始的 Object 類型,則將構造函數 f 的 prototype 賦值給 proto,不然將 Object 的 prototype 賦值給 proto。

    1. 建立繼承自 proto 的對象

      經過 Object.create 建立對象 newObj,newObj 可經過原型鏈繼承 proto 的屬性。

    1. 綁定 this,將其值設置爲第三步生成的對象

      4.1. 經過 f.apply 調用 f,等同於 newObj.f(), 在構造函數 f 中執行 this.xxx = xxx;等同於執行 newObj.xxx = xxx,至關於 this 綁定了 newObj。

      4.2. 調用構造函數,可能返回一個對象 returnObj。

    1. 返回新生成的對象

      若是第 4 步中的 returnObj 值爲 Object 類型,則 new 操做最終返回這個對象 returnObj,不然返回第 4 步中綁定this的的 newObj。

來看下面的例子:

經過 mockNew 函數構造對象的過程當中,會調用上述第 4 步 f.apply(newObj, Array.prototype.slice.call(arguments, 1)),等同於調用 newObj.f(...arguments),則 this.name = 'lily'等同於 newObj.name = 'lily',mockNew 返回 newObj 時,p 就等於 newObj,所以 p 可以訪問到 person 的 name 屬性。

function person() {
  this.name = 'lily';
}

//這裏,能夠認爲mockNew(person)等同於new person()
var p = mockNew(person);
p.name // lily

複製代碼

來看另外一個例子:

var human = {
  name: 'lucy',

}

//返回了一個對象,則new操做符最終返回這個對象
function person() {
  this.name = 'lily';
  return human;
}

var p = mockNew(person);
p.name // lucy

複製代碼

由此,經過 new 操做符調用構造函數時,this 的最終指向爲 new 返回的對象,即新建立的對象 newObj 或構造函數中返回的對象 returnObj(上例中的 human)。

func.call 和 func.apply

func.call 和 func.apply 的做用同樣都是改變執行上下文,只是接收參數的形式不一樣。 func.apply 方法傳入兩個參數,第一個參數是想要指定的上下文,爲空則指向全局對象,第二個參數是函數參數組成的數組。 func.call 方法傳入兩個參數,第一個參數是想要指定的上下文,第二個參數是傳入的是一個參數列表,而不是單個數組。

/*
  thisArg: 想要指定的環境
  argsArray: 參數數組
*/
func.apply(thisArg, argsArray)

/*
  thisArg: 想要指定的環境
  arg一、arg2...: 參數列表
*/
func.call(thisArg, arg1, arg2, ...)
複製代碼

以下 boy 並無 getName 方法,可是經過 apply/call 改變 this 的指向達到了在 boy 中調用 girl 的 getName 方法。

function getName(firstName, lastName) {
    console.log(`${firstName}.${this.name}.${lastName}`)
  };
  const girl = {
    name: 'lucy',
    getName,
  };
  const boy = {
    name: 'Jeffrey'
  };
  //至關於boy.getName(['Michael', 'Jordan'])
  girl.getName.apply(boy, ['Michael', 'Jordan']);
  girl.getName.call(boy, 'Michael', 'Jordan');
  => Michael.Jeffrey.Jordan
複製代碼

bind 函數

bind 方法不會當即執行,而是返回一個改變了上下文 this 後的函數。

const newGetName = girl.getName.bind(boy);
newGetName('Michael', 'Jordan')
=> Michael.Jeffrey.Jordan
複製代碼

綜上,this 的指向由其具體的執行環境決定,同時也能夠經過函數的原型方法 apply、call 以及 bind 來顯式地改變 this 的指向。不過,箭頭函數的this,老是指向定義時所在的對象,而不是運行時所在的對象,apply、call也沒法更改。

相關文章
相關標籤/搜索