原文:Gentle explanation of ‘this’ keyword in JavaScript 链接:https://dmitripavlutin.com/gentle-explanation-of-this-in-javascript/ 翻译:Robin

1.神秘的 this

很久以来,this 对于我和一些初级开发者来说都很神秘。它的特性强大,需要努力去理解。

对于拥有 Java、PHP及其他标准语言背景的人来说,this 被看作是类方法中的当前对象的实例:不多不少。通常,它不会在方法外使用,这样也不会造成一些困惑。

在 JavaScript 中,情况会有所不同:this 是一个函数当前的执行上下文。JavaScript 有 4 种函数调用类型:

  • 函数调用:alert(‘Hello World!’)
  • 方法调用:console.log(‘Hello World!’)
  • 构造器调用:new RegExp(‘\d’)
  • 间接调用:alert.call(undefined, ‘Hello World!’)

每种调用方式决定了上下文,因此 this 会和开发者预想的有所出入。

深入浅出 JavaScript  'this' 关键字 - 图1

此外,严格模式 也会影响执行上下文。

理解 this 关键字的重点是对函数调用以及上下文影响要有清晰的认识。

本文聚焦于调用分析,函数调用对于 this 的影响以及判断上下文的一些常见陷阱解释。

在这之前,让我们熟悉几个术语:

  • 函数 调用,函数体的代码执行,或者说简单的调用函数。类如 parseInt 函数的调用 parseInt(‘15’)
  • 调用的 上下文 就是函数体内的 this 值。例如 map.set(‘key’, ‘value’) 的调用的上下文就是 map
  • 函数 作用域 就是函数体内可访问的变量、对象和函数集合。

目录:

  1. 神秘的 this
  2. 函数调用
    1. 函数调用的 this
    2. 严格模式下函数调用的 this
    3. 陷阱:内部函数的 this
  3. 方法调用
    1. 方法调用的 this
    2. 陷阱:方法与对象分离
  4. 构造器调用
    1. 构造器调用的 this
    2. 陷阱:忘记关键字 new
  5. 间接调用
    1. 间接调用的 this
  6. 绑定函数
    1. 绑定函数的 this
    2. 紧上下文绑定
  7. 箭头函数
    1. 箭头函数的 this
    2. 陷阱:使用箭头函数定义方法
  8. 总结

2.函数调用

当一个表达式赋值了一个函数对象,后面跟着圆括号 ,被分号隔开的参数以及一个圆括号 ),函数调用就被执行。例如 parseInt(‘18’)

函数调用表达式不是 obj.myFunc() 这样的一个 属性访问器,后者会产生 方法调用。记住这个区别十分重要。

函数调用的简单例子:

  1. function hello(name) {
  2. return 'Hello ' + name + '!';
  3. }
  4. // Function invocation
  5. var message = hello('World');
  6. console.log(message); // => 'Hello World!'

hello(‘World’) 是一个函数调用:hello 表达式被赋值一个函数对象,紧接着一对圆括号和 ‘World’ 参数。

更高级的例子是 IIFE (立即执行函数表达式)。

  1. var message = (function(name) {
  2. return 'Hello ' + name + '!';
  3. })('World');
  4. console.log(message) // => 'Hello World!'

IIFE 也是一个函数调用:(function(name) {…}) 的第一对圆括号是一个函数对象赋值表达式,紧跟着包着 ‘World’ 参数的圆括号。

2.1. 函数调用的 this

函数调用的 this全局对象

全局对象由执行环境决定。在浏览器中是 window 对象。

深入浅出 JavaScript  'this' 关键字 - 图2

在函数调用中,执行上下文就是全局对象。

让我们看看下面这个函数的上下文:

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

sum(15, 16) 被调用时,JavaScript 自动的把 this 设置为全局对象,在浏览器中即 window

this 在函数作用域之外使用时(即顶级作用域:全局执行上下文),它是全局对象:

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

2.2. 严格模式下函数调用的 this

严格模式下函数调用的 thisundefined

严格模式是在 ECMAScript 5.1 被引入的。它提供了更好的安全性和错误检查。

启用严格模式,需将 ‘use strict’ 放在函数体顶部。

一旦启用,严格模式会影响执行上下文,在普通函数调用里 thisundefined。在 2.1 的例子中,执行上下文 再是全局对象。

深入浅出 JavaScript  'this' 关键字 - 图3

严格模式下函数执行的例子:

  1. function multiply(a, b) {
  2. 'use strict'; // enable the strict mode
  3. console.log(this === undefined); // => true
  4. return a * b;
  5. }
  6. // multiply() function invocation with strict mode enabled
  7. // this in multiply() is undefined
  8. multiply(2, 5); // => 10

multiply(2, 5) 被调用时,thisundefined

严格模式不仅对当前作用域起作用,还对内部作用域起作用(所有内部声明的函数)。

  1. function execute() {
  2. 'use strict'; // activate the strict mode
  3. function concat(str1, str2) {
  4. // the strict mode is enabled too
  5. console.log(this === undefined); // => true
  6. return str1 + str2;
  7. }
  8. // concat() is invoked as a function in strict mode
  9. // this in concat() is undefined
  10. concat('Hello', ' World!'); // => "Hello World!"
  11. }
  12. execute();

‘use strict’ 出现在 execute 函数体的顶部时,在它的作用域内就启用了严格模式。因为 concatexecute 作用域内被声明,它因此继承了严格模式。在调用 concat(‘Hello’, ‘ World!’) 时,this 等于 undefined

单个 JavaScript 文件可能同时包含严格和非严格模式。因此,在单个脚本中的同一种调用类型可能有着不同的上下文行为:

  1. function nonStrictSum(a, b) {
  2. // non-strict mode
  3. console.log(this === window); // => true
  4. return a + b;
  5. }
  6. function strictSum(a, b) {
  7. 'use strict';
  8. // strict mode is enabled
  9. console.log(this === undefined); // => true
  10. return a + b;
  11. }
  12. // nonStrictSum() is invoked as a function in non-strict mode
  13. // this in nonStrictSum() is the window object
  14. nonStrictSum(5, 6); // => 11
  15. // strictSum() is invoked as a function in strict mode
  16. // this in strictSum() is undefined
  17. strictSum(8, 12); // => 20

2.3. 陷阱:内部函数的 this

函数调用的常见陷阱是认为内部函数的 this 和外部函数一样。

准确来说内部函数的上下文只依赖于调用(类型),而不是外部函数的上下文。

想要期望的 this,需要使用间接调用(使用 .call() 或者 .apply(),见第 5 节)或者绑定函数(使用 .bind(),见第 6 节)去改变内部函数的上下文。

下面是一个计算两个数之和的例子:

  1. var numbers = {
  2. numberA: 5,
  3. numberB: 10,
  4. sum: function() {
  5. console.log(this === numbers); // => true
  6. function calculate() {
  7. // this is window or undefined in strict mode
  8. console.log(this === numbers); // => false
  9. return this.numberA + this.numberB;
  10. }
  11. return calculate();
  12. }
  13. };
  14. numbers.sum(); // => NaN or throws TypeError in strict mode

numbers.sum() 是对对象的方法调用(见第 3 节),sum 的上下文就是 numbers 对象。calculate 定义在 sum 中,因此,你可能会认为 calculate()this 也是 numbers

然而,calculate() 是函数调用(并非方法调用)并且它的 this 是全局对象 window(示例2.1.)或者严格模式下的 undefined (示例2.2.)。即使外部函数 sum 的上下文是 numbers 对象,也没有影响到这里。

numbers.sum() 的调用结果是 NaN 或者在严格模式下抛出错误 TypeError: Cannot read property ‘numberA’ of undefined。总之不是所期待的结果 5 + 10 = 15,全因为 calculate 没有正确调用。

为了解决这个问题,calculate 函数应该和 sum 函数那样在相同的上下文中执行,这样才能访问 numberAnumberB 属性。

一个解决方案是通过调用 calculate.call(this) (函数的间接调用,见第 5 节)去手动的改变 calculate 的上下文。

  1. var numbers = {
  2. numberA: 5,
  3. numberB: 10,
  4. sum: function() {
  5. console.log(this === numbers); // => true
  6. function calculate() {
  7. console.log(this === numbers); // => true
  8. return this.numberA + this.numberB;
  9. }
  10. // use .call() method to modify the context
  11. return calculate.call(this);
  12. }
  13. };
  14. numbers.sum(); // => 15

calculate.call(this) 除了照常执行 calculate 函数,还会将第一个参数作为上下文的值。现在,this.numberA + this.numberB 等价于 numbers.numberA + numbers.numberB。函数将返回希望的结果 5 + 10 = 15

3.方法调用

方法 就是保存在一个对象中的函数属性。例如:

  1. var myObject = {
  2. // helloFunction is a method
  3. helloFunction: function() {
  4. return 'Hello World!';
  5. }
  6. };
  7. var message = myObject.helloFunction();

helloFunctionmyObject 的方法。使用属性访问器获取方法:myObject.helloFunction

当使用函数对象属性访问器后跟圆括号 ,一组逗号分隔的参数和圆括号 的这种赋值表达式时,就是方法调用

回想下前面的例子,myObject.helloFunction() 就是对 myObject 对象的 helloFunction 的方法调用。[1, 2].join(‘,’)/\s/.test(‘beautiful world’) 也是方法调用。

区分 函数调用(见第 2 节)和 方法调用 很重要,因为他们是不同的类型。主要不同是方法调用需要以属性访问器的形式去调用函数(obj.myFunc() 或者 obj‘myFunc’),然而函数调用不需要(myFunc())。

下面的例子展示了如何区分这些类型:

  1. ['Hello', 'World'].join(', '); // method invocation
  2. ({ ten: function() { return 10; } }).ten(); // method invocation
  3. var obj = {};
  4. obj.myFunction = function() {
  5. return new Date().toString();
  6. };
  7. obj.myFunction(); // method invocation
  8. var otherFunction = obj.myFunction;
  9. otherFunction(); // function invocation
  10. parseFloat('16.60'); // function invocation
  11. isNaN(0); // function invocation

理解函数调用和方法调用之间的区别有助于正确识别上下文。

3.1. 方法调用的 this

方法调用中的 this 就是 拥有该方法的对象

对对象的方法调用时,this 就是对象本身:

深入浅出 JavaScript  'this' 关键字 - 图4

让我们写一个对象,其中一个方法实现了自增:

  1. var calc = {
  2. num: 0,
  3. increment: function() {
  4. console.log(this === calc); // => true
  5. this.num += 1;
  6. return this.num;
  7. }
  8. };
  9. // method invocation. this is calc
  10. calc.increment(); // => 1
  11. calc.increment(); // => 2

调用 calc.increment()increment 函数的上下文即 calc 对象。因此,使用 this.num 会增加属性数值。

我们来看另一个例子。一个 JavaScript 对象从它的 原型 继承了一个方法。当继承的方法在对象上调用时,调用的上下文仍然是对象本身:

  1. var myDog = Object.create({
  2. sayName: function() {
  3. console.log(this === myDog); // => true
  4. return this.name;
  5. }
  6. });
  7. myDog.name = 'Milo';
  8. // method invocation. this is myDog
  9. myDog.sayName(); // => 'Milo'

Object.create() 生成了一个新的对象 myDog 并设置了原型。 myDog 对象继承了 sayName 方法。

myDog.sayName() 执行时,myDog 就是调用上下文。

在 ECMAscript 6 的 class 语法中,方法调用上下文同样是实例本身:

  1. class Planet {
  2. constructor(name) {
  3. this.name = name;
  4. }
  5. getName() {
  6. console.log(this === earth); // => true
  7. return this.name;
  8. }
  9. }
  10. var earth = new Planet('Earth');
  11. // method invocation. the context is earth
  12. earth.getName(); // => 'Earth'

3.2. 陷阱:方法与对象分离

对象的方法可以抽离出来赋给独立的变量 var alone = myObj.myMethod。当方法被单独调用时,即使用 alone() 将从原对象分离,你可能认为 this 是定义该方法的对象。

准确来说,不使用对象的方法调用,此时发生了函数调用:这里的 this 是全局对象 window 或者严格模式下为 undefined(见 2.1 和 2.2)。

创建绑定函数 var alone = myObj.myMethod.bind(myObj)(使用 .bind(),见第 6 节)修复上下文,变为拥有该方法的对象。

下面的例子创建了 Animal 构造器和一个实例—— myCat。使用 setTimeout() 在一秒后打印 myCat 对象信息:

  1. function Animal(type, legs) {
  2. this.type = type;
  3. this.legs = legs;
  4. this.logInfo = function() {
  5. console.log(this === myCat); // => false
  6. console.log('The ' + this.type + ' has ' + this.legs + ' legs');
  7. }
  8. }
  9. var myCat = new Animal('Cat', 4);
  10. // logs "The undefined has undefined legs"
  11. // or throws a TypeError in strict mode
  12. setTimeout(myCat.logInfo, 1000);

你可能认为 setTimout 会调用 myCat.logInfo(),将会打印 myCat 对象的信息。

不幸的是在 setTimout(myCat.logInfo) 中当参数被传递时同对象分离了。下面的语句是等价的:

  1. setTimout(myCat.logInfo);
  2. // is equivalent to:
  3. var extractedLogInfo = myCat.logInfo;
  4. setTimout(extractedLogInfo);

logInfo 作为函数被调用时,this 就是全局对象或者 undefined(而不是 myCat 对象)。因此对象信息没有正确打印。

一个函数使用 .bind() (见第 6 节)方法进行绑定。如果分离的方法绑定了 myCat 对象,上下文问题迎刃而解:

  1. function Animal(type, legs) {
  2. this.type = type;
  3. this.legs = legs;
  4. this.logInfo = function() {
  5. console.log(this === myCat); // => true
  6. console.log('The ' + this.type + ' has ' + this.legs + ' legs');
  7. };
  8. }
  9. var myCat = new Animal('Cat', 4);
  10. // logs "The Cat has 4 legs"
  11. setTimeout(myCat.logInfo.bind(myCat), 1000);

myCat.logInfo.bind(myCat) 返回一个执行与 logInfo 一致的新函数,thismyCat,即使执行的是函数调用。

4. 构造器调用

使用 new 关键字,后跟一个函数对象赋值表达式,圆括号 ,逗号分隔的一组参数和圆括号 ,此时发生了 构造器调用。例如: new RegExp(‘\d’)

该例子声明了函数 Country,然后作为构造器调用:

  1. function Country(name, traveled) {
  2. this.name = name ? name : 'United Kingdom';
  3. this.traveled = Boolean(traveled); // transform to a boolean
  4. }
  5. Country.prototype.travel = function() {
  6. this.traveled = true;
  7. };
  8. // Constructor invocation
  9. var france = new Country('France', false);
  10. // Constructor invocation
  11. var unitedKingdom = new Country;
  12. france.travel(); // Travel to France

new Country(‘France’, false)Country 函数的构造器调用。执行的结果就是生成一个新的对象,name 属性是 ‘France’。调用无参的构造器,圆括号可以省略: new Country

从 ECMAScript 2015 开始,JavaScript 允许使用 class 语法定义构造器:

  1. class City {
  2. constructor(name, traveled) {
  3. this.name = name;
  4. this.traveled = false;
  5. }
  6. travel() {
  7. this.traveled = true;
  8. }
  9. }
  10. // Constructor invocation
  11. var paris = new City('Paris', false);
  12. paris.travel();

new City(‘Paris’) 是构造器调用。对象的初始化由类中特殊的方法:constructor 去处理,this 就是新创建的对象。

构造器调用创建一个空的新对象,并继承了构造器原型的属性。构造器方法的作用就是初始化对象。你可能已经知道,这种调用类型的上下文就是生成的实例。这是下一个章节的主题。

当属性访问器 myObject.myFunction 冠以 new 关键字,JavaScript会执行构造器调用,并非方法调用。例如 new myObject.myFunction():首先使用属性访问器 extractedFunction = myObject.myFunction 将函数分离,然后作为构造器进行调用来创建新的对象:new extractedFunction()

4.1. 构造器调用的 this

构造器调用的 this 就是新创建的对象

构造器调用的上下文就是新创建的对象。使用构造器函数的参数来初始化对象,设置属性的初始值,还有事件句柄等。

深入浅出 JavaScript  'this' 关键字 - 图5

让我们检验下面例子的上下文:

  1. function Foo () {
  2. console.log(this instanceof Foo); // => true
  3. this.property = 'Default Value';
  4. }
  5. // Constructor invocation
  6. var fooInstance = new Foo();
  7. fooInstance.property; // => 'Default Value'

new Foo() 调用了构造器,上下文是 fooInstanceFoo 内部对象进行了初始化:this.property 被赋了一个默认值。

当使用 class (ES2015)语法时也是一样,只是初始化发生在了 constructor 方法。

  1. class Bar {
  2. constructor() {
  3. console.log(this instanceof Bar); // => true
  4. this.property = 'Default Value';
  5. }
  6. }
  7. // Constructor invocation
  8. var barInstance = new Bar();
  9. barInstance.property; // => 'Default Value'

new Bar() 执行时,JavaScript 创建了一个空对象并生成构造器方法的上下文。现在你可以用 this 关键字向对象中添加属性:this.property = ‘Default Value’

4.2. 陷阱:忘记关键字 new

某些JavaScript函数进行构造器或者函数调用时都会生成实例。例如 RegExp

  1. var reg1 = new RegExp('\\w+');
  2. var reg2 = RegExp('\\w+');
  3. reg1 instanceof RegExp; // => true
  4. reg2 instanceof RegExp; // => true
  5. reg1.source === reg2.source; // => true

当执行 new RegExp(‘\w+’)RegExp(‘\w+’) 时,JavaScript会生成等价的正则表达式对象。

使用函数调用生成对象是一个潜在的问题(除了工厂模式),因为某些构造器可能在缺失 new 关键字时忽略初始化对象的逻辑。

下面的例子说明了这个问题:

  1. function Vehicle(type, wheelsCount) {
  2. this.type = type;
  3. this.wheelsCount = wheelsCount;
  4. return this;
  5. }
  6. // Function invocation
  7. var car = Vehicle('Car', 4);
  8. car.type; // => 'Car'
  9. car.wheelsCount // => 4
  10. car === window // => true

Vehicle 是一个函数来为上下文对象设置 typewheelsCount。执行 Vehicle(‘Car’, 4) 返回 car 对象,其中: car.type‘Car’car.wheelsCount4。你可能认为该段代码会创建并初始化一个新对象。

然而,在这个函数调用(见2.1.)中 this 就是 window 对象,Vehicle(‘Car’, 4) 错误的为 window 对象设置了属性。新的对象并没有生成。

确保使用 new 操作符以期构造器调用被执行:

  1. function Vehicle(type, wheelsCount) {
  2. if (!(this instanceof Vehicle)) {
  3. throw Error('Error: Incorrect invocation');
  4. }
  5. this.type = type;
  6. this.wheelsCount = wheelsCount;
  7. return this;
  8. }
  9. // Constructor invocation
  10. var car = new Vehicle('Car', 4);
  11. car.type // => 'Car'
  12. car.wheelsCount // => 4
  13. car instanceof Vehicle // => true
  14. // Function invocation. Generates an error.
  15. var brokenCar = Vehicle('Broken Car', 3);

new Vehicle(‘Car’, 4) 正确执行:新的对象被创建和初始化,因为 new 关键字出现在了构造器调用之前。

5. 间接调用

当一个函数使用 myFun.call() 或者 myFun.apply() 方法执行时就是间接调用。

JavaScript中函数是头等对象,这意味着函数是对象。对象的类型是 Function

函数对象的方法列表里 .call().apply() 用来给调用函数时配置一个上下文:

  • .call(thisArg[, arg1[, arg2[, …]]]) 方法接受第一个参数 thisArg 作为调用上下文以及一个参数列表 arg1, arg2, … 传递给被调用的函数作为参数。
  • .apply(thisArg, [arg1, arg2, …]) 方法接受第一个参数 thisArg 作为调用上下文以及一个参数数组 [arg1, arg2, …] 传递给被调用的函数作为参数。

下面是个间接调用的示例:

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

increment.call()increment.apply() 都调用了自增函数,参数为 10

主要不同是,.call() 接受一个参数列表,例如 myFun.call(thisValue, ‘val1’, ‘val2’)。而 .apply() 接受一个参数数组,即 myFunc.apply(thisValue, [‘val1’, ‘val2’])

5.1. 间接调用的 this

间接调用中的 this 就是 .call() 或者 .apply() 的第一个参数。

很明显 this 在间接调用中就是 .call() 或者 .apply() 传递的一个参数。

深入浅出 JavaScript  'this' 关键字 - 图6

下面是一个间接调用上下文的例子:

  1. var rabbit = { name: 'White Rabbit' };
  2. function concatName(string) {
  3. console.log(this === rabbit); // => true
  4. return string + this.name;
  5. }
  6. // Indirect invocations
  7. concatName.call(rabbit, 'Hello '); // => 'Hello White Rabbit'
  8. concatName.apply(rabbit, ['Bye ']); // => 'Bye White Rabbit'

当函数执行需要指定上下文时非常有用。可以解决函数调用时 this 总为 window 或者严格模式下 undefined (见2.3.)的上下文问题。可以用来实现对象的方法调用(见之前的代码示例)。

另一个例子是在 ES5 中调用双亲的构造器生成类的继承关系:

  1. function Runner(name) {
  2. console.log(this instanceof Rabbit); // => true
  3. this.name = name;
  4. }
  5. function Rabbit(name, countLegs) {
  6. console.log(this instanceof Rabbit); // => true
  7. // Indirect invocation. Call parent constructor.
  8. Runner.call(this, name);
  9. this.countLegs = countLegs;
  10. }
  11. var myRabbit = new Rabbit('White Rabbit', 4);
  12. myRabbit; // { name: 'White Rabbit', countLegs: 4 }

RabbitRunner.call(this, name) 对双亲函数进行了间接调用来初始化对象。

6. 绑定函数

绑定函数是连接一个对象的函数。常常由原函数调用 .bind() 方法生成。原函数和绑定函数有着相同的代码和作用域,但是在执行时有着不同的上下文。

.bind(thisArg[, arg1[, arg2[, …]]]) 方法接受第一个参数 thisArg 作为调用时绑定函数的上下文以及可选的参数列表 arg1, arg2, … 传递给调用的函数作为参数。并返回一个绑定了 thisArg 的函数。

下面的代码创建了一个绑定函数并调用:

  1. function multiply(number) {
  2. 'use strict';
  3. return this * number;
  4. }
  5. // create a bound function with context
  6. var double = multiply.bind(2);
  7. // invoke the bound function
  8. double(3); // => 6
  9. double(10); // => 20

multiply.bind(2) 返回了一个新函数对象 double,并绑定了数字 2multiplydouble 有相同的代码和作用域。

.apply().call() (见第 5 节)会立即调用不同,.bind() 方法仅会返回一个新函数,并在之后的调用中使用预先配置的 this

6.1. 绑定函数的 this

当调用绑定函数时 this 就是 .bind() 的第一个参数

.bind() 的作用是生成新函数,并在之后的调用中使用第一个参数作为上下文。这允许使用一个预知的 this 值来生成函数。

深入浅出 JavaScript  'this' 关键字 - 图7

让我们看下如何配置绑定函数的 this

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

numbers.getNumbers.bind(numbers) 返回了一个绑定 numbers 对象的函数 boundGetNumbersboundGetNumbers() 调用时 this 就是 numbers 并返回了正确的数组对象。

numbers.getNumbers 没有使用绑定然后抽离并赋给 simpleGetNumbers。调用 simpleGetNumbersthiswindow 或者 undefined,并不是 numbers 对象(见3.2. 陷阱)。simpleGetNumbers() 没有返回正确的数组。

6.2. 紧上下文绑定

.bind() 生成 不变的上下文链接 并永久保持。绑定函数不运行改变已链接的上下文,即使你调用 .call() 或者 .apply() 应用一个不同的上下文,甚至是重新绑定(译者注:调用bind())。

只有绑定函数的构造器调用可以改变,然而这种方式并不被推荐(使用构造器调用,而非绑定函数)。

下面的例子中创建了一个绑定函数,然后尝试更改已经预定义的上下文:

  1. function getThis() {
  2. 'use strict';
  3. return this;
  4. }
  5. var one = getThis.bind(1);
  6. // Bound function invocation
  7. one(); // => 1
  8. // Use bound function with .apply() and .call()
  9. one.call(2); // => 1
  10. one.apply(2); // => 1
  11. // Bind again
  12. one.bind(2)(); // => 1
  13. // Call the bound function as a constructor
  14. new one(); // => Object

只有 new one() 改变了绑定函数的上下文,其他调用类型的 this 仍等于 1

7. 箭头函数

箭头函数 的目的是以更简短的方式声明函数并在词法上绑定上下文。

用法:

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

箭头函数有更轻量的语法,省略了关键字 function。当函数只有一个语句时,你甚至可以省略 return

箭头函数是匿名的,这意味着(译者注:函数的) name 属性为空字符串 ‘’。这种情况下它没有函数名(这对于递归很有用,分离了事件句柄)。

同时,与常规函数不同,它不提供 arguments 对象。然而在 ES2015 中使用 剩余参数 可以解决这个问题。

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

7.1. 箭头函数的 this

this 是定义箭头函数的 封闭上下文

箭头函数不会创建自己的执行上下文,this 来自定义箭头函数的外部函数。

深入浅出 JavaScript  'this' 关键字 - 图8

下面例子展示了上下文的透明性:

  1. class Point {
  2. constructor(x, y) {
  3. this.x = x;
  4. this.y = y;
  5. }
  6. log() {
  7. console.log(this === myPoint); // => true
  8. setTimeout(()=> {
  9. console.log(this === myPoint); // => true
  10. console.log(this.x + ':' + this.y); // => '95:165'
  11. }, 1000);
  12. }
  13. }
  14. var myPoint = new Point(95, 165);
  15. myPoint.log();

setTimeout 调用箭头函数,并和 log() 方法一样使用相同的上下文(myPoint 对象)。可以看到,箭头函数 ‘继承’ 了定义它的函数的上下文。

在这个例子中如果用常规函数则会创建自己的上下文(window 或者 undefined)。因此想要相同的代码正确运行有必要绑定上下文:setTimeout(function() {…}.bind(this))。这不太简洁,使用箭头函数则非常清晰简单。

如果箭头函数定义在顶级作用域(在任何函数之外),上下文永远是全局对象(在浏览器中就是 window):

  1. var getContext = () => {
  2. console.log(this === window); // => true
  3. return this;
  4. };
  5. console.log(getContext() === window); // => true

箭头函数绑定词法上下文仅一次并永久绑定。使用上下文改变方法(译者注:.call().apply() 这些)也无法修改:

  1. var numbers = [1, 2];
  2. (function() {
  3. var get = () => {
  4. console.log(this === numbers); // => true
  5. return this;
  6. };
  7. console.log(this === numbers); // => true
  8. get(); // => [1, 2]
  9. // Use arrow function with .apply() and .call()
  10. get.call([0]); // => [1, 2]
  11. get.apply([0]); // => [1, 2]
  12. // Bind
  13. get.bind([0])(); // => [1, 2]
  14. }).call(numbers);

使用 .call(numbers) 对函数表达式间接调用,调用的 thisnumbers。箭头函数 getthis 也是 numbers,因为它使用词法上下文。

无论 get 如何调用,箭头函数永远保持初始的上下文 numbersget.call([0]) 和 . get.apply([0]) 使用其他的上下文进行间接调用,以及 get.bind([0])() 进行重绑定也无效。

箭头函数不能用作构造器。如果使用 new get() 进行构造器调用,JavaScript 会抛出错误:TypeError: get is not a constructor

7.2. 陷阱:使用箭头函数定义方法

你可能想要使用箭头函数来为对象声明方法。当然可以:使用 (param) => {…} 而非 function(param) {..},相对于函数表达式会非常简短。

下面的例子中我们对 Period 类使用箭头函数定义 format() 方法:

  1. function Period (hours, minutes) {
  2. this.hours = hours;
  3. this.minutes = minutes;
  4. }
  5. Period.prototype.format = () => {
  6. console.log(this === window); // => true
  7. return this.hours + ' hours and ' + this.minutes + ' minutes';
  8. };
  9. var walkPeriod = new Period(2, 30);
  10. walkPeriod.format(); // => 'undefined hours and undefined minutes'

因为 format 是箭头函数并在全局上下文(顶级作用域)中定义,那么 this 就是 window 对象。

即使 format 在对象中作为方法执行:walkPeriod.format()window 仍是调用的上下文。因为箭头函数有一个静态上下文在不同的调用类型中不会改变。this 就是 window,因此 this.hoursthis.minutes 都是 undefined。方法会返回:‘undefined hours and undefined minutes’,这与你预期的结果不一样。

函数表达式可以解决这个问题,因为常规函数的上下文依赖于调用(译者注:类型):

  1. function Period (hours, minutes) {
  2. this.hours = hours;
  3. this.minutes = minutes;
  4. }
  5. Period.prototype.format = function() {
  6. console.log(this === walkPeriod); // => true
  7. return this.hours + ' hours and ' + this.minutes + ' minutes';
  8. };
  9. var walkPeriod = new Period(2, 30);
  10. walkPeriod.format(); // => '2 hours and 30 minutes'

walkPeriod.format() 是一个对象的方法调用(见 3.1.),上下文是 walkPeriod 对象。this.hours 等于 2this.minutes 等于 30,因此方法返回正确的结果:‘2 hours and 30 minutes’

8. 总结

因为函数调用(译者注:类型)对于 this 有很大的影响,从现在开始 不要 问自己:

this 从哪里来?

而是 要问自己

函数是 如何调用 的?

对于箭头函数要问自己:

箭头函数在 定义处 的 this 是什么?

处理 this 时思路正确才不会令你头疼。

如果你对上下文陷阱的例子感兴趣或者刚刚碰到了这些困难,在下面进行评论让我们一起讨论!

传播关于JavaScript的知识并分享该帖子,你的同事会感激你。

记住,不要丢掉你的上下文 ;)