JavaScript中‘this’理解

1. this之謎

  • 在JavaScript中,this是當(dāng)前執(zhí)行函數(shù)的上下文。因?yàn)镴avaScript有4種不同的函數(shù)調(diào)用方式:

  • 函數(shù)調(diào)用: alert('Hello World!')

  • 方法調(diào)用: console.log('Hello World!')

  • 構(gòu)造函數(shù)調(diào)用: new RegExp('\d')

  • 隱式調(diào)用: alert.call(undefined, 'Hello World!')

  • 并且每種方法都定義了自己的上下文,this會(huì)表現(xiàn)得跟程序員預(yù)期的不太一樣。同時(shí),strict模式也會(huì)影響函數(shù)執(zhí)行時(shí)的上下文。

理解this的關(guān)鍵點(diǎn)就是要對(duì)函數(shù)調(diào)用以及它如何影響上下文有個(gè)清晰的觀點(diǎn)。這篇文章將會(huì)著重于對(duì)函數(shù)調(diào)用的解釋、函數(shù)調(diào)用如何影響this以及展示確定上下文時(shí)常見的陷阱。

在開始之前,讓我們來熟悉一些術(shù)語(yǔ):

函數(shù)調(diào)用 指執(zhí)行構(gòu)成一個(gè)函數(shù)的代碼(簡(jiǎn)單說就是call一個(gè)函數(shù))例如 parseInt('15')是parseInt函數(shù)調(diào)用.

函數(shù)調(diào)用的上下文 指this在函數(shù)體中的值。

函數(shù)的作用域 指的是在函數(shù)體內(nèi)可以使用的變量、對(duì)象以及函數(shù)的集合。

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

當(dāng)一個(gè)表達(dá)式為函數(shù)接著一個(gè)(,一些用逗號(hào)分隔的參數(shù)以及一個(gè))時(shí),函數(shù)調(diào)用被執(zhí)行。這個(gè)表達(dá)式不能是屬性訪問,如myObject.myFunction,因?yàn)檫@會(huì)變成一個(gè)方法調(diào)用。

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

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

全局對(duì)象是由執(zhí)行的環(huán)境決定的。在瀏覽器里它是window對(duì)象。

在函數(shù)調(diào)用里,函數(shù)執(zhí)行的上下文是全局對(duì)象。讓我們一起看看下面函數(shù)里的上下文:

function sum(a, b) {
   console.log(this === window); // => true
   this.myNumber = 20; // add 'myNumber' property to global object
   return a + b;
}
// sum() is invoked as a function
// this in sum() is a global object (window)
sum(15, 16);     // => 31
window.myNumber; // => 20

當(dāng)this在所有函數(shù)作用域以外(最上層的作用域:全局執(zhí)行的上下文)調(diào)用時(shí),它也指向全局對(duì)象:

2.2. 函數(shù)調(diào)用中的this, strict模式

==strict模式下,函數(shù)調(diào)用中的this是undefined==

  • strict模式在ECMAScript -5.1中被引入,它是一個(gè)受限制的JavaScript變種,提供了更好的安全性以及錯(cuò)誤檢查。為了使用它,把'use strict'放在函數(shù)體的開始。這個(gè)模式會(huì)影響執(zhí)行的上下文,把this變成undefined。
    在strict模式下執(zhí)行函數(shù)的例子:
function multiply(a, b) {
    'use strict'; // enable the strict mode
    console.log(this === undefined); // => true
    return a * b;
}
// multiply() function invocation with strict mode enabled
// this in multiply() is undefined
multiply(2, 5); // => 10
  • strict模式不僅在當(dāng)前作用域起作用,也會(huì)對(duì)內(nèi)部的作用域起作用(對(duì)所有在內(nèi)部定義的函數(shù)有效):
function execute() {
    'use strict'; // activate the strict mode
    function concat(str1, str2) {
        // the strict mode is enabled too
        console.log(this === undefined); // => true
        return str1 + str2;
    }
    // concat() is invoked as a function in strict mode
    // this in concat() is undefined
    concat('Hello', ' World!'); // => "Hello World!"
}
execute();
  • 單個(gè)的JavaScript文件可能既包含strict模式又包含非strict模式。所以,在單個(gè)的腳本內(nèi),同樣的調(diào)用方法可能有不同的上下文行為:
function nonStrictSum(a, b) {
    // non-strict mode
    console.log(this === window); // => true
    return a + b;
}
function strictSum(a, b) {
    'use strict';
    // strict mode is enabled
    console.log(this === undefined); // => true
    return a + b;
}
// nonStrictSum() is invoked as a function in non-strict mode
// this in nonStrictSum() is the window object
nonStrictSum(5, 6); // => 11
// strictSum() is invoked as a function in strict mode
// this in strictSum() is undefined
strictSum(8, 12); // => 20

2.3. 陷阱: 內(nèi)部函數(shù)中的this

  • 一個(gè)函數(shù)調(diào)用中的常見錯(cuò)誤就是以為this在內(nèi)部函數(shù)中跟在外部函數(shù)中一樣。 正確來說,內(nèi)部函數(shù)的上下文依賴于調(diào)用方法,而不是外部函數(shù)的上下文。 為了能使this跟預(yù)期的一樣,用隱式調(diào)用來修改內(nèi)部函數(shù)的上下文(用.call()或者.apply(), 如5.所示)或者創(chuàng)建一個(gè)綁定函數(shù)(用.bind(), 如6.所示)。
var numbers = {
   numberA: 5,
   numberB: 10,
   sum: function() {
     console.log(this === numbers); // => true
     function calculate() {
       // this is window or undefined in strict mode
       console.log(this === numbers); // => false
       return this.numberA + this.numberB;
     }
     return calculate();
   }
};
numbers.sum(); // => NaN or throws TypeError in strict mode

numbers.sum()是一個(gè)對(duì)象上的方法調(diào)用 (見3.),所以sum中的上下文是numbers對(duì)象。calculate函數(shù)定義在sum內(nèi)部,所以你會(huì)指望calculate()中的this也是numbers對(duì)象。然而,calculate()是一個(gè)函數(shù)調(diào)用(而不是方法調(diào)用),它的this是全局對(duì)象window(例子2.1.)或者strict模式下的undefined(例子2.2.)。即使外部函數(shù)sum的上下文是numbers對(duì)象,它在這里也沒有影響。numbers.sum()的調(diào)用結(jié)果是NaN或者strict模式下的TypeError: Cannot read property 'numberA' of undefined錯(cuò)誤。因?yàn)閏alculate沒有被正確調(diào)用,結(jié)果絕不是預(yù)期的5 + 10 = 15。

  • 為了解決這個(gè)問題,calculate應(yīng)該跟sum有一樣的上下文,以便于使用numberA和numberB。解決方法之一是使用.call()方法(見章節(jié)5.):
var numbers = {
   numberA: 5,
   numberB: 10,
   sum: function() {
     console.log(this === numbers); // => true
     function calculate() {
       console.log(this === numbers); // => true
       return this.numberA + this.numberB;
     }
     // use .call() method to modify the context
     return calculate.call(this);
   }
};
numbers.sum(); // => 15

calculate.call(this)像往常一樣執(zhí)行calculate,但是上下文由第一個(gè)參數(shù)指定。現(xiàn)在this.numberA + this.numberB相當(dāng)于numbers.numberA + numbers.numberB,函數(shù)會(huì)返回預(yù)期的結(jié)果5 + 10 = 15。

3. 方法調(diào)用

一個(gè)方法是作為一個(gè)對(duì)象的屬性存儲(chǔ)的函數(shù)。例如:

var myObject = {
    // helloFunction is a method
    helloFunction: function() {
        return 'Hello World!';
    }
};
var message = myObject.helloFunction();

helloFunctio是myObject的一個(gè)方法。為了使用這個(gè)方法, 使用屬性訪問: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)用(見2.)跟方法調(diào)用是很重要的,因?yàn)樗麄兺耆煌?。他們最主要的區(qū)別在于==方法調(diào)用要求函數(shù)以屬性訪問的形式調(diào)用==(如<expression>.functionProperty()或者<expression>'functionProperty'),而函數(shù)調(diào)用并沒有這樣的要求(如<expression>())。

['Hello', 'World'].join(', '); // method invocation
({ ten: function() { return 10; } }).ten(); // method invocation
var obj = {};
obj.myFunction = function() {
  return new Date().toString();
};
obj.myFunction(); // method invocation

var otherFunction = obj.myFunction;
otherFunction();     // function invocation
parseFloat('16.60'); // function invocation
isNaN(0);            // function invocation

3.1. 方法調(diào)用中的this

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

當(dāng)調(diào)用一個(gè)對(duì)象上的方法時(shí),this變成這個(gè)對(duì)象自身。 讓我們一起來創(chuàng)建一個(gè)對(duì)象,它帶有一個(gè)可以增大數(shù)字的方法:

var 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()會(huì)把increment函數(shù)的上下文變成calc對(duì)象。所以,用this.num來增加num這個(gè)屬性跟預(yù)期一樣工作。

  • javaScript對(duì)象會(huì)從它的prototype繼承方法。當(dāng)這個(gè)繼承的方法在新的對(duì)象上被調(diào)用時(shí),上下文仍然是該對(duì)象本身:
var myDog = Object.create({
  sayName: function() {
     console.log(this === myDog); // => true
     return this.name;
  }
});
myDog.name = 'Milo';
// method invocation. this is myDog
myDog.sayName(); // => 'Milo'

Object.create()創(chuàng)建了一個(gè)新的對(duì)象myDog,并且設(shè)置了它的prototype。myDog繼承了sayName方法。當(dāng)執(zhí)行myDog.sayName()時(shí),myDog是調(diào)用的上下文。

  • 在ECMAScript 6的class語(yǔ)法中,方法調(diào)用的上下文也是這個(gè)實(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. 陷阱:從object中分離方法

  • 一個(gè)對(duì)象中的方法可以賦值給另一個(gè)變量。當(dāng)用這個(gè)變量調(diào)用方法時(shí),你可能以為this指向定義這個(gè)方法的對(duì)象。

正確來說如果這個(gè)方法在沒有對(duì)象的時(shí)候被調(diào)用,它會(huì)變成函數(shù)調(diào)用:this變成全局對(duì)象window或者strict模式下的undefined(見2.1和2.2)。 用綁定函數(shù)(用.bind(), 見6.)可以修正上下文,使它變成擁有這個(gè)方法的對(duì)象。

  • 下面的例子創(chuàng)建了Animal構(gòu)造函數(shù)并創(chuàng)造了它的一個(gè)實(shí)例 - myCat。 setTimout()會(huì)在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');
    }
}
var myCat = new Animal('Cat', 4);
// logs "The undefined has undefined legs"
// or throws a TypeError in strict mode
setTimeout(myCat.logInfo, 1000);
  • 你可能會(huì)以為setTimout會(huì)調(diào)用myCat.logInfo(),輸出關(guān)于myCat對(duì)象的信息。實(shí)際上,這個(gè)方法在作為參數(shù)傳遞給setTimout(myCat.logInfo)時(shí)已經(jīng)從原對(duì)象上分離了,1秒鐘之后發(fā)生的是一個(gè)函數(shù)調(diào)用。當(dāng)logInfo作為函數(shù)被調(diào)用時(shí),this是全局對(duì)象,或者strict模式下的undefined(反正不是myCat對(duì)象),所以不會(huì)正確地輸出信息。

  • 函數(shù)可以通過.bind()方法跟一個(gè)對(duì)象綁定(見6.)。如果這個(gè)分離的方法與myCat綁定,那么上下文的問題就解決了:

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');
    };
}
var myCat = new Animal('Cat', 4);
// logs "The Cat has 4 legs"
setTimeout(myCat.logInfo.bind(myCat), 1000);
  • yCat.logInfo.bind(myCat)返回一個(gè)跟logInfo執(zhí)行效果一樣的函數(shù),但是它的this即使在函數(shù)調(diào)用情況下也是myCat。

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è)例子聲明了一個(gè)Country函數(shù),并且將它作為一個(gè)構(gòu)造函數(shù)調(diào)用:
function Country(name, traveled) {
    this.name = name ? name : 'United Kingdom';
    this.traveled = Boolean(traveled); // transform to a boolean
}
Country.prototype.travel = function() {
    this.traveled = true;
};
// Constructor invocation
var france = new Country('France', false);
// Constructor invocation
var 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

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

class City {
  constructor(name, traveled) {
    this.name = name;
    this.traveled = false;
  }
  travel() {
    this.traveled = true;
  }
}
// Constructor invocation
var 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í),JavaScript會(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í)間處理函數(shù)等等。
function Foo () {
    console.log(this instanceof Foo); // => true
    this.property = 'Default Value';
}
// Constructor invocation
var fooInstance = new Foo();
fooInstance.property; // => 'Default Value'

new Foo()正在調(diào)用一個(gè)構(gòu)造函數(shù),它的上下文是fooInstance。其中,F(xiàn)oo被初始化了:this.property被賦予了一個(gè)默認(rèn)值。

  • 同樣的情況在用class語(yǔ)法(從ES6起)時(shí)也會(huì)發(fā)生,唯一的區(qū)別是初始化在constructor方法中進(jìn)行:
class Bar {
    constructor() {
        console.log(this instanceof Bar); // => true
        this.property = 'Default Value';
    }
}
// Constructor invocation
var barInstance = new Bar();
barInstance.property; // => 'Default Value'
  • 當(dāng)new Bar()執(zhí)行時(shí),JavaScript創(chuàng)建了一個(gè)空的對(duì)象,把它作為constructor方法的上下文?,F(xiàn)在,你可以用this關(guān)鍵詞給它添加屬性:this.property = 'Default Value'。

4.2. 陷阱: 忘了new

有些JavaScirpt函數(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í),JavaScrit會(huì)創(chuàng)建相同的正則表達(dá)式對(duì)象。

  • 因?yàn)橛行?gòu)造函數(shù)在new關(guān)鍵詞缺失的情況下,可能跳過對(duì)象初始化,用函數(shù)調(diào)用創(chuàng)建對(duì)象會(huì)存在問題(不包括工廠模式)。 下面的例子就說明了這個(gè)問題:
function Vehicle(type, wheelsCount) {
    this.type = type;
    this.wheelsCount = wheelsCount;
    return this;
}
// Function invocation
var car = Vehicle('Car', 4);
car.type;       // => 'Car'
car.wheelsCount // => 4
car === window  // => true

Vehicle是一個(gè)在上下文上設(shè)置了type跟wheelsCount屬性的函數(shù)。當(dāng)執(zhí)行Vehicle('Car', 4)時(shí),返回了一個(gè)car對(duì)象,它的屬性是正確的:car.type是'Car', car.wheelsCount是4。你可能以為它正確地創(chuàng)建并初始化了對(duì)象。 然而,在函數(shù)調(diào)用中,this是window對(duì)象(見2.1.),Vehicle('Car', 4)實(shí)際上是在給window對(duì)象設(shè)置屬性--這是錯(cuò)的。它并沒有創(chuàng)建一個(gè)新的對(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
var car = new Vehicle('Car', 4);
car.type               // => 'Car'
car.wheelsCount        // => 4
car instanceof Vehicle // => true

// Function invocation. Generates an error.
var brokenCat = Vehicle('Broken Car', 3);

new Vehicle('Car', 4)工作正常:因?yàn)閚ew關(guān)鍵詞出現(xiàn)在構(gòu)造函數(shù)調(diào)用前,一個(gè)新的對(duì)象被創(chuàng)建并初始化。 在構(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。

5. 隱式調(diào)用

當(dāng)函數(shù)被.call()或者.apply()調(diào)用時(shí),執(zhí)行的是隱式調(diào)用。

  • 函數(shù)在JavaScript中是第一類對(duì)象,這意味著函數(shù)也是對(duì)象。它的類型是Function。根據(jù)這個(gè)函數(shù)對(duì)象所擁有的方法列表,.call()和.apply()可以跟一個(gè)可變的上下文一起調(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ì)象==[args]作為被調(diào)用函數(shù)的參數(shù)傳入。

  • 下面是一個(gè)隱式調(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是傳入.call()或.apply()中的第一個(gè)參數(shù)。下面的這個(gè)例子就說明了這一點(diǎn):
var rabbit = { name: 'White Rabbit' };
function concatName(string) {
    console.log(this === rabbit); // => true
    return string + this.name;
}
// Indirect invocations
concatName.call(rabbit, 'Hello ');  // => 'Hello White Rabbit'
concatName.apply(rabbit, ['Bye ']); // => 'Bye White Rabbit'
  • 當(dāng)一個(gè)函數(shù)應(yīng)該在特定的上下文中執(zhí)行時(shí),隱式調(diào)用就非常有用。例如為了解決方法調(diào)用時(shí),this總是window或strict模式下的undefined的上下文問題(見2.3.)。隱式調(diào)用可以用于模擬在一個(gè)對(duì)象上調(diào)用某個(gè)方法(見之前的代碼樣例)。

另一個(gè)實(shí)際的例子是在ES5中,在創(chuàng)建的類的結(jié)構(gòu)層次中中,調(diào)用父類的構(gòu)造函數(shù):

function Runner(name) {
    console.log(this instanceof Rabbit); // => true
    this.name = name;
}
function Rabbit(name, countLegs) {
    console.log(this instanceof Rabbit); // => true
    // Indirect invocation. Call parent constructor.
    Runner.call(this, name);
    this.countLegs = countLegs;
}
var 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ù)是一個(gè)與對(duì)象綁定的函數(shù)。通常它是通過在原函數(shù)上使用 .bind()來創(chuàng)建的。原函數(shù)和綁定的函數(shù)共享代碼跟作用域,但是在執(zhí)行時(shí)有不同的上下文。

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

  • 下面的代碼創(chuàng)建了一個(gè)綁定函數(shù)并在之后調(diào)用它:
function multiply(number) {
    'use strict';
    return this * number;
}
// create a bound function with context
var double = multiply.bind(2);
// invoke the bound function
double(3);  // => 6
double(10); // => 20

multiply.bind(2)返回了一個(gè)新的函數(shù)對(duì)象double,double綁定了數(shù)字2。multiply跟double有相同的代碼跟作用域。

跟.apply()以及.call()方法(見5.)馬上調(diào)用函數(shù)不同,.bind()函數(shù)返回一個(gè)新的方法,它應(yīng)該在之后被調(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í)的上下文是傳入.bind()的第一個(gè)參數(shù)。它是一種非常強(qiáng)大的技巧,使你可以創(chuàng)建一個(gè)定義了this值的函數(shù)。

  • 讓我們來看看如何在一個(gè)綁定函數(shù)中設(shè)置this:

var numbers = {
    array: [3, 5, 10],
    getNumbers: function() {
        return this.array;
    }
};
// Create a bound function
var boundGetNumbers = numbers.getNumbers.bind(numbers);
boundGetNumbers(); // => [3, 5, 10]
// Extract method from object
var simpleGetNumbers = numbers.getNumbers;
simpleGetNumbers(); // => undefined or throws an error in strict mode

numbers.getNumbers.bind(numbers)返回了一個(gè)綁定了number對(duì)象的boundGetNumbers函數(shù)。boundGetNumbers()調(diào)用時(shí)的this是number對(duì)象,并能夠返回正確的數(shù)組對(duì)象。numbers.getNumbers函數(shù)能在不綁定的情況下賦值給變量simpleGetNumbers。在之后的函數(shù)調(diào)用中,simpleGetNumbers()的this是window或者strict模式下的undefined,不是number對(duì)象(見3.2. 陷阱)。在這個(gè)情況下,simpleGetNumbers()不會(huì)正確返回?cái)?shù)組。

==.bind()永久性地建立了一個(gè)上下文的鏈接,并且會(huì)一直保持它.== 一個(gè)綁定函數(shù)不能通過.call()或者.apply()來改變它的上下文,甚至是再次綁定也不會(huì)有什么作用。 只有用綁定函數(shù)的構(gòu)造函數(shù)調(diào)用方法能夠改變上下文,但并不推薦這個(gè)方法(因?yàn)闃?gòu)造函數(shù)調(diào)用用的是常規(guī)函數(shù)而不是綁定函數(shù))。 下面的例子聲明了一個(gè)綁定函數(shù),接著試圖改變它預(yù)先定義好的上下文:

function getThis() {
    'use strict';
    return this;
}
var one = getThis.bind(1);
// Bound function invocation
one(); // => 1
// Use bound function with .apply() and .call()
one.call(2);  // => 1
one.apply(2); // => 1
// Bind again
one.bind(2)(); // => 1
// Call the bound function as a constructor
new one(); // => Object
只有new one()改變了綁定函數(shù)的上下文,其他方式的調(diào)用中this總是等于1。

7. 箭頭函數(shù)

箭頭函數(shù)被設(shè)計(jì)來以更簡(jiǎn)短的形式定義函數(shù)。并且能從詞法上綁定上下文。它能以下面的方式被使用:

  • 箭頭函數(shù)帶來了更輕量的語(yǔ)法,避免了冗長(zhǎng)的function關(guān)鍵詞。你甚至可以在函數(shù)只有一個(gè)語(yǔ)句的時(shí)候省略return。

  • 因?yàn)榧^函數(shù)是匿名的,這意味著它的name屬性是個(gè)空字符串''。這樣一來,它就沒有一個(gè)詞法上的函數(shù)名(函數(shù)名在遞歸跟事件解綁時(shí)會(huì)比較有用)。同時(shí),跟常規(guī)函數(shù)相反,它也不提供arguments對(duì)象。但是,這在ES6中通過rest parameters修復(fù)了:

var 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í)封裝好的上下文

  • 箭頭函數(shù)并不會(huì)創(chuàng)建它自己的上下文,它從它定義處的外部函數(shù)獲得this上下文。下面的例子說明了這個(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);
    }
}
var myPoint = new Point(95, 165);
myPoint.log();

setTimeout在調(diào)用箭頭函數(shù)時(shí)跟log()使用了相同的上下文(myPoint對(duì)象)。正如所見,箭頭函數(shù)從它定義處“繼承”了函數(shù)的上下文。 如果在這個(gè)例子里嘗試用常規(guī)函數(shù),它會(huì)建立自己的上下文(window或strict模式下的undefined)。所以,為了讓同樣的代碼能在函數(shù)表達(dá)式中正確運(yùn)行,需要手動(dòng)綁定上下文:setTimeout(function() {...}.bind(this))。這樣一來就顯得很啰嗦,不如用箭頭函數(shù)來得簡(jiǎn)短。

  • 如果箭頭函數(shù)定義在最上層的作用域(在所有函數(shù)之外),那么上下文就總是全局對(duì)象(瀏覽器中的window對(duì)象):
var getContext = () => {
   console.log(this === window); // => true
   return this;
};
console.log(getContext() === window); // => true
  • 箭頭函數(shù)會(huì)一勞永逸地綁定詞法作用域。即使使用修改上下文的方法,this也不能被改變:
var numbers = [1, 2];
(function() {
    var 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);

一個(gè)函數(shù)表達(dá)式通過.call(numbers)被隱式調(diào)用了,這使得這個(gè)調(diào)用的this變成了numbers。這樣一來,箭頭函數(shù)get的this也變成了numbers,因?yàn)樗菑脑~法上獲得的上下文。

  • 無論get是怎么被調(diào)用的,它一直保持了一開始的上下文numbers。用其他上下文的隱式調(diào)用(通過.call()或.apply())或者重新綁定(通過.bind())都不會(huì)起作用

  • 箭頭函數(shù)不能用作構(gòu)造函數(shù)。如果像構(gòu)造函數(shù)一樣調(diào)用new get(), JavaScript會(huì)拋出異常:TypeError: get is not a constructor。

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

  • 你可能想用箭頭函數(shù)在一個(gè)對(duì)象上定義方法。這很合情合理:箭頭函數(shù)的定義相比于函數(shù)表達(dá)式短得多:例如(param) => {...}而不是function(param) {..}。

  • 這個(gè)例子用箭頭函數(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';
};
var 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)用方式的改變而改變。

  • 函數(shù)表達(dá)式可以解決這個(gè)問題,因?yàn)橐粋€(gè)常規(guī)的函數(shù)會(huì)隨著調(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';
};
var walkPeriod = new Period(2, 30);
walkPeriod.format(); // => '2 hours and 30 minutes'

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

8. 結(jié)論

  • 因?yàn)楹瘮?shù)調(diào)用對(duì)this有最大的影響,從現(xiàn)在起,不要再問你自己:

this是從哪里來的?

  • 而要問自己:

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

  • 對(duì)于箭頭函數(shù),問問你自己:

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Javascript this 在JavaScript中, this 是當(dāng)前執(zhí)行函數(shù)的上下文。 JavaScri...
    Cause_XL閱讀 464評(píng)論 0 1
  • 新手在入門 JavaScript 的過程中,一定會(huì)踩很多關(guān)于 this 的坑,出現(xiàn)問題的本質(zhì)就是 this 指針的...
    一縷殤流化隱半邊冰霜閱讀 3,910評(píng)論 15 47
  • 官方中文版原文鏈接 感謝社區(qū)中各位的大力支持,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券,享受所有官網(wǎng)優(yōu)惠,并抽取幸運(yùn)大...
    HetfieldJoe閱讀 7,001評(píng)論 15 54
  • 一 今年春節(jié),接到一個(gè)陌生電話。拿起來才知道,居然是已有8年多沒聯(lián)系的朋友。他開口第一句就是:“沒想到啊,都8年了...
    危笑天閱讀 1,177評(píng)論 6 2
  • 你陪我童年 我伴你老年 不高的肩膀,佝僂的背,一個(gè)腿的拐棍,老式的手電筒,陪伴著我的童年,小時(shí)候是你溫暖我冰涼的手...
    半糖cf閱讀 221評(píng)論 0 0

友情鏈接更多精彩內(nèi)容