JavaScript中this究竟指向什么档押?

摘要: 神奇的this琳骡!

Fundebug經(jīng)授權(quán)轉(zhuǎn)載锅论,版權(quán)歸原作者所有。

1. this 的奧秘

很多時(shí)候, JS 中的 this 對(duì)于咱們的初學(xué)者很容易產(chǎn)生困惑不解楣号。 this 的功能很強(qiáng)大最易,但需要一定付出才能慢慢理解它。

對(duì)Java炫狱、PHP或其他標(biāo)準(zhǔn)語言來看耘纱,this 表示類方法中當(dāng)前對(duì)象的實(shí)例。大多數(shù)情況下毕荐,this 不能在方法之外使用束析,這樣就比較不會(huì)造成混淆。

在J要中情況就有所不同: this表示函數(shù)的當(dāng)前執(zhí)行上下文憎亚,JS 中函數(shù)調(diào)用主要有以下幾種方式:

  • 函數(shù)調(diào)用: alert('Hello World!')
  • 方法調(diào)用: console.log('Hello World!')
  • 構(gòu)造函數(shù): new RegExp('\\d')
  • 隱式調(diào)用: alert.call(undefined, 'Hello World!')

每種調(diào)用類型以自己的方式定義上下文员寇,所以就很容易產(chǎn)生混淆弄慰。

此外,嚴(yán)格模式也會(huì)影響執(zhí)行上下文蝶锋。

理解this關(guān)鍵是要清楚的知道函數(shù)調(diào)用及其如何影響上下文陆爽。

本文主要說明函數(shù)的調(diào)用方式及如何影響 this,并且說明執(zhí)行上下文的常見陷阱扳缕。

在開始之前慌闭,先知道幾個(gè)術(shù)語:

調(diào)用函數(shù)正在執(zhí)行創(chuàng)建函數(shù)體的代碼,或者只是調(diào)用函數(shù)躯舔。 例如驴剔,parseInt函數(shù)調(diào)用是parseInt('15')。

  • 函數(shù)調(diào)用:執(zhí)行構(gòu)成函數(shù)主體的代碼:例如粥庄,parseInt函數(shù)調(diào)用是parseInt('15')丧失。
  • 調(diào)用的上下文:指 this 在函數(shù)體內(nèi)的值。 例如惜互,map.set('key', 'value')的調(diào)用上下文是 map布讹。
  • 函數(shù)的作用域:是在函數(shù)體中可訪問的變量、對(duì)象和函數(shù)的集合训堆。

2. 函數(shù)調(diào)用

當(dāng)一個(gè)表達(dá)式為函數(shù)接著一個(gè)(描验,一些用逗號(hào)分隔的參數(shù)以及一個(gè)時(shí),函數(shù)調(diào)用被執(zhí)行坑鱼,例如parseInt('18')挠乳。

函數(shù)調(diào)用表達(dá)式不能是屬性方式的調(diào)用,如 obj.myFunc()姑躲,這種是創(chuàng)建一個(gè)方法調(diào)用睡扬。再如 [1,5].join(',')不是函數(shù)調(diào)用,而是方法調(diào)用黍析,這種區(qū)別需要記住哈卖怜,很重要滴

函數(shù)調(diào)用的一個(gè)簡(jiǎn)單示例:

    function hello(name) {
      return 'Hello ' + name + '!';
    }
    // 函數(shù)調(diào)用
    const message = hello('World');
    console.log(message); // => 'Hello World!'

hello('World')是函數(shù)調(diào)用: hello表達(dá)式等價(jià)于一個(gè)函數(shù)阐枣,跟在它后面的是一對(duì)括號(hào)以及'World'參數(shù)马靠。

一個(gè)更高級(jí)的例子是IIFE(立即調(diào)用的函數(shù)表達(dá)式)

    const message = (function(name) {
       return 'Hello ' + name + '!';
    })('World');
    console.log(message) // => 'Hello World!'

IIFE也是一個(gè)函數(shù)調(diào)用:第一對(duì)圓括號(hào)(function(name) {...})是一個(gè)表達(dá)式,它的計(jì)算結(jié)果是一個(gè)函數(shù)對(duì)象蔼两,后面跟著一對(duì)圓括號(hào)甩鳄,圓括號(hào)的參數(shù)是“World”

2.1. 在函數(shù)調(diào)用中的this

this 在函數(shù)調(diào)用中是一個(gè)全局對(duì)象

局對(duì)象由執(zhí)行環(huán)境決定额划。在瀏覽器中妙啃,thiswindow 對(duì)象。

image

在函數(shù)調(diào)用中,執(zhí)行上下文是全局對(duì)象揖赴。

再來看看下面函數(shù)中的上下文又是什么鬼:

    function sum(a, b) {
       console.log(this === window); // => true
       this.myNumber = 20; // 將'myNumber'屬性添加到全局對(duì)象
       return a + b;
    }
    // sum() is invoked as a function
    // sum() 中的 `this` 是一個(gè)全局對(duì)象(window)
    sum(15, 16);     // => 31
    window.myNumber; // => 20

在調(diào)用sum(15,16)時(shí)馆匿,JS 自動(dòng)將this設(shè)置為全局對(duì)象,在瀏覽器中該對(duì)象是window燥滑。

當(dāng)this在任何函數(shù)作用域(最頂層作用域:全局執(zhí)行上下文)之外使用渐北,this 表示 window 對(duì)象

    console.log(this === window); // => true
    this.myString = 'Hello World!';
    console.log(window.myString); // => 'Hello World!'
    <!-- In an html file -->
    <script type="text/javascript">
       console.log(this === window); // => true
    </script>

2.2 嚴(yán)格模式下的函數(shù)調(diào)用 this 又是什么樣的

this 在嚴(yán)格模式下的函數(shù)調(diào)用中為 undefined

嚴(yán)格模式是在 ECMAScript 5.1中引入的,它提供了更好的安全性和更強(qiáng)的錯(cuò)誤檢查铭拧。

要啟用嚴(yán)格模式赃蛛,函數(shù)頭部寫入use strict 即可。

啟用后搀菩,嚴(yán)格模式會(huì)影響執(zhí)行上下文呕臂,this 在常規(guī)函數(shù)調(diào)用中值為undefined。 與上述情況2.1相反秕磷,執(zhí)行上下文不再是全局對(duì)象。

image

嚴(yán)格模式函數(shù)調(diào)用示例:

    function multiply(a, b) {
      'use strict'; // 啟用嚴(yán)格模式
      console.log(this === undefined); // => true
      return a * b;
    }
    multiply(2, 5); // => 10

當(dāng)multiply(2,5)作為函數(shù)調(diào)用時(shí)炼团,thisundefined澎嚣。

嚴(yán)格模式不僅在當(dāng)前作用域中有效,在內(nèi)部作用域中也是有效的(對(duì)于在內(nèi)部聲明的所有函數(shù)):

    function execute() {
       'use strict'; // 開啟嚴(yán)格模式  
       function concat(str1, str2) {
         // 嚴(yán)格模式仍然有效  
         console.log(this === undefined); // => true
         return str1 + str2;
       }
       // concat() 在嚴(yán)格模式下作為函數(shù)調(diào)用
       // this in concat() is undefined
       concat('Hello', ' World!'); // => "Hello World!"
    }
    execute();

'use strict'被插入到執(zhí)行體的頂部瘟芝,在其作用域內(nèi)啟用嚴(yán)格模式易桃。 因?yàn)楹瘮?shù)concat是在執(zhí)行的作用域中聲明的,所以它繼承了嚴(yán)格模式锌俱。

單個(gè)JS文件可能包含嚴(yán)格和非嚴(yán)格模式晤郑。 因此,對(duì)于相同的調(diào)用類型贸宏,可以在單個(gè)腳本中具有不同的上下文行為:

    function nonStrictSum(a, b) {
      // 非嚴(yán)格模式
      console.log(this === window); // => true
      return a + b;
    }
    function strictSum(a, b) {
      'use strict';
      // 啟用嚴(yán)格模式
      console.log(this === undefined); // => true
      return a + b;
    }
    
    nonStrictSum(5, 6); // => 11
    strictSum(8, 12); // => 20

2.3 陷阱:this 在內(nèi)部函數(shù)中的時(shí)候

函數(shù)調(diào)用的一個(gè)常見陷阱是造寝,認(rèn)為this在內(nèi)部函數(shù)中的情況與外部函數(shù)中的情況相同。

正確地說吭练,內(nèi)部函數(shù)的上下文只依賴于它的調(diào)用類型诫龙,而不依賴于外部函數(shù)的上下文。

要將 this 設(shè)置為所需的值鲫咽,可以通過 .call().apply()修改內(nèi)部函數(shù)的上下文或使用.bind()創(chuàng)建綁定函數(shù)签赃。

下面的例子是計(jì)算兩個(gè)數(shù)的和:

    const numbers = {
       numberA: 5,
       numberB: 10,
       sum: function() {
         console.log(this === numbers); // => true
         function calculate() {
           console.log(this === numbers); // => false
           return this.numberA + this.numberB;
         }
         return calculate();
       }
    };
    numbers.sum(); // => NaN 

sum()是對(duì)象上的方法調(diào)用,所以sum中的上下文是numbers對(duì)象分尸。calculate函數(shù)是在sum中定義的锦聊,你可能希望在calculate()this也表示number對(duì)象。

calculate()是一個(gè)函數(shù)調(diào)用(不是方法調(diào)用)箩绍,它將this作為全局對(duì)象window(非嚴(yán)格模下)孔庭。即使外部函數(shù)sum將上下文作為number對(duì)象,它在calculate里面沒有影響材蛛。

sum()的調(diào)用結(jié)果是NaN史飞,不是預(yù)期的結(jié)果5 + 10 = 15尖昏,這都是因?yàn)闆]有正確調(diào)用calculate

為了解決這個(gè)問題构资,calculate函數(shù)中上下文應(yīng)該與 sum 中的一樣抽诉,以便可以訪問numberAnumberB屬性。

一種解決方案是通過調(diào)用calculator.call(this)手動(dòng)將calculate上下文更改為所需的上下文吐绵。

    const numbers = {
       numberA: 5,
       numberB: 10,
       sum: function() {
         console.log(this === numbers); // => true
         function calculate() {
           console.log(this === numbers); // => true
           return this.numberA + this.numberB;
         }
         // 使用 .call() 方法修改上下文
         return calculate.call(this);
       }
    };
    numbers.sum(); // => 15

call(this)像往常一樣執(zhí)行calculate函數(shù)迹淌,但 call 會(huì)把上下文修改為指定為第一個(gè)參數(shù)的值。

現(xiàn)在this.numberA + this.numberB相當(dāng)于numbers.numberA + numbers.numberB己单。 該函數(shù)返回預(yù)期結(jié)果5 + 10 = 15唉窃。

另一種就是使用箭頭函數(shù)

    const numbers = {
       numberA: 5,
       numberB: 10,
       sum: function() {
         console.log(this === numbers); // => true
         const calculate = () => {
           console.log(this === numbers); // => true
           return this.numberA + this.numberB;
         }
         return calculate();
       }
    };
    numbers.sum(); // => 15

3. 方法調(diào)用

方法是存儲(chǔ)在對(duì)象屬性中的函數(shù)。例如

    const myObject = {
      // helloFunction 是一個(gè)方法
      helloFunction: function() {
        return 'Hello World!';
      }
    };
    const message = myObject.helloFunction();

helloFunctionmyObject的一個(gè)方法纹笼,要調(diào)用該方法纹份,可以這樣子調(diào)用 :myObject.helloFunction

當(dāng)一個(gè)表達(dá)式以屬性訪問的形式執(zhí)行時(shí)廷痘,執(zhí)行的是方法調(diào)用蔓涧,它相當(dāng)于以個(gè)函數(shù)接著(,一組用逗號(hào)分隔的參數(shù)以及)笋额。

利用前面的例子元暴,myObject.helloFunction()是對(duì)象myObject上的一個(gè)helloFunction的方法調(diào)用。[1, 2].join(',')/\s/.test('beautiful world')也被認(rèn)為是方法調(diào)用兄猩。

區(qū)分函數(shù)調(diào)用和方法調(diào)用非常重要茉盏,因?yàn)樗鼈兪遣煌念愋汀V饕獏^(qū)別在于方法調(diào)用需要一個(gè)屬性訪問器形式來調(diào)用函數(shù)(obj.myFunc()obj['myFunc']())枢冤,而函數(shù)調(diào)用不需要(myFunc())鸠姨。

    ['Hello', 'World'].join(', '); // 方法調(diào)用
    ({ ten: function() { return 10; } }).ten(); // 方法調(diào)用
    const obj = {};
    obj.myFunction = function() {
      return new Date().toString();
    };
    obj.myFunction(); // 方法調(diào)用
    
    const otherFunction = obj.myFunction;
    otherFunction();     // 函數(shù)調(diào)用
    parseFloat('16.60'); // 函數(shù)調(diào)用
    isNaN(0);            // 函數(shù)調(diào)用

理解函數(shù)調(diào)用和方法調(diào)用之間的區(qū)別有助于正確識(shí)別上下文。

3.1 方法調(diào)用中 this 是腫么樣

在方法調(diào)用中淹真,this是擁有這個(gè)方法的對(duì)象

當(dāng)調(diào)用對(duì)象上的方法時(shí)享怀,this就變成了對(duì)象本身。

image

創(chuàng)建一個(gè)對(duì)象趟咆,該對(duì)象有一個(gè)遞增數(shù)字的方法

    const calc = {
      num: 0,
      increment: function() {
        console.log(this === calc); // => true
        this.num += 1;
        return this.num;
      }
    };
    // method invocation. this is calc
    calc.increment(); // => 1
    calc.increment(); // => 2

調(diào)用calc.increment()使increment函數(shù)的上下文成為calc對(duì)象添瓷。所以使用this.num來增加num屬性是有效的。

再來看看另一個(gè)例子值纱。JS對(duì)象從原型繼承一個(gè)方法鳞贷,當(dāng)在對(duì)象上調(diào)用繼承的方法時(shí),調(diào)用的上下文仍然是對(duì)象本身

    const myDog = Object.create({
      sayName: function() {
         console.log(this === myDog); // => true
         return this.name;
      }
    });
    myDog.name = 'Milo';
    // 方法調(diào)用 this 指向 myDog
    myDog.sayName(); // => 'Milo'

Object.create()創(chuàng)建一個(gè)新對(duì)象myDog虐唠,并根據(jù)第一個(gè)參數(shù)設(shè)置其原型搀愧。myDog對(duì)象繼承sayName方法。

執(zhí)行myDog. sayname()時(shí),myDog是調(diào)用的上下文咱筛。

在EC6 class 語法中搓幌,方法調(diào)用上下文也是實(shí)例本身

    class Planet {
      constructor(name) {
        this.name = name;    
      }
      getName() {
        console.log(this === earth); // => true
        return this.name;
      }
    }
    var earth = new Planet('Earth');
    // method invocation. the context is earth
    earth.getName(); // => 'Earth'

3.2 陷阱:將方法與其對(duì)象分離

方法可以從對(duì)象中提取到一個(gè)單獨(dú)的變量const alone = myObj.myMethod。當(dāng)方法單獨(dú)調(diào)用時(shí)迅箩,與原始對(duì)象alone()分離溉愁,你可能認(rèn)為當(dāng)前的this就是定義方法的對(duì)象myObject

如果方法在沒有對(duì)象的情況下調(diào)用饲趋,那么函數(shù)調(diào)用就會(huì)發(fā)生拐揭,此時(shí)的this指向全局對(duì)象window嚴(yán)格模式下是undefined

下面的示例定義了Animal構(gòu)造函數(shù)并創(chuàng)建了它的一個(gè)實(shí)例:myCat奕塑。然后setTimout()在1秒后打印myCat對(duì)象信息

    function Animal(type, legs) {
      this.type = type;
      this.legs = legs;  
      this.logInfo = function() {
        console.log(this === myCat); // => false
        console.log('The ' + this.type + ' has ' + this.legs + ' legs');
      }
    }
    const myCat = new Animal('Cat', 4);
    // The undefined has undefined legs 
    setTimeout(myCat.logInfo, 1000);

你可能認(rèn)為setTimout調(diào)用myCat.loginfo()時(shí)堂污,它應(yīng)該打印關(guān)于myCat對(duì)象的信息。

不幸的是龄砰,方法在作為參數(shù)傳遞時(shí)與對(duì)象是分離盟猖,setTimout(myCat.logInfo)以下情況是等效的:

    setTimout(myCat.logInfo);
    // 等價(jià)于
    const extractedLogInfo = myCat.logInfo;
    setTimout(extractedLogInfo);

將分離的logInfo作為函數(shù)調(diào)用時(shí),this是全局 window,所以對(duì)象信息沒有正確地打印换棚。

函數(shù)可以使用.bind()方法與對(duì)象綁定,就可以解決 this 指向的問題式镐。

    function Animal(type, legs) {
      this.type = type;
      this.legs = legs;  
      this.logInfo = function() {
        console.log(this === myCat); // => true
        console.log('The ' + this.type + ' has ' + this.legs + ' legs');
      };
    }
    const myCat = new Animal('Cat', 4);
    // logs "The Cat has 4 legs"
    setTimeout(myCat.logInfo.bind(myCat), 1000);

myCat.logInfo.bind(myCat)返回一個(gè)新函數(shù),它的執(zhí)行方式與logInfo完全相同圃泡,但是此時(shí)的 this 指向 myCat碟案,即使在函數(shù)調(diào)用中也是如此愿险。

另一種解決方案是將logInfo()方法定義為一個(gè)箭頭函數(shù):

    function Animal(type, legs) {
      this.type = type;
      this.legs = legs;  
      this.logInfo = () => {
        console.log(this === myCat); // => true
        console.log('The ' + this.type + ' has ' + this.legs + ' legs');
      };
    }
    const myCat = new Animal('Cat', 4);
    // logs "The Cat has 4 legs"
    setTimeout(myCat.logInfo, 1000);

4. 構(gòu)造函數(shù)調(diào)用

當(dāng)new關(guān)鍵詞緊接著函數(shù)對(duì)象,(,一組逗號(hào)分隔的參數(shù)以及)時(shí)被調(diào)用颇蜡,執(zhí)行的是構(gòu)造函數(shù)調(diào)用如new RegExp('\\d')

聲明了一個(gè)Country函數(shù)辆亏,并且將它作為一個(gè)構(gòu)造函數(shù)調(diào)用:

    function Country(name, traveled) {
       this.name = name ? name : 'United Kingdom';
       this.traveled = Boolean(traveled); 
    }
    Country.prototype.travel = function() {
      this.traveled = true;
    };
    // 構(gòu)造函數(shù)調(diào)用
    const france = new Country('France', false);
    // 構(gòu)造函數(shù)調(diào)用
    const unitedKingdom = new Country;
    
    france.travel(); // Travel to France

new Country('France', false)Country函數(shù)的構(gòu)造函數(shù)調(diào)用风秤。它的執(zhí)行結(jié)果是一個(gè)name屬性為'France'的新的對(duì)象。 如果這個(gè)構(gòu)造函數(shù)調(diào)用時(shí)不需要參數(shù)扮叨,那么括號(hào)可以省略:new Country缤弦。

從ES6開始,JS 允許用class關(guān)鍵詞來定義構(gòu)造函數(shù)

    class City {
      constructor(name, traveled) {
        this.name = name;
        this.traveled = false;
      }
      travel() {
        this.traveled = true;
      }
    }
    // Constructor invocation
    const paris = new City('Paris', false);
    paris.travel();

new City('Paris')是構(gòu)造函數(shù)調(diào)用彻磁。這個(gè)對(duì)象的初始化由這個(gè)類中一個(gè)特殊的方法constructor來處理碍沐。其中,this指向新創(chuàng)建的對(duì)象衷蜓。

構(gòu)造函數(shù)創(chuàng)建了一個(gè)新的空的對(duì)象累提,它從構(gòu)造函數(shù)的原型繼承了屬性。構(gòu)造函數(shù)的作用就是去初始化這個(gè)對(duì)象磁浇。 可能你已經(jīng)知道了斋陪,在這種類型的調(diào)用中,上下文指向新創(chuàng)建的實(shí)例。

當(dāng)屬性訪問myObject.myFunction前面有一個(gè)new關(guān)鍵詞時(shí)无虚,JS會(huì)執(zhí)行構(gòu)造函數(shù)調(diào)用而不是原來的方法調(diào)用缔赠。

例如new myObject.myFunction():它相當(dāng)于先用屬性訪問把方法提取出來extractedFunction = myObject.myFunction,然后利用把它作為構(gòu)造函數(shù)創(chuàng)建一個(gè)新的對(duì)象: new extractedFunction()友题。

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

在構(gòu)造函數(shù)調(diào)用中 this 指向新創(chuàng)建的對(duì)象

構(gòu)造函數(shù)調(diào)用的上下文是新創(chuàng)建的對(duì)象嗤堰。它利用構(gòu)造函數(shù)的參數(shù)初始化新的對(duì)象,設(shè)定屬性的初始值咆爽,添加事件處理函數(shù)等等梁棠。

image

來看看下面示例中的上下文

    function Foo () {
      console.log(this instanceof Foo); // => true
      this.property = 'Default Value';
    }
    // Constructor invocation
    const fooInstance = new Foo();
    fooInstance.property; // => 'Default Value'

new Foo() 正在進(jìn)行構(gòu)造函數(shù)調(diào)用,其中上下文是fooInstance斗埂。 在Foo內(nèi)部初始化對(duì)象:this.property被賦值為默認(rèn)值符糊。

同樣的情況在用class語法(從ES6起)時(shí)也會(huì)發(fā)生,唯一的區(qū)別是初始化在constructor方法中進(jìn)行:

    class Bar {
      constructor() {
        console.log(this instanceof Bar); // => true
        this.property = 'Default Value';
      }
    }
    // Constructor invocation
    const barInstance = new Bar();
    barInstance.property; // => 'Default Value'

4.2. 陷阱: 忘了使用 new

有些JS函數(shù)不是只在作為構(gòu)造函數(shù)調(diào)用的時(shí)候才創(chuàng)建新的對(duì)象呛凶,作為函數(shù)調(diào)用時(shí)也會(huì)男娄,例如RegExp

    var reg1 = new RegExp('\\w+');
    var reg2 = RegExp('\\w+');
    
    reg1 instanceof RegExp;      // => true
    reg2 instanceof RegExp;      // => true
    reg1.source === reg2.source; // => true

當(dāng)執(zhí)行的 new RegExp('\\w+')RegExp('\\w+')時(shí),JS 會(huì)創(chuàng)建等價(jià)的正則表達(dá)式對(duì)象漾稀。

使用函數(shù)調(diào)用來創(chuàng)建對(duì)象存在一個(gè)潛在的問題(不包括工廠模式)模闲,因?yàn)橐恍?gòu)造函數(shù)可能會(huì)忽略在缺少new關(guān)鍵字時(shí)初始化對(duì)象的邏輯。

下面的例子說明了這個(gè)問題:

    function Vehicle(type, wheelsCount) {
      this.type = type;
      this.wheelsCount = wheelsCount;
      return this;
    }
    // 忘記使用 new 
    const car = Vehicle('Car', 4);
    car.type;       // => 'Car'
    car.wheelsCount // => 4
    car === window  // => true

Vehicle是一個(gè)在上下文對(duì)象上設(shè)置typewheelsCount屬性的函數(shù)崭捍。

當(dāng)執(zhí)行Vehicle('Car'尸折, 4)時(shí),返回一個(gè)對(duì)象Car殷蛇,它具有正確的屬性:Car.typeCarCar.wheelsCount4实夹,你可能認(rèn)為它很適合創(chuàng)建和初始化新對(duì)象。

然而粒梦,在函數(shù)調(diào)用中亮航,thiswindow對(duì)象 ,因此 Vehicle('Car'匀们,4)window 對(duì)象上設(shè)置屬性缴淋。 顯然這是錯(cuò)誤,它并沒有創(chuàng)建新對(duì)象泄朴。

當(dāng)你希望調(diào)用構(gòu)造函數(shù)時(shí)重抖,確保你使用了new操作符:

    function Vehicle(type, wheelsCount) {
      if (!(this instanceof Vehicle)) {
        throw Error('Error: Incorrect invocation');
      }
      this.type = type;
      this.wheelsCount = wheelsCount;
      return this;
    }
    // Constructor invocation
    const car = new Vehicle('Car', 4);
    car.type               // => 'Car'
    car.wheelsCount        // => 4
    car instanceof Vehicle // => true
    
    // Function invocation. Throws an error.
    const brokenCar = Vehicle('Broken Car', 3);

new Vehicle('Car',4) 運(yùn)行正常:創(chuàng)建并初始化一個(gè)新對(duì)象祖灰,因?yàn)闃?gòu)造函數(shù)調(diào)用中時(shí)使用了new關(guān)鍵字钟沛。

在構(gòu)造函數(shù)里添加了一個(gè)驗(yàn)證this instanceof Vehicle來確保執(zhí)行的上下文是正確的對(duì)象類型。如果this不是Vehicle夫植,那么就會(huì)報(bào)錯(cuò)讹剔。這樣油讯,如果執(zhí)行Vehicle('Broken Car', 3)(沒有new),我們會(huì)得到一個(gè)異常:Error: Incorrect invocation延欠。

給大家推薦一個(gè)好用的BUG監(jiān)控工具 Fundebug陌兑。

5. 隱式調(diào)用

使用myFun.call()myFun.apply()方法調(diào)用函數(shù)時(shí),執(zhí)行的是隱式調(diào)用由捎。

JS中的函數(shù)是第一類對(duì)象兔综,這意味著函數(shù)就是對(duì)象,對(duì)象的類型為Function狞玛。從函數(shù)對(duì)象的方法列表中软驰,.call().apply()用于調(diào)用具有可配置上下文的函數(shù)。

  • 方法 .call(thisArg[, arg1[, arg2[, ...]]])將接受的第一個(gè)參數(shù)thisArg作為調(diào)用時(shí)的上下文心肪,arg1, arg2, ...這些則作為參數(shù)傳入被調(diào)用的函數(shù)锭亏。
  • 方法.apply(thisArg, [args])將接受的第一個(gè)參數(shù)thisArg作為調(diào)用時(shí)的上下文,并且接受另一個(gè)類似數(shù)組的對(duì)象[arg1, arg2, ...]作為被調(diào)用函數(shù)的參數(shù)傳入硬鞍。

下面是隱式調(diào)用的例子

    function increment(number) {
      return ++number;  
    }
    increment.call(undefined, 10);    // => 11
    increment.apply(undefined, [10]); // => 11

increment.call()increment.apply()都用參數(shù)10調(diào)用了這個(gè)自增函數(shù)慧瘤。

兩者的區(qū)別是.call()接受一組參數(shù),例如myFunction.call(thisValue, 'value1', 'value2')固该。而.apply()接受的一組參數(shù)必須是一個(gè)類似數(shù)組的對(duì)象锅减,例如myFunction.apply(thisValue, ['value1', 'value2'])。

5.1. 隱式調(diào)用中的this

在隱式調(diào)用.call()或.apply()中伐坏,this是第一個(gè)參數(shù)

很明顯怔匣,在隱式調(diào)用中,this作為第一個(gè)參數(shù)傳遞給.call().apply()桦沉。

    var rabbit = { name: 'White Rabbit' };
    function concatName(string) {
      console.log(this === rabbit); // => true
      return string + this.name;
    }
    concatName.call(rabbit, 'Hello ');  // => 'Hello White Rabbit'
    concatName.apply(rabbit, ['Bye ']); // => 'Bye White Rabbit'

當(dāng)應(yīng)該使用特定上下文執(zhí)行函數(shù)時(shí)每瞒,隱式調(diào)用非常有用。例如為了解決方法調(diào)用時(shí)永部,this總是window或嚴(yán)格模式下的undefined的上下文問題独泞。隱式調(diào)用可以用于模擬在一個(gè)對(duì)象上調(diào)用某個(gè)方法呐矾。

    function Runner(name) {
      console.log(this instanceof Rabbit); // => true
      this.name = name;  
    }
    function Rabbit(name, countLegs) {
      console.log(this instanceof Rabbit); // => true
      Runner.call(this, name);
      this.countLegs = countLegs;
    }
    const myRabbit = new Rabbit('White Rabbit', 4);
    myRabbit; // { name: 'White Rabbit', countLegs: 4 }

Rabbit中的Runner.call(this, name)隱式調(diào)用了父類的函數(shù)來初始化這個(gè)對(duì)象苔埋。

6. 綁定函數(shù)

綁定函數(shù)是與對(duì)象連接的函數(shù)。通常使用.bind()方法從原始函數(shù)創(chuàng)建蜒犯。原始函數(shù)和綁定函數(shù)共享相同的代碼和作用域组橄,但執(zhí)行時(shí)上下文不同。

方法 myFunc.bind(thisArg[, arg1[, arg2[, ...]]])接受第一個(gè)參數(shù)thisArg作為綁定函數(shù)執(zhí)行時(shí)的上下文罚随,并且它接受一組可選的參數(shù) arg1, arg2, ...作為被調(diào)用函數(shù)的參數(shù)玉工。它返回一個(gè)綁定了thisArg的新函數(shù)。

    function multiply(number) {
      'use strict';
      return this * number;
    }
    const double = multiply.bind(2);
    
    double(3);  // => 6
    double(10); // => 20

bind(2)返回一個(gè)新的函數(shù)對(duì)象double淘菩,double 綁定了數(shù)字2遵班。multiplydouble具有相同的代碼和作用域屠升。

.apply().call() 方法相反,它不會(huì)立即調(diào)用該函數(shù)狭郑,.bind()方法只返回一個(gè)新函數(shù)腹暖,在之后被調(diào)用,只是this已經(jīng)被提前設(shè)置好了翰萨。

6.1. 綁定函數(shù)中的this

在調(diào)用綁定函數(shù)時(shí)脏答,this.bind()的第一個(gè)參數(shù)。

.bind()的作用是創(chuàng)建一個(gè)新函數(shù)亩鬼,調(diào)用該函數(shù)時(shí)殖告,將上下文作為傳遞給.bind()的第一個(gè)參數(shù)。它是一種強(qiáng)大的技術(shù)雳锋,使咱們可以創(chuàng)建一個(gè)定義了this值的函數(shù)黄绩。

image

來看看,如何在如何在綁定函數(shù)設(shè)置 this

    const numbers = {
      array: [3, 5, 10],
      getNumbers: function() {
        return this.array;    
      }
    };
    const boundGetNumbers = numbers.getNumbers.bind(numbers);
    boundGetNumbers(); // => [3, 5, 10]
    // Extract method from object
    const simpleGetNumbers = numbers.getNumbers;
    simpleGetNumbers(); // => undefined (嚴(yán)格模式下報(bào)錯(cuò))

numbers.getNumbers.bind(numbers)返回綁定numbers對(duì)象boundGetNumbers函數(shù)玷过。boundGetNumbers()調(diào)用時(shí)的thisnumber對(duì)象宝与,并能夠返回正確的數(shù)組對(duì)象。

可以將函數(shù)numbers.getNumbers提取到變量simpleGetNumbers中而不進(jìn)行綁定冶匹。在之后的函數(shù)調(diào)用中simpleGetNumbers()thiswindow(嚴(yán)格模式下為undefined)习劫,不是number對(duì)象。在這個(gè)情況下嚼隘,simpleGetNumbers()不會(huì)正確返回?cái)?shù)組诽里。

6.2 緊密的上下文綁定

.bind()創(chuàng)建一個(gè)永久的上下文鏈接,并始終保持它飞蛹。 一個(gè)綁定函數(shù)不能通過.call()或者.apply()來改變它的上下文谤狡,甚至是再次綁定也不會(huì)有什么作用。

只有綁定函數(shù)的構(gòu)造函數(shù)調(diào)用才能更改已經(jīng)綁定的上下文卧檐,但是很不推薦的做法(構(gòu)造函數(shù)調(diào)用必須使用常規(guī)的非綁定函數(shù))墓懂。

下面示例創(chuàng)建一個(gè)綁定函數(shù),然后嘗試更改其已預(yù)先定義好的上下文

    function getThis() {
      'use strict';
      return this;
    }
    const one = getThis.bind(1);
    // 綁定函數(shù)調(diào)用
    one(); // => 1
    // 使用帶有.apply()和.call()的綁定函數(shù)
    one.call(2);  // => 1
    one.apply(2); // => 1
    // 再次綁定
    one.bind(2)(); // => 1
    // 以構(gòu)造函數(shù)的形式調(diào)用綁定函數(shù)
    new one(); // => Object

只有new one()改變了綁定函數(shù)的上下文霉囚,其他方式的調(diào)用中this總是等于1捕仔。

7. 箭頭函數(shù)

箭頭函數(shù)用于以更短的形式聲明函數(shù),并在詞法上綁定上下文盈罐。它可以這樣使用

    const hello = (name) => {
      return 'Hello ' + name;
    };
    hello('World'); // => 'Hello World'
    // Keep only even numbers
    [1, 2, 5, 6].filter(item => item % 2 === 0); // => [2, 6]

箭頭函數(shù)語法簡(jiǎn)單榜跌,沒有冗長(zhǎng)的function 關(guān)鍵字。當(dāng)箭頭函數(shù)只有一條語句時(shí)盅粪,甚至可以省略return關(guān)鍵字钓葫。

箭頭函數(shù)是匿名的,這意味著name屬性是一個(gè)空字符串''票顾。這樣它就沒有詞法上函數(shù)名(函數(shù)名對(duì)于遞歸础浮、分離事件處理程序非常有用)

同時(shí)帆调,跟常規(guī)函數(shù)相反,它也不提供arguments對(duì)象豆同。但是贷帮,這在ES6中通過rest parameters修復(fù)了:

    const sumArguments = (...args) => {
       console.log(typeof arguments); // => 'undefined'
       return args.reduce((result, item) => result + item);
    };
    sumArguments.name      // => ''
    sumArguments(5, 5, 6); // => 16

7.1. 箭頭函數(shù)中的this

this 定義箭頭函數(shù)的封閉上下文

箭頭函數(shù)不會(huì)創(chuàng)建自己的執(zhí)行上下文,而是從定義它的外部函數(shù)中獲取 this诱告。 換句話說撵枢,箭頭函數(shù)在詞匯上綁定 this

image

下面的例子說明了這個(gè)上下文透明的特性:

    class Point {
      constructor(x, y) {
        this.x = x;
        this.y = y;
      }
      log() {
        console.log(this === myPoint); // => true
        setTimeout(()=> {
          console.log(this === myPoint);      // => true
          console.log(this.x + ':' + this.y); // => '95:165'
        }, 1000);
      }
    }
    const myPoint = new Point(95, 165);
    myPoint.log();

setTimeout使用與log()方法相同的上下文(myPoint對(duì)象)調(diào)用箭頭函數(shù)精居。正如所見锄禽,箭頭函數(shù)從定義它的函數(shù)繼承上下文。

如果在這個(gè)例子里嘗試用常規(guī)函數(shù),它創(chuàng)建自己的上下文(window或嚴(yán)格模式下的undefined)靴姿。因此沃但,要使相同的代碼正確地使用函數(shù)表達(dá)式,需要手動(dòng)綁定上下文:setTimeout(function(){…}.bind(this))佛吓。這很冗長(zhǎng)宵晚,使用箭頭函數(shù)是一種更簡(jiǎn)潔、更短的解決方案维雇。

如果箭頭函數(shù)在最頂層的作用域中定義(在任何函數(shù)之外)淤刃,則上下文始終是全局對(duì)象(瀏覽器中的 window):

    onst getContext = () => {
       console.log(this === window); // => true
       return this;
    };
    console.log(getContext() === window); // => true

箭頭函數(shù)一勞永逸地與詞匯上下文綁定。 即使修改上下文吱型,this也不能被改變:

    const numbers = [1, 2];
    (function() {  
      const get = () => {
        console.log(this === numbers); // => true
        return this;
      };
      console.log(this === numbers); // => true
      get(); // => [1, 2]
      // Use arrow function with .apply() and .call()
      get.call([0]);  // => [1, 2]
      get.apply([0]); // => [1, 2]
      // Bind
      get.bind([0])(); // => [1, 2]
    }).call(numbers);

無論如何調(diào)用箭頭函數(shù)get逸贾,它總是保留詞匯上下文numbers。 用其他上下文的隱式調(diào)用(通過 get.call([0])get.apply([0]))或者重新綁定(通過.bind())都不會(huì)起作用津滞。

箭頭函數(shù)不能用作構(gòu)造函數(shù)铝侵。 將它作為構(gòu)造函數(shù)調(diào)用(new get())會(huì)拋出一個(gè)錯(cuò)誤:TypeError: get is not a constructor

7.2. 陷阱: 用箭頭函數(shù)定義方法

你可能希望使用箭頭函數(shù)來聲明一個(gè)對(duì)象上的方法触徐。箭頭函數(shù)的定義相比于函數(shù)表達(dá)式短得多:(param) => {...} instead of function(param) {..}咪鲜。

來看看例子讼积,用箭頭函數(shù)在Period類上定義了format()方法:

    function Period (hours, minutes) {  
      this.hours = hours;
      this.minutes = minutes;
    }
    Period.prototype.format = () => {
      console.log(this === window); // => true
      return this.hours + ' hours and ' + this.minutes + ' minutes';
    };
    const walkPeriod = new Period(2, 30);  
    walkPeriod.format(); // => 'undefined hours and undefined minutes'

由于format是一個(gè)箭頭函數(shù)厘惦,并且在全局上下文(最頂層的作用域)中定義,因此 this 指向window對(duì)象炬太。

即使format作為方法在一個(gè)對(duì)象上被調(diào)用如walkPeriod.format()孔祸,window仍然是這次調(diào)用的上下文隆敢。之所以會(huì)這樣是因?yàn)榧^函數(shù)有靜態(tài)的上下文发皿,并不會(huì)隨著調(diào)用方式的改變而改變崔慧。

該方法返回'undefined hours和undefined minutes',這不是咱們想要的結(jié)果穴墅。

函數(shù)表達(dá)式解決了這個(gè)問題惶室,因?yàn)槌R?guī)函數(shù)確實(shí)能根據(jù)實(shí)際調(diào)用改變它的上下文:

    function Period (hours, minutes) {  
      this.hours = hours;
      this.minutes = minutes;
    }
    Period.prototype.format = function() {
      console.log(this === walkPeriod); // => true
      return this.hours + ' hours and ' + this.minutes + ' minutes';
    };
    const walkPeriod = new Period(2, 30);  
    walkPeriod.format(); // => '2 hours and 30 minutes'

walkPeriod.format()是一個(gè)對(duì)象上的方法調(diào)用温自,它的上下文是walkPeriod對(duì)象。this.hours等于2皇钞,this.minutes等于30悼泌,所以這個(gè)方法返回了正確的結(jié)果:'2 hours and 30 minutes'

原文:https://dmitripavlutin.com/gentle-explanation-of-this-in-javascript/

代碼部署后可能存在的BUG沒法實(shí)時(shí)知道夹界,事后為了解決這些BUG馆里,花了大量的時(shí)間進(jìn)行l(wèi)og 調(diào)試,這邊順便給大家推薦一個(gè)好用的BUG監(jiān)控工具 Fundebug可柿。

總結(jié)

為函數(shù)調(diào)用對(duì)this影響最大鸠踪,從現(xiàn)在開始不要問自己:

this 是從哪里來的?

而是要看看

函數(shù)是怎么被調(diào)用的复斥?

對(duì)于箭頭函數(shù)营密,需要想想

在這個(gè)箭頭函數(shù)被定義的地方,this是什么目锭?

這是處理this時(shí)的正確想法评汰,它們可以讓你免于頭痛。

關(guān)于Fundebug

Fundebug專注于JavaScript痢虹、微信小程序被去、微信小游戲、支付寶小程序奖唯、React Native编振、Node.js和Java線上應(yīng)用實(shí)時(shí)BUG監(jiān)控。 自從2016年雙十一正式上線臭埋,F(xiàn)undebug累計(jì)處理了20億+錯(cuò)誤事件踪央,付費(fèi)客戶有陽光保險(xiǎn)、核桃編程瓢阴、荔枝FM畅蹂、掌門1對(duì)1、微脈荣恐、青團(tuán)社等眾多品牌企業(yè)液斜。歡迎大家免費(fèi)試用

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末叠穆,一起剝皮案震驚了整個(gè)濱河市少漆,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌硼被,老刑警劉巖示损,帶你破解...
    沈念sama閱讀 210,914評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異嚷硫,居然都是意外死亡检访,警方通過查閱死者的電腦和手機(jī)始鱼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評(píng)論 2 383
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來脆贵,“玉大人医清,你說我怎么就攤上這事÷舭保” “怎么了会烙?”我有些...
    開封第一講書人閱讀 156,531評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)筒捺。 經(jīng)常有香客問我持搜,道長(zhǎng),這世上最難降的妖魔是什么焙矛? 我笑而不...
    開封第一講書人閱讀 56,309評(píng)論 1 282
  • 正文 為了忘掉前任葫盼,我火速辦了婚禮,結(jié)果婚禮上村斟,老公的妹妹穿的比我還像新娘贫导。我一直安慰自己,他們只是感情好蟆盹,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,381評(píng)論 5 384
  • 文/花漫 我一把揭開白布孩灯。 她就那樣靜靜地躺著,像睡著了一般逾滥。 火紅的嫁衣襯著肌膚如雪峰档。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,730評(píng)論 1 289
  • 那天寨昙,我揣著相機(jī)與錄音讥巡,去河邊找鬼。 笑死舔哪,一個(gè)胖子當(dāng)著我的面吹牛欢顷,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播捉蚤,決...
    沈念sama閱讀 38,882評(píng)論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼抬驴,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了缆巧?” 一聲冷哼從身側(cè)響起布持,我...
    開封第一講書人閱讀 37,643評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎陕悬,沒想到半個(gè)月后题暖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,095評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,448評(píng)論 2 325
  • 正文 我和宋清朗相戀三年芙委,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了逞敷。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片狂秦。...
    茶點(diǎn)故事閱讀 38,566評(píng)論 1 339
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡灌侣,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出裂问,到底是詐尸還是另有隱情侧啼,我是刑警寧澤,帶...
    沈念sama閱讀 34,253評(píng)論 4 328
  • 正文 年R本政府宣布堪簿,位于F島的核電站痊乾,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏椭更。R本人自食惡果不足惜哪审,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,829評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望虑瀑。 院中可真熱鬧湿滓,春花似錦、人聲如沸舌狗。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽痛侍。三九已至朝氓,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間主届,已是汗流浹背赵哲。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評(píng)論 1 264
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留君丁,地道東北人誓竿。 一個(gè)月前我還...
    沈念sama閱讀 46,248評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像谈截,于是被迫代替她去往敵國(guó)和親筷屡。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,440評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • 1.概念 在JavaScript中簸喂,this 是指當(dāng)前函數(shù)中正在執(zhí)行的上下文環(huán)境毙死,因?yàn)檫@門語言擁有四種不同的函數(shù)調(diào)...
    BluesCurry閱讀 1,127評(píng)論 0 2
  • 1. this之謎 在JavaScript中,this是當(dāng)前執(zhí)行函數(shù)的上下文喻鳄。因?yàn)镴avaScript有4種不同的...
    百里少龍閱讀 990評(píng)論 0 3
  • 函數(shù)和對(duì)象 1扼倘、函數(shù) 1.1 函數(shù)概述 函數(shù)對(duì)于任何一門語言來說都是核心的概念。通過函數(shù)可以封裝任意多條語句,而且...
    道無虛閱讀 4,543評(píng)論 0 5
  • 關(guān)于 this this 關(guān)鍵字是 JavaScript 中最復(fù)雜的機(jī)制之一再菊。它是一個(gè)很特別的關(guān)鍵字爪喘,被自動(dòng)定義在...
    游學(xué)者灬墨槿閱讀 571評(píng)論 1 2
  • 在javaScript系列 [01]-javaScript函數(shù)基礎(chǔ)這篇文章中我已經(jīng)簡(jiǎn)單介紹了JavaScript語...
    文頂頂閱讀 918評(píng)論 0 4