深刻JavaScript中的this對象

this 對象詳解

this關鍵字是函數當中最重要的一個知識點。它在JavaScript中的表現也會有一些細微的不一樣,在嚴格和非嚴格模式之下也會有一些差異。html

絕大多數狀況下,this的指向由函數的調用方式決定。它不能被賦值,而且每次函數調用,它也有可能會不一樣。ES5引入了bind方法來設置函數的this值,而不須要考慮函數的調用方式,ES6的箭頭函數不提供自身的this綁定,它的this由當前上下文決定。前端

const obj = {
        name:"hello,world!",
        getName(){
            return this.name;
        }
    }
    console.log(obj.getName());//"hello,world!"
複製代碼

語法:

this
複製代碼

它的值是當前上下文(global,function,eval)中的一個屬性,在非嚴格模式下,它老是指向一個對象,而在嚴格模式下,它能夠被設置成任意值。node

描述

全局上下文

全局上下文即全局對象,例如在瀏覽器環境當中,this始終指的是window對象,不管是否是嚴格模式。來看以下一個示例:web

//在瀏覽器環境中,window對象就是全局對象
    console.log(this === window);//true

    //不用標識符定義一個變量,也會自動將該變量添加到window對象中,做爲window對象的一個屬性
    a = 250;
    console.log(this.a);//250

    this.message = "hello,world!";
    console.log(message);
    console.log(window.message);
    //都是打印的"hello,world!"
複製代碼

筆記:能夠始終使用globalThis來獲取一個全局對象,不管你的代碼是否在當前上下文運行。typescript

var obj = {
        func:function(){
            console.log(this);
            console.log(globalThis);
        }
    }
    obj.func();//先打印obj對象,再打印window對象,瀏覽器環境中
複製代碼

函數上下文

在函數內部,this取決於它被調用的方式。例如如下的非嚴格模式下,沒有手動去經過設置調用方式,而且是在全局環境下調用的,因此this指向全局對象。segmentfault

function fn(){
      return this;
  }
  //在瀏覽器環境中
  console.log(fn() === window);//true
  //在node.js環境中
  console.log(fn() === globalThis);//true
複製代碼

然而,在嚴格模式下,若是沒有爲this設置值,那麼this會保持爲undefined。如:數組

function fn(){
 'use strict';
        return this;
    }
    console.log(fn() === undefined) //true
複製代碼

tips:上例中,由於fn是直接被調用的,也就是並非做爲對象的屬性來調用(window.fn),因此this應是undefined,有些瀏覽器在最初支持嚴格模式的時候並無正確的實現這個功能,因此錯誤的返回了window對象。瀏覽器

若是想要改變this值,須要使用callapply方法。如例:markdown

var obj = { value:"this is custom object!"};
    var value = "this is global object!";
    var getThis = function(){
        return this.value;
    }
    console.log(getThis());//"this is global object!"
    console.log(getThis.apply(obj))//"this is custom object!"
    console.log(getThis.call(obj))//"this is custom object!"
複製代碼

類上下文

儘管ES6的類和函數有些類似,this的表現也會相似,但也有一些區別和注意事項。app

在類當中,this就是一個常規的類對象,類裏面定義的非靜態的方法都會被添加到this對象的原型當中。例:

class Test {
        constructor(){
            const p = Object.getPrototypeOf(this);
            console.log(Object.getOwnPropertyNames(p));
        }
        getName(){}
        getValue(){}
        static getNameAndValue(){}
    }
    new Test();//["constructor","getName","getValue"]
複製代碼

tips:靜態方法不是this的屬性,它們只是類自身的屬性。

好比,咱們要調用以上的getNameAndValue方法,咱們能夠像以下這樣調用:

Test.getNameAndValue();
    //或者
    const test = new Test();
    test.constructor.getNameAndValue();
複製代碼

派生類

在派生類當中,不會像基類那樣,有初始的綁定。什麼是派生類?也就是繼承基類的類。例如:

class Base {
    constructor(){
        this.key = "base";
    }
}
class Test extends Base {}
//這裏的test就是一個派生類
複製代碼

在派生類的構造函數當中,若是不使用super綁定this,則在使用this的過程當中會報錯Must call super constructor in derived class before accessing 'this' or returning from derived constructor。大體意思就是要有一個super綁定。如:

class Base {
    constructor(){
        this.key = "base";
    }
}
class Test extends Base {
    constructor(){
        console.log(this);
    }
}
//ReferenceError
複製代碼

可是若是咱們稍微改一下,以下:

class Base {
    constructor(){
        this.key = "base";
    }
}
class Test extends Base {
    constructor(){
        super();//這時候會生成一個this綁定
        console.log(this);
    }
}
//Test,繼承了基類的屬性和方法,至關於執行this = new Base()
複製代碼

派生類不能在沒有super方法的構造函數中返回一個除對象之外的值,或者說是有super方法的前面直接返回一個對象之外的值也是不行的,除非根本就沒有構造函數。如:

class Base {
    constructor(){
        this.key = "base";
    }
}
class Test extends Base {
    constructor(){
        return 1;
        super();
    }
}
//TypeError
複製代碼

可是下面的示例不會出錯:

class Base {
    constructor(){
        this.key = "base";
    }
}
class Test extends Base {
    constructor(){
        return {};
        super();
    }
}
複製代碼

下面示例會報錯:

class Base {
    constructor(){
        this.key = "base";
    }
}
class Test extends Base {
    constructor(){
        return 1;
    }
}
//TypeError
複製代碼

下面示例不會報錯:

class Base {
    constructor(){
        this.key = "base";
    }
}
class Test extends Base {
    constructor(){
        return {};
    }
}
複製代碼

this和對象之間的轉換

在非嚴格模式下,若是調用call或apply方法,傳入的第一個參數,也就是被用做this的值不是一個對象,則會嘗試被轉換爲對象。基本類型值,如null何undefined會被轉換成全局對象,而像其餘的基本類型值則會使用對應的構造函數來轉換成對象。例如number類型數字1就會調用new Number(1),string類型'test'就會調用new String('test')。

例如:

function sum(c,d){
    return this.a + this.b + c + d;
}
var a = 3,b = 4;
var count = {
    a:1,
    b:2
}
//call方法後面的參數直接被用做函數的參數
console.log(sum.call(count,3,4));//10
console.log(sum.call(count,'3',4))//'334'
console.log(sum.call(null,3,4));//14
console.log(sum.call(undefined,'3',4));//'734'
console.log(sum.call(1,3,4));//new Number(1)上沒有a和b屬性,因此是this.a + this.b就是NaN,即兩個undefined相加
console.log(sum.call('',1,'2'))//'NaN2'
//apply方法參數只能傳數組參數
//TypeError
// console.log(sum.apply(count,3,4));
// console.log(sum.apply(count,'3',4))
// console.log(sum.apply(null,3,4));
// console.log(sum.apply(undefined,'3',4));
// console.log(sum.apply(1,3,4));
// console.log(sum.apply('',1,'2'))
//必須這樣傳
console.log(sum.apply(count,[3,4]));//10
console.log(sum.apply(count,['3',4]))//'334'
console.log(sum.apply(null,[3,4]));//14
console.log(sum.apply(undefined,['3',4]));//'734'
console.log(sum.apply(1,[3,4]));//new Number(1)上沒有a和b屬性,因此是this.a + this.b就是NaN,即兩個undefined相加
console.log(sum.apply('',[1,'2']))//'NaN2'
複製代碼

再來看一個示例以下:

function test(){
    console.log(Object.prototype.toString.call(this))
}
console.log(test.call(7));//[object Number]
console.log(test.call(undefined));//[object global],在瀏覽器環境下指向爲[Object window]
console.log(test.apply('123'));//[object String]
複製代碼

根據以上示例,咱們就能夠知道了利用Object.prototype.toString方法來判斷一個對象的類型。如能夠封裝一個函數以下:

function isObject(value){
    return Object.prototype.toString.call(value) === '[object Object]';
}
//等價於
function isObject(value){
    return Object.prototype.toString.apply(value) === '[object Object]';
}
//等價於
function isObject(value){
    return {}.toString.call(value) === '[object Object]';
}
//等價於
function isObject(value){
    return {}.toString.apply(value) === '[object Object]';
}
複製代碼

bind方法

ES5引入了bind方法,該方法爲Function的原型對象上的一個屬性,在一個函數fn中調用fn.bind(object)將會建立一個和該函數相同做用域以及相同函數體的函數,可是它的this值將被綁定到bind方法的第一個參數,不管這個新建立的函數以什麼方式調用。如:

function fn(){
    var value = "test";
    return this.value;
}
var obj = {
    value:"objName"
}
var newFn = fn.bind(obj);
console.log(fn.bind(obj)());//objName
console.log(newFn());//objName
var bindObj = {
    value:"bind",
    f:fn,
    g:newFn,
    h:fn.bind(bindObj)
}
var newBind = {
    a:fn.bind(bindObj)
}
console.log(bindObj.f());//bind
console.log(bindObj.g());//objName
console.log(bindObj.h());//undefined
console.log(newBind.a());//bind
複製代碼

箭頭函數

在箭頭函數中,this與封閉環境當中的上下文的this綁定一致,在全局環境中,那它的this就是全局對象。如:

var obj = {
    a:() => {
        return this;
    },
    b:function(){
        var x = () => { return this;};
        return x();
    }
}
console.log(obj.a());//global
console.log(obj.b());//obj
複製代碼

注意:不管使用call,apply仍是bind其中的哪種方法,都不能改變箭頭函數的this指向,由於都將被忽略,可是仍然能夠傳遞參數,理論上第一個參數設置爲null或者undefined爲最佳實踐。

如:

//在瀏覽器環境下globalObject是window對象
    let globalObject = this;
    let getThis = () => this;
    console.log(getThis() === globalObject);//true
    let obj = {
        getThis:getThis
    }
    console.log(obj.getThis() === globalObject);//true
    console.log(obj.getThis.call(obj) === globalObject);//true
    console.log(obj.getThis.apply(obj) === globalObject);//true
    // 使用bind並未改變this指向
    console.log(obj.getThis.bind(obj)() === globalObject);//true
複製代碼

也就是說,不管如何,箭頭函數的this都指向它的封閉環境中的this。以下:

var obj = {
    a:() => {
        return this;
    },
    b:function(){
        var x = () => { return this;};
        return x();
    }
}
console.log(obj.a());//global在瀏覽器環境下是window對象
console.log(obj.b());//obj
複製代碼

做爲某個對象

當調用某個對象中的函數中的方法時,在訪問該函數中的this對象,將會指向這個對象。例如:

var value = "this is a global value!";
    var obj = {
        value:"this is a custom object value!",
        getValue:function(){
            return this.value;
        }
    }
    console.log(obj.getValue());//"this is a custom object value!"
複製代碼

這樣的行爲方式徹底不會受函數定義的方式和位置影響,例如:

var value = "this is a global value!";
    var obj = {
        value:"this is a custom object value!",
        getValue:getValue
    }
    function getValue(){
        return this.value;
    }
    console.log(obj.getValue());//"this is a custom object value!"
複製代碼

此外,它只受最接近的引用對象的影響。如:

var value = "this is a global value!";
    var obj = {
        value:"this is a custom object value!",
        getValue:getValue
    }
    obj.b = {
        value:"this is b object value!",
        getValue:getValue
    }
    function getValue(){
        return this.value;
    }
    console.log(obj.b.getValue());//"this is b object value!"
複製代碼

對象原型鏈中的this

在對象的原型鏈中,this一樣也指向的是調用這個方法的對象,實際上也就至關於該方法在這個對象上同樣。如:

var obj = {
       sum:function(){
           return this.a + this.b;
       }
   }
   var newObj = Object.create(obj);
   newObj.a = 1;
   newObj.b = 2;
   console.log(newObj.sum());//3
   console.log(obj.sum());//NaN
複製代碼

上例中,newObj對象繼承了obj的sum方法,而且咱們未newObj添加了a和b屬性,若是咱們調用newObj的sum方法,this實際上指向的就是newObj這個對象,因此咱們能夠獲得結果爲3,可是咱們調用obj.sum方法的時候,this指向的是obj,obj對象並無a和b屬性,因此也就是兩個undefined相加,就會是NaN。obj就做爲了newObj的原型對象,這也是原型鏈當中的一個很是重要的特色。

注意:Object.create()方法表示建立一個新對象,會以第一個參數做爲新對象的原型對象,第一個參數只能爲null或者新對象,不能爲其它基本類型的值,如undefined,1,''等。

getter或setter中的this

在一個對象的setter或者getter中一樣的this指向設置或者獲取這個屬性的對象。如:

function average(){
       return (this.a + this.b + this.c) / 3;
   }
   var obj = {
       a:1,
       b:2,
       c:3
       get sum:function(){
           return this.a + this.b + this.c;
       }
   }
   Object.defineProperty(obj,'average',{
       get:average,
       enumerable:true,
       configurable:true
   });
   console.log(obj.average,obj.sum);//2,6
複製代碼

構造函數中的this對象

當一個函數被當作構造函數調用時(使用new關鍵字),this指向的就是實例化的那個對象。

注意:儘管構造函數返回的默認值就是this指向的那個對象,可是也能夠手動設置成返回其它的對象,若是手動設置的值不是一個對象,則返回this對象。

如:

function C(){
        this.a = 1;
    }
    var c1 = new C();
    console.log(c1.a);//1
    function C2(){
        var obj = {
            a:2
        }
        this.a = 3;
        return obj;
    }
    var c2 = new C2();
    console.log(c2.a);//2
複製代碼

在上例中實例化的c2的構造函數C2中,因爲手動的設置了返回的對象obj,因此致使this.a = 3這條語句被忽略,從而獲得結果爲2,就好像"殭屍"代碼。固然也不能算是"殭屍"代碼,由於實際上它是被執行了的,只不過對外部沒有形成影響,因此能夠被忽略。

做爲一個DOM事件處理函數

當函數是一個DOM事件處理函數,它的this就指向觸發事件的元素(有一些瀏覽器在使用非addEventListener動態添加函數時不遵照這個約定)。如:

function changeStyle(e){
    console.log(this === e.currentTarget);//true
    console.log(this === e.target);//true
    //將背景色更改成紅色
    this.style.setProperty('background',"#f00");
}

// 獲取文檔中全部的DOM元素
var elements = document.getElementsByTagName('*');

for(let i = 0,len = elements.length;i < len;i++){
    //爲每一個獲取到的元素添加事件
    elements[i].addEventListener('click',changeStyle,false);
}
複製代碼

內聯事件中的this

當在內聯事件中調用函數時,this指向的就是這個元素。但只有最外層的代碼才指向這個元素,若是是內部嵌套函數中沒有指定this,則指向全局對象。如:

<button type="button" onclick="document.writeln(this.tagName.toLowerCase())">clicked me</button> 
    <!-- 點擊按鈕會在頁面中出現button -->
複製代碼
<button type="button" onclick="document.writeln((function(){return this})())">clicked me</button>
<!-- 在瀏覽器環境下頁面會寫入[object Window] -->
複製代碼

在類中更改this綁定

類中的this取決於如何調用,但實際上在開發當中,咱們手動的去綁定this爲該類實例是一個頗有用的方式,咱們能夠在構造函數中去更改this綁定。如:

class Animal {
    constructor(){
        //利用bind方法讓this指向實例化的類對象
        this.getAnimalName = this.getAnimalName.bind(this);
    }
    getAnimalName(){
        console.log("The animal name is ",this.animalName);
    }
    getAnimalNameAgain(){
        console.log("The animal name is ",this.animalName);
    }
    get animalName(){
        return "dog";
    }
}
class Bird {
    get animalName(){
        return "bird";
    }
}

let animal = new Animal();
console.log(animal.getAnimalName());//The animal name is dog;
let bird = new Bird();
bird.getAnimalName = animal.getAnimalName;
console.log(bird.getAnimalName());//The animal name is dog;

bird.getAnimalNameAgain = animal.getAnimalNameAgain;
console.log(bird.getAnimalNameAgain());//The animal name is bird;
複製代碼

在這個示例中,咱們始終將getAnimalName方法的this綁定到實例化的Animal類對象上,因此儘管在Bird類中定義了一個animalName屬性,咱們在調用getAnimalName方法的時候,始終獲得的就是Animal中的animalName屬性。因此第二個打印仍然是dog

注意:在類的內部老是使用的嚴格模式,因此調用一個this值爲undefined的方法會拋出錯誤。

打個廣告,我在思否上線的課程玩轉typescript1玩轉typescript2適用於有必定基礎的前端,還望你們多多支持,謝謝。

相關文章
相關標籤/搜索