原文: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

严格模式下函数调用的 this 为 undefined

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

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

一旦启用,严格模式会影响执行上下文,在普通函数调用里 this 为 undefined。在 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) 被调用时,this 为 undefined。

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

  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 函数体的顶部时,在它的作用域内就启用了严格模式。因为 concat 在 execute 作用域内被声明,它因此继承了严格模式。在调用 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 函数那样在相同的上下文中执行,这样才能访问 numberA 和 numberB 属性。

一个解决方案是通过调用 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();

helloFunction 是 myObject 的方法。使用属性访问器获取方法: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 一致的新函数,this 为 myCat,即使执行的是函数调用。

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() 调用了构造器,上下文是 fooInstance。Foo 内部对象进行了初始化: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 是一个函数来为上下文对象设置 type 和 wheelsCount。执行 Vehicle(‘Car’, 4) 返回 car 对象,其中: car.type 为 ‘Car’ 且 car.wheelsCount 为 4。你可能认为该段代码会创建并初始化一个新对象。

然而,在这个函数调用(见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 }

Rabbit 中 Runner.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,并绑定了数字 2。multiply 和 double 有相同的代码和作用域。

和 .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 对象的函数 boundGetNumbers。boundGetNumbers() 调用时 this 就是 numbers 并返回了正确的数组对象。

numbers.getNumbers 没有使用绑定然后抽离并赋给 simpleGetNumbers。调用 simpleGetNumbers,this 为 window 或者 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) 对函数表达式间接调用,调用的 this 是 numbers。箭头函数 get 的 this 也是 numbers,因为它使用词法上下文。

无论 get 如何调用,箭头函数永远保持初始的上下文 numbers。get.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.hours 和 this.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 等于 2 ,this.minutes 等于 30,因此方法返回正确的结果:‘2 hours and 30 minutes’。

8. 总结

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

this 从哪里来?

而是 要问自己:

函数是 如何调用 的?

对于箭头函数要问自己:

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

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

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

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

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