溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

JavaScript中的this實(shí)例分析

發(fā)布時間:2022-06-08 09:27:24 來源:億速云 閱讀:93 作者:zzz 欄目:開發(fā)技術(shù)

本篇內(nèi)容主要講解“JavaScript中的this實(shí)例分析”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實(shí)用性強(qiáng)。下面就讓小編來帶大家學(xué)習(xí)“JavaScript中的this實(shí)例分析”吧!

    普通函數(shù)中的 this

    我們來看例題:請給出下面代碼的運(yùn)行結(jié)果。

    例題1

    function f1 () {
       console.log(this)
    }
    f1() // window

    普通函數(shù)在非嚴(yán)格的全局環(huán)境下調(diào)用時,其中的 this 指向的是 window。

    例題2

    "use strict"
    function f1 () {
       console.log(this)
    }
    f1() // undefined

    用了嚴(yán)格模式 "use strict",嚴(yán)格模式下無法再意外創(chuàng)建全局變量,所以 this 不為 window 而為 undefined。

    注意:babel 轉(zhuǎn)成 ES6 的,babel 會自動給 js 文件上加上嚴(yán)格模式。

    箭頭函數(shù)中的 this

    在箭頭函數(shù)中,this 的指向是由外層(函數(shù)或全局)作用域來決定的。

    例題3

    const Animal = {
        getName: function() {
            setTimeout(function() {
                console.log(this)
            })
        }
    }
    Animal.getName() // window

    此時 this 指向 window。這里也印證了那句經(jīng)典的話:“匿名函數(shù)的 this 永遠(yuǎn)指向 window”。

    如果要讓 this 指向 Animal 這個對象,則可以巧用箭頭函數(shù)來解決。

    例題4

    const Animal = {
        getName: function() {
            setTimeout(() => {
                console.log(this)
            })
        }
    }
    Animal.getName() // {getName: ?}

    嚴(yán)格模式對箭頭函數(shù)沒有效果

    例題5

    "use strict";
    const f1 = () => {
       console.log(this)
    }
    f1() // window

    我們都知道箭頭函數(shù)體內(nèi)的 this 對象,就是定義時所在的對象,而不是使用時所在的對象。普通函數(shù)使用了嚴(yán)格模式 this 會指向 undefined 但箭頭函數(shù)依然指向了 window。

    函數(shù)作為對象的方法中的 this

    例題6

    const obj = {
        name: "coboy", 
        age: 18, 
        add: function() {
            console.log(this, this.name, this.age)
        }
    };
    obj.add(); // {name: "coboy", age: 18, add: ?} "coboy" 18

    在對象方法中,作為對象的一個方法被調(diào)用時,this 指向調(diào)用它所在方法的對象。也就是開頭我們所說的那句:“誰調(diào)用了它,它就指向誰”,在這里很明顯是 obj 調(diào)用了它。

    例題7

    const obj = {
        name: "coboy", 
        age: 18, 
        add: function() {
            console.log(this, this.name, this.age)
        }
    };
    const fn = obj.add
    fn() // window

    這個時候 this 則仍然指向 window。obj 對象方法 add 賦值給 fn 之后,fn 仍然在 window 的全局環(huán)境中執(zhí)行,所以 this 仍然指向 window。

    例題8

    const obj = {
        name: "coboy", 
        age: 18, 
        add: function() {
            function fn() {
                console.log(this)
            }
            fn()
        }
    };
    obj.add() // window

    如果在對象方法內(nèi)部聲明一個函數(shù),這個函數(shù)的 this 在對象方法執(zhí)行的時候指向就不是這個對象了,而是指向 window 了。

    同樣想要 this 指向 obj 可以通過箭頭函數(shù)來實(shí)現(xiàn):

    const obj = {
        name: "coboy", 
        age: 18, 
        add: function() {
            const fn = () => {
                console.log(this)
            }
            fn()
        }
    };
    obj.add() // obj

    再次說明箭頭函數(shù)的 this 是由外層函數(shù)作用域或者全局作用域決定的。

    上下文對象調(diào)用中的 this

    例題9

    const obj = {
        name: "coboy", 
        age: 18, 
        add: function() {
            return this
        }
    };
    console.log(obj.add() === obj) // true

    參考上文我們很容易知道 this 就是指向 obj 對象本身,所以返回 true。

    例題10

    const animal = {
        name: "coboy", 
        age: 18, 
        dog: {
            name: 'cobyte',
            getName: function() {
                console.log(this.name)
            }
        }
    };
    animal.dog.getName() // 'cobyte'

    如果函數(shù)中的 this 是被上一級的對象所調(diào)用的,那么 this 指向的就是上一級的對象,也就是開頭所說的:“誰調(diào)用了它,它就指向誰”。

    例題11

    const obj1 = {
        txt: 'coboy1',
        getName: function() {
            return this.txt
        }
    }
    const obj2 = {
        txt: 'coboy2',
        getName: function() {
            return obj1.getName()
        }
    }
    const obj3 = {
        txt: 'coboy3',
        getName: function() {
            return obj2.getName()
        }
    }
    console.log(obj1.getName()) 
    console.log(obj2.getName())
    console.log(obj3.getName())

    三個最終都打印了coboy1。

    執(zhí)行 obj3.getName 里面返回的是 obj2.getName 里面返回的結(jié)果,obj2.getName 里面返回的是 obj1.getName 的結(jié)果,obj1.getName 返回的結(jié)果就是 'coboy1'。

    如果上面的題改一下:

    例題12

    const obj1 = {
        txt: 'coboy1',
        getName: function() {
            return this.txt
        }
    }
    const obj2 = {
        txt: 'coboy2',
        getName: function() {
            return obj1.getName()
        }
    }
    const obj3 = {
        txt: 'coboy3',
        getName: function() {
            const fn = obj1.getName
            return fn()
        }
    }
    console.log(obj1.getName()) 
    console.log(obj2.getName())
    console.log(obj3.getName())

    這個時候輸出了 coboy1, coboy1, undefined。

    最后一個其實(shí)在上面例題5中已經(jīng)有說明了。通過 const fn = obj1.getName 的賦值進(jìn)行了“裸奔”調(diào)用,因此這里的 this 指向了 window,運(yùn)行結(jié)果當(dāng)然是 undefined。

    例題13

    上述的例題10中的 obj2.getName() 如果要它輸出‘coboy2’,如果不使用 bind、call、apply 方法該怎么做?

    const obj1 = {
        txt: 'coboy1',
        getName: function() {
            return this.txt
        }
    }
    const obj2 = {
        txt: 'coboy2',
        getName: obj1.getName
    }
    
    console.log(obj1.getName()) 
    console.log(obj2.getName())

    上述方法同樣說明了那個重要的結(jié)論:this 指向最后調(diào)用它的對象。

    我們將函數(shù) obj1 的 getName 函數(shù)掛載到了 obj2 的對象上,getName 最終作為 obj2 對象的方法被調(diào)用。

    在構(gòu)造函數(shù)中的 this

    通過 new 操作符來構(gòu)建一個構(gòu)造函數(shù)的實(shí)例對象,這個構(gòu)造函數(shù)中的 this 就指向這個新的實(shí)例對象。同時構(gòu)造函數(shù) prototype 屬性下面方法中的 this 也指向這個新的實(shí)例對象。

    例題14

    function Animal(){
        console.log(this) // Animal {}
    }
    const a1 = new Animal();
    console.log(a1) // Animal {}
    function Animal(){
        this.txt = 'coboy';
        this.age = 100;
    }
    Animal.prototype.getNum = function(){
        return this.txt;
    }
    const a1 = new Animal();
    console.log(a1.age) // 100
    console.log(a1.getNum()) // 'coboy'

    在構(gòu)造函數(shù)中出現(xiàn)顯式 return 的情況。

    例題15

    function Animal(){
        this.txt = 'coboy'
        const obj = {txt: 'cobyte'}
        return obj
    }
    
    const a1 = new Animal();
    console.log(a1.txt) // cobyte

    此時 a1 返回的是空對象 obj。

    例題16

    function Animal(){
        this.txt = 'coboy'
        return 1
    }
    
    const a1 = new Animal();
    console.log(a1.txt) // 'coboy'

    由此可以看出,如果構(gòu)造函數(shù)中顯式返回一個值,且返回的是一個對象,那么 this 就指向返回的對象,如果返回的不是一個對象,而是基本類型,那么 this 仍然指向?qū)嵗?/p>

    call,apply,bind 顯式修改 this 指向

    call方法

    例題17

    const obj = {
        txt: "coboy", 
        age: 18, 
        getName: function() {
            console.log(this, this.txt)
        }
    };
    const obj1 = {
        txt: 'cobyte'
    }
    obj.getName(); // this指向obj
    obj.getName.call(obj1); // this指向obj1
    obj.getName.call(); // this指向window

    apply方法

    例題18

    const obj = {
        txt: "coboy", 
        age: 18, 
        getName: function() {
            console.log(this, this.txt)
        }
    };
    const obj1 = {
        txt: 'cobyte'
    }
    
    obj.getName.apply(obj1) // this指向obj1
    obj.getName.apply() // this指向window

    call 方法和 apply 方法的區(qū)別

    例題19

    const obj = {
        txt: "coboy", 
        age: 18, 
        getName: function(name1, name2) {
            console.log(this, name1, name2)
        }
    };
    const obj1 = {
        txt: 'cobyte'
    }
    
    obj.getName.call(obj1, 'coboy1', 'coboy2')
    obj.getName.apply(obj1, ['coboy1', 'coboy2'])

    可見 call 和 apply 主要區(qū)別是在傳參上。apply 方法與 call 方法用法基本相同,不同點(diǎn)主要是 call() 方法的第二個參數(shù)和之后的參數(shù)可以是任意數(shù)據(jù)類型,而 apply 的第二個參數(shù)是數(shù)組類型或者 arguments 參數(shù)集合。

    bind 方法

    例題20

    const obj = {
        txt: "coboy", 
        age: 18, 
        getName: function() {
            console.log(this.txt)
        }
    };
    const obj2 = {
        txt: "cobyte"
    }
    const newGetName = obj.getName.bind(obj2)
    newGetName() // this指向obj2
    obj.getName() // this仍然指向obj

    bind() 方法也能修改 this 指向,不過調(diào)用 bind() 方法不會執(zhí)行 getName()函數(shù),也不會改變 getName() 函數(shù)本身,只會返回一個已經(jīng)修改了 this 指向的新函數(shù),這個新函數(shù)可以賦值給一個變量,調(diào)用這個變量新函數(shù)才能執(zhí)行 getName()。

    call() 方法和 bind() 方法的區(qū)別在于

    • bind 的返回值是函數(shù),并且不會自動調(diào)用執(zhí)行。

    • 兩者后面的參數(shù)的使用也不同。call 是 把第二個及以后的參數(shù)作為原函數(shù)的實(shí)參傳進(jìn)去, 而 bind 實(shí)參在其傳入?yún)?shù)的基礎(chǔ)上往后獲取參數(shù)執(zhí)行。

    例題21

    function fn(a, b, c){ 
        console.log(a, b, c); 
    }
    const fn1 = fn.bind({abc : 123},600);
    fn(100,200,300) // 100,200,300 
    fn1(100,200,300) // 600,100,200 
    fn1(200,300) // 600,200,300 
    fn.call({abc : 123},600) // 600,undefined,undefined
    fn.call({abc : 123},600,100,200) // 600,100,200

    this 優(yōu)先級

    我們通常把通過 call、apply、bind、new 對 this 進(jìn)行綁定的情況稱為顯式綁定,而把根據(jù)調(diào)用關(guān)系確定 this 指向的情況稱為隱式綁定。那么顯示綁定和隱式綁定誰的優(yōu)先級更高呢?

    例題22

    function getName() {
        console.log(this.txt)
    }
    
    const obj1 = {
        txt: 'coboy1',
        getName: getName
    }
    
    const obj2 = {
        txt: 'coboy2',
        getName: getName
    }
    
    obj1.getName.call(obj2) // 'coboy2'
    obj2.getName.apply(obj1) // 'coboy1'

    可以看出 call、apply 的顯示綁定比隱式綁定優(yōu)先級更高些。

    例題23

    function getName(name) {
       this.txt = name
    }
    
    const obj1 = {}
    
    const newGetName = getName.bind(obj1)
    newGetName('coboy')
    console.log(obj1) // {txt: "coboy"}

    當(dāng)再使用 newGetName 作為構(gòu)造函數(shù)時。

    const obj2 = new newGetName('cobyte')
    console.log(obj2.txt) // 'cobyte'

    這個時候新對象中的 txt 屬性值為 'cobyte'。

    newGetName 函數(shù)本身是通過 bind 方法構(gòu)造的函數(shù),其內(nèi)部已經(jīng)將this綁定為 obj1,當(dāng)它再次作為構(gòu)造函數(shù)通過 new 被調(diào)用時,返回的實(shí)例就已經(jīng)和 obj1 解綁了。也就是說,new 綁定修改了 bind 綁定中的 this 指向,所以 new 綁定的優(yōu)先級比顯式 bind 綁定的更高。

    例題24

    function getName() {
       return name => {
          return this.txt
       }
    }
    
    const obj1 = { txt: 'coboy1' }
    const obj2 = { txt: 'coboy2' }
    
    const newGetName = getName.call(obj1)
    console.log(newGetName.call(obj2)) // 'coboy1'

    由于 getName 中的 this 綁定到了 obj1 上,所以 newGetName(引用箭頭函數(shù)) 中的 this 也會綁到 obj1 上,箭頭函數(shù)的綁定無法被修改。

    例題25

    var txt = 'good boy'
    const getName = () => name => {
        return this.txt
    }
    
    const obj1 = { txt: 'coboy1' }
    const obj2 = { txt: 'coboy2' }
    
    const newGetName = getName.call(obj1)
    console.log(newGetName.call(obj2)) // 'good boy'

    例題26

    const txt = 'good boy'
    const getName = () => name => {
        return this.txt
    }
    
    const obj1 = { txt: 'coboy1' }
    const obj2 = { txt: 'coboy2' }
    
    const newGetName = getName.call(obj1)
    console.log(newGetName.call(obj2)) // undefined

    const 聲明的變量不會掛到 window 全局對象上,所以 this 指向 window 時,自然也找不到 txt 變量了。

    箭頭函數(shù)的 this 綁定無法修改

    例題27

    function Fn() {
        return txt => {
            return this.txt
        }
    }
    
    const obj1 = {
        txt: 'coboy'
    }
    const obj2 = {
        txt: 'cobyte'
    }
    
    const f = Fn.call(obj1)
    console.log(f.call(obj2)) // 'coboy'

    由于 Fn 中的 this 綁定到了 obj1 上,所以 f 中的 this 也會綁定到 obj1 上, 箭頭函數(shù)的綁定無法被修改。

    例題28

    var txt = '意外不'
    const Fn = () => txt => {
       return this.txt
    }
    
    const obj1 = {
        txt: 'coboy'
    }
    const obj2 = {
        txt: 'cobyte'
    }
    
    const f = Fn.call(obj1)
    console.log(f.call(obj2)) // '意外不'

    如果將 var 聲明方式改成 const 或 let 則最后輸出為 undefined,原因是使用 const 或 let 聲明的變量不會掛載到 window 全局對象上。因此,this 指向 window 時,自然也找不到 txt 變量了。

    從手寫 new 操作符中去理解 this

    有一道經(jīng)典的面試題,JS 的 new 操作符到底做了什么?

    • 創(chuàng)建一個新的空對象

    • 把這個新的空對象的隱式原型(__proto__)指向構(gòu)造函數(shù)的原型對象(prototype

    • 把構(gòu)造函數(shù)中的 this 指向新創(chuàng)建的空對象并且執(zhí)行構(gòu)造函數(shù)返回執(zhí)行結(jié)果

    • 判斷返回的執(zhí)行結(jié)果是否是引用類型,如果是引用類型則返回執(zhí)行結(jié)果,new 操作失敗,否則返回創(chuàng)建的新對象

    /*
      create函數(shù)要接受不定量的參數(shù),第一個參數(shù)是構(gòu)造函數(shù)(也就是new操作符的目標(biāo)函數(shù)),其余參數(shù)被構(gòu)造函數(shù)使用。
      new Create() 是一種js語法糖。我們可以用函數(shù)調(diào)用的方式模擬實(shí)現(xiàn)
    */
    function create(Fn,...args){
        // 1、創(chuàng)建一個空的對象
        let obj = {}; // let obj = Object.create({});
        // 2、將空對象的原型prototype指向構(gòu)造函數(shù)的原型
        Object.setPrototypeOf(obj,Fn.prototype); // obj.__proto__ = Fn.prototype 
        // 以上 1、2步還可以通過 const obj = Object.create(Fn.prototype) 實(shí)現(xiàn)
        // 3、改變構(gòu)造函數(shù)的上下文(this),并將參數(shù)傳入
        let result = Fn.apply(obj,args);
        // 4、如果構(gòu)造函數(shù)執(zhí)行后,返回的結(jié)果是對象類型,則直接將該結(jié)果返回,否則返回 obj 對象
        return result instanceof Object ? result : obj;
        // return typeof result === 'object' && result != null ? result : obj
    }

    一般情況下構(gòu)造函數(shù)沒有返回值,但是作為函數(shù),是可以有返回值的,這就解析了上面例題15和例題16的原因了。 在 new 的時候,會對構(gòu)造函數(shù)的返回值做一些判斷:如果返回值是基礎(chǔ)數(shù)據(jù)類型,則忽略返回值,如果返回值是引用數(shù)據(jù)類型,則使用 return 的返回,也就是 new 操作符無效。

    從手寫 call、apply、bind 中去理解 this

    手寫 call 的實(shí)現(xiàn)

    Function.prototype.myCall = function (context, ...args) {
        context = context || window
        // 創(chuàng)建唯一的屬性防止污染
        const key = Symbol()
        // this 就是綁定的那個函數(shù)
        context[key] = this
        const result = context[key](...args)
        delete context[key]
        return result
    }
    • myCall 中的 this 指向誰?

    myCall 已經(jīng)設(shè)置在 Function 構(gòu)造函數(shù)的原型對象(prototype)上了,所以每個函數(shù)都可以調(diào)用 myCall 方法,比如函數(shù) Fn.myCall(),根據(jù) this 的確定規(guī)律:“誰調(diào)用它,this 就指向誰”,所以myCall方法內(nèi)的 this 就指向了調(diào)用的函數(shù),也可以說是要綁定的那個函數(shù)。

    • Fn.myCall(obj) 本質(zhì)就是把函數(shù) Fn 賦值到 對象 obj 上,然后通過對象 obj.Fn() 來執(zhí)行函數(shù) Fn,那么最終又回到那個 this 的確定規(guī)律:“誰調(diào)用它,this 就指向誰”,因?yàn)閷ο?obj 調(diào)用了 Fn 所以 Fn 內(nèi)部的 this 就指向了對象 obj。

    手寫 apply 的實(shí)現(xiàn)

    apply 的實(shí)現(xiàn)跟 call 的實(shí)現(xiàn)基本是一樣的,因?yàn)樗麄兊氖褂梅绞揭不疽粯樱皇莻鲄⒌姆绞讲灰粯?。apply 的參數(shù)必須以數(shù)組的形式傳參。

    Function.prototype.myApply = function (context, args) {
        if(!Array.isArray(args)) {
           new Error('參數(shù)必須是數(shù)組')
        }
        context = context || window
        // 創(chuàng)建唯一的屬性防止污染
        const key = Symbol()
        // this 就是綁定的那個函數(shù)
        context[key] = this
        const result = context[key](...args)
        delete context[key]
        return result
    }

    手寫 bind 的實(shí)現(xiàn)

    bind 和 call、apply 方法的區(qū)別是它不會立即執(zhí)行,它是返回一個改變了 this 指向的函數(shù),在綁定的時候可以傳參,之后執(zhí)行的時候,還可以繼續(xù)傳參數(shù)數(shù)。這個就是一個典型的閉包行為了,是不是。

    我們先來實(shí)現(xiàn)一個簡單版的:

    Function.prototype.myBind = function (ctx, ...args) {
        // 根據(jù)上文我們可以知道 this 就是調(diào)用的那個函數(shù)
        const self = this
        return function bound(...newArgs) {
            // 在再次執(zhí)行的的時候去改變 this 的指向
            return self.apply(ctx, [...args, ...newArgs])
        }
    }

    但是,就如之前在 this 優(yōu)先級分析那里所展示的規(guī)則:bind 返回的函數(shù)如果作為構(gòu)造函數(shù)通過 new 操作符進(jìn)行實(shí)例化操作的話,綁定的 this 就會實(shí)效。

    為了實(shí)現(xiàn)這樣的規(guī)則,我們就需要區(qū)分這兩種情況的調(diào)用方式,那么怎么區(qū)分呢?首先返回出去的是 bound 函數(shù),那么 new 操作符實(shí)例化的就是 bound 函數(shù)。通過上文 “從手寫 new 操作符中去理解 this” 中我們可以知道當(dāng)函數(shù)被 new 進(jìn)行實(shí)例化的時候, 構(gòu)造函數(shù)內(nèi)部的 this 就是指向?qū)嵗膶ο?,那么判斷一個函數(shù)是否是一個實(shí)例化的對象的構(gòu)造函數(shù)時可以通過 intanceof 操作符判斷。

    知識點(diǎn): instanceof 運(yùn)算符用于檢測構(gòu)造函數(shù)的 prototype 屬性是否出現(xiàn)在某個實(shí)例對象的原型鏈上。

    Function.prototype.myBind = function (ctx, ...args) {
        // 根據(jù)上文我們可以知道 this 就是調(diào)用的那個函數(shù)
        const self = this
        return function bound(...newArgs) {
            // new Fn 的時候, this 是 Fn 的實(shí)例對象
            if(this instanceof Fn) {
                return new self(...args, ...newArgs)
            }
            // 在再次執(zhí)行的的時候去改變 this 的指向
            return self.apply(ctx, [...args, ...newArgs])
        }
    }

    另外我們也可以通過上文的實(shí)現(xiàn) new 操作符的代碼來實(shí)現(xiàn) bind 里面的 new 操作。

    完整的復(fù)雜版:

    Function.prototype.myBind = function (ctx) {
      const self = this
      if (!Object.prototype.toString.call(self) === '[object Function]') {
        throw TypeError('myBind must be called on a function');
      }
      // 對 context 進(jìn)行深拷貝,防止 bind 執(zhí)行后返回函數(shù)未執(zhí)行期間,context 被修改
      ctx = JSON.parse(JSON.stringify(ctx)) || window;
    
      const args = Array.prototype.slice.call(arguments, 1);
    
      /**
       * 構(gòu)造函數(shù)生成對象實(shí)例
       * @returns {Object|*}
       */
      const create = function (conFn) {
        const obj = {};
    
        /* 設(shè)置原型指向,確定繼承關(guān)系 */
        obj.__proto__ = conFn.prototype;
    
        /**
         * 1、執(zhí)行目標(biāo)函數(shù),綁定函數(shù)內(nèi)部的屬性
         * 2、如果目標(biāo)函數(shù)有對象類型的返回值則取返回值,符合js new關(guān)鍵字的規(guī)范
         */
        const res = conFn.apply(obj, Array.prototype.slice.call(arguments,1));
        return typeof res === 'object' && res != null ? res : obj;
      };
    
      const bound = function () {
        // new 操作符操作的時候
        if (this instanceof bound) {
          return create(self, args.concat(Array.prototype.slice.call(arguments)));
        }
        return self.apply(ctx, args.concat(Array.prototype.slice.call(arguments)));
      };
    
      return bound;
    };

    為什么顯式綁定的 this 要比隱式綁定的 this 優(yōu)先級要高

    通過上面的實(shí)現(xiàn)原理,我們就可以理解為什么上面的 this 優(yōu)先級中通過 call、apply、bind 和 new 操作符的顯式綁定的 this 要比隱式綁定的 this 優(yōu)先級要高了。例如上面的 obj1.getName.call(obj2) 中的 getName 方法本來是通過 obj1 來調(diào)用的,但通過 call 方法之后,實(shí)際 getName 方法變成了 obj2.getName() 來執(zhí)行了。

    到此,相信大家對“JavaScript中的this實(shí)例分析”有了更深的了解,不妨來實(shí)際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

    向AI問一下細(xì)節(jié)
    AI