1. 1. 什么情况下需要使用 this
  2. 2. this 到底是指什么(有何作用)
  3. 3. 函数中的 this
  4. 4. 隐式绑定:对象方法中的 this
  5. 5. 显示绑定 call, apply, new
  6. 6. 箭头函数
  7. 7. this 的本质
Table of Contents ▼

JavaScript 起手式之 this

JavaScript 中的 this 是一个比较难的概念,既不同于OOP,也似乎没有一条清晰唯一的判断标准,而且说不定你还经常用其他方法跳过这个坑;很早以前我就想写一篇总结this的笔记,总是觉得不放心,踩了无数坑后才痛下决心一定要知其所以然。阅读前,先思考一个问题,到底什么情况下需要使用 this ?

什么情况下需要使用 this

面向对象编程

function Person(firstName, lastName){
  this.firstName = firstName;
  this.lastName = lastName;
  this.showFullName = function(){
    return `${this.firstName} ${lastName}`;
  }
}

const p = new Person('foo', 'bar');
p.showFullName();

JavaScript 和其他 OOP 语言有较大差别,但面向对象的思想都是一样,对数据进行抽象,高内聚低耦合,更符合人类的思考方式。

16年创业期间,突然发现函数式编程大火,其实不管是面向对象还是函数式编程,都有自己的优点,不必神话某一种模式,在实践中,学会灵活运用,皆为我用。

再看一个链式调用(级联)的例子:

let ladder = {
  step: 0,
  up() {
    this.step++;
  },
  down() {
    this.step--;
  },
  showStep() { // shows the current step
    alert(this.step);
  }
};

ladder.up();
ladder.up();
ladder.down();
ladder.showStep(); // 1

改成链式调用:

let ladder = {
  step: 0,
  up() {
    this.step++;
    return this;
  },
  down(){
    this.step--;
    return this;
  },
  showStep() {
    alert(this.step);
  }
}

ladder.up().up().down().showStep(); // 1

这个例子可能没有太多感觉,但是在遇到回调嵌套时,链式调用的好处就提现出来了。

this 到底是指什么(有何作用)

上面的两个例子虽然简单,但是都可以引出一个问题,this 在其中扮演了什么角色。为什么 this.showFullName() 能直接使用 firstNamelastName ,为何 ladder.up().up().down() 记住了之前的操作。或者说,不用 this 如何重写上面的代码。

大学接触面向对象时,我认为自己已经准确理解了 this,但在 JS 中总觉得没摸到根本,有一次想到上面的问题后,才回头重新思考:我们之所以用 this,是因为 this 可以直接帮我们引用对象无须手动传递,但在 JS 中,this 是动态不确定的,需要仔细分析指向的对象是否符合预期。

执行上下文由 3 部分组成:作用域链,变量对象,this。如果抽象为一个对象:

executionContextObj = {
  'scopeChain': { /* variableObject + all parent execution context's variableObject */ },
  'variableObject': { /* function arguments / parameters, inner variable and function declarations */ },
  'this': {},
}

理解 this 不能不讲执行上下文,这篇文章无法囊括所有知识点,相关内容可阅读《What is the Execution Context & Stack in JavaScript?》

函数中的 this

遇到 this 时,你会想:“这个 this 指向的是哪个对象”。但是如果 this 单独出现在函数中,this 指向什么?

function foo() {
  var a = 2;
  this.bar();
}

function bar() {
  console.log(this.a);
}

foo(); // undefined

摘自《你不知道的JavaScript上卷》第二部分第1章

和其他语言不同,js 中的 this 可单独出现在函数中:独立函数调用。

在非严格模式中,this 被绑定到全局对象(浏览器中即 window);而严格模式中,this 则是 undefined

上面的例子时用来混淆视听的,很久以前我看过一篇讲解 this 的文章,当时就记住了”谁调用的这个函数,this 就指向谁“,若以此为标准,函数之间的调用似乎就有疑惑了。有时候想想,其实这是非常有意思的,我们一直试图去找 this,但却忽略了它本身的意义:隐式传递上下文。对于单纯的函数调用,仅仅是输入输出,设计上作为数据的转换或者生成,不需要上下文的传递。

Please note that usually a call of a function that uses this without an object is not normal, but rather a programming mistake. If a function has this, then it is usually meant to be called in the context of an object. ——《The Modern Javascript Tutorial》

// strict mode
function foo() {
  "use strict";
  console.log(this.a);
}

var a = 2;
foo(); // TypeError: Cannot read property 'a' of undefined

// non-strict mode
function bar() {
  console.log(this.a);
}

var a = 2;
bar(); // 2

隐式绑定:对象方法中的 this

var a = 1;
function foo() {
  console.log(this.a);
}

var obj = {
  a: 2,
  foo,
  bar: function() {
    console.log(this.a);
  },
};

obj.foo(); // 2
obj.bar(); // 2

foo 和 bar 的定义方式不同,但在调用时,this 都指向了 obj。this 和函数声明的位置无关,只取决于函数的调用方式。

换种方式调: 

const bar = obj.bar;
bar(); // 1

bar 和 obj.bar 都是同一个函数的引用,但调用方式却有所不同,bar()相当于我们直接运行了这个函数,非严格模式下,this 指向了 window

同样的道理,思考一下定时器中的 this:

var a = 1;
const obj = {
  a: 2,
  foo: function() {
    setTimeout(function() {
      console.log(this.a);
    }, 0);
  },
};
obj.foo(); // 1;

上面的代码,以前我会这么解释:setTimeout 中的函数被放到任务队列中执行,脱离了原本的上下文,实际上就相当于在全局 window 下运行。但是,《你不知道的JavaScript》中的解释我觉得更为透彻:

参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值

回调函数改变了本身预期的 this ,比如异步请求的回调,除了使用 that,JavaScript 本身还提供了手动绑定this的方法:callapply

显示绑定 call, apply, new

call, apply, bind

Function.prototype.callFunction.prototype.apply 接收两个参数,第一个参数是 object,强制把 this 绑定到这个对象上:

function foo() {
  console.log(this.a);
}
const obj = {
  a: 2,
};
const bar = function() {
  return foo.apply(obj);
}
console.log(bar()); // 2

创建包裹函数 bar 传递 this 并返回返回值。

创建一个更通用的辅助函数:

function bind(fn, obj) {
  return function(){
    fn.apply(obj, arguments);
  };
}

const obj = { a: 2 };
function foo() {
  console.log(this.a);
}
const bar = bind(foo, obj);
bar(); // 2

ES5 提供了内置的 Function.prototype.bind,参考 bind 的 API 思考如何自己实现。

new

new 绑定看起来很正常很简单,如下:

function Foo(a) {
  this.a = a;
}
const foo = new Foo(2);
foo.a(); // 2

JavaScript 中的 new 和传统的构造函数有所区别,小心这个误区。

箭头函数

箭头函数最为简单,this 与当前函数无关,继承外层函数调用的 this

今年面试的时候,有个面试官问我在 setTimeout 中的 this 是指向谁,我说这都是不变的,永远和外层一样;他说,不对,是 windowsetTimeout 中的函数执行时,上下文环境已经是全局对象了。看起来似乎很有道理,但是可能这位大佬太过自负自己曾经的理解,希望大家不要被误解。

function() {
  setTimeout(() => {
    console.log(this);
  });
}

const obj = { a: 2 };
foo.call(obj); // 2

this 的本质

上面的总结几乎包括了所有开发中 this 出现的场景,但经验性的总结似乎有总会有漏洞,所以还需要从深的层次去理解 this 的原理。看下面的列子:

let obj, method;

obj = {
  go: function() { alert(this); }
};

obj.go();               // (1) [object Object]

(obj.go)();             // (2) [object Object]

(method = obj.go)();    // (3) undefined

(obj.go || obj.stop)(); // (4) undefined

解释上面的列子前,再看隐式调用的例子:

var a = 1;
var obj = {
  a: 2,
  bar: function() {
    console.log(this.a);
  },
};

var bar = obj.bar;
obj.bar(); // 2
bar(); // 1

我们之前的解释说过,obj.bar()bar() 是两种不同的调用方式,但是到底哪不同?在 js 中,方法与对象是独立存在的,对象的属性指向函数的引用,而这个函数也可以和其他对象关联,似乎都和 this 无关呀。真正的技巧在于 .obj.method 返回的值并不是 Function 而是一个 [Reference Type](https://tc39.github.io/ecma262/#sec-reference-specification-type)

Reference 由三部分组成 base, name , strict

  • base:The base value component is either undefined, an Object, a Boolean, a String, a Symbol, a Number, or an Environment Record. A base value component of undefined indicates that the Reference could not be resolved to a binding.
  • name:The referenced name component is a String or Symbol value.
  • strict:Boolean-valued strict reference flag

obj.bar 返回的就是 (obj, '``bar``'``, false)

当一个 Reference 后跟 ()Refefrence 的信息被传递下去, this 这时候等于 base;而其他情况,Reference 的信息将被丢弃,仅仅传递函数的值(引用),导致 this 丢失。

obj['bar']()obj.bar() 相同

最后,希望你真的能想清楚 (obj.go)() 为何是输出是 2,这是一道难倒了许多高手(一位腾讯前端,一位迅雷资深前端)的题目。

如果谈 this 的实践,还应该详细讲解 react constructor 中 this 的绑定,很长时间我对这个地方的理解有误,但为了便于各个笔记间的独立,这部分留在 react 的系列进行讲解。