[JavaScript 笔记] 最全的 this 判断方法

JavaScript 中 this 的指向肯定是大多数前端开发者最头疼的问题之一,就算是经验丰富的老手都可能被绕到迷失方向。本文想做的就是去追寻 this 的迷踪。
this 是什么 this 是 JavaScript 中的一个关键字,它是由运行环境内置的一个变量,且运行期不能重新赋值,那 this 到底是什么?
JavaScript 是基于原型的脚本语言,最初的设计是为了给 web 页面添加一些辅助处理逻辑,比如表单校验、动画效果、时钟等等。而且,早期 JavaScript 逻辑非常简单,基本都是脚本片段,写起来也是 c 语言 面向过程的味道,根本没有现在的代码分层、模块加载、打包发布等等高级功能。
再说说主角 this,它还有段陈年往事:作者 Brendan Eich 本人的想法是设计一门类似 Schema 的函数式的脚本语言,然而当时的网景公司决定要蹭流行语言 Java 的热度,所以取名 JavaScript。基于这些考虑,作者借鉴了 c 语言 的基本语法和 self 语言 原型式面向对象机制, 塑造了 JavaScript 简单轻量又灵活的特点,但是为了更像 Java 一点,最后硬生生加入了 this, new 的特性。
this 指向的各种场景 因为上述历史原因,导致 JavaScript 的 this 没有前后统一的设计,很难理解就不足奇怪了。它不是 Java 那样指向对象实例,也不是执行上下文,它在不同的代码场景,判断规则不一样。这就是它的复杂性所在。
不过,也不是完全没有规则可循,总结下来,场景也就那么几种,你掌握了判断技巧,就能准确判断 this
1. 全局
全局就是不在任何函数里面,这种情况不管是否严格模式,this 都是指向全局对象,浏览器里面就是 window (后面的示例代码没有特别注明的都浏览器环境)。

console.log(this); // window

2. 普通函数
普通函数的直接调用,函数内 this 非严格模式指向全局对象,严格模式下指向 undefined
function foo() { console.log(this); // window }foo(); function bar() { 'use strict'; console.log(this); // undefined }bar();

另外,函数表达式和立即执行函数也是这个规则。
const foo = function () { console.log(this); // window }; foo(); (function bar() { console.log(this); // window })();

3. 属性方法
属性方法也是一个函数,不过它是一个对象的属性,调用时通过 . 操作符访问和调用,方法内 this 指向方法所属对象。 注意:类和原型的方法也是属性方法。
const foo = { bar: function () { console.log(this); // {bar} }, }; foo.bar();

另外,通过原型和 class 定义方法也是指向实例对象。
function Foo() { console.log(this); // Foo{} }Foo.prototype.bar = function () { console.log(this); // Foo{} }const f1 = new Foo(); f1.bar(); class Foobar() { function bar () { console.log(this); // Foobar{} } }const f2 = new Foobar(); f2.bar();

【[JavaScript 笔记] 最全的 this 判断方法】注意,下面场景 baz 属于普通函数调用,因为它不是通过对象属性来访问的。
const foo = { bar: function () { console.log(this); // {bar}function baz() { console.log(this); // window } baz(); }, }; foo.bar();

还有这种情况,对象属性弹出后也是普通函数调用,它也不是通过对象属性来访问的。
const foo = { bar: function () { console.log(this); // window }, }; const baz = foo.bar; baz();

4. new 构造函数
JavaScript 里面,普通函数都可以作为构造函数,然后使用 new 关键字构造出对象,构造函数内 this 指向新构建的对象。
function Foo() { console.log(this); // Foo{} }const f1 = new Foo();

5. call, apply
通过 call, apply 间接调用函数(call, apply 功能一致,只是传参格式不一样), this 指向是通过第一个参数指定,需要注意的是:非严格模式下,非对象会尝试转成对象,nullundefined 会转为全局对象;严格模式下,则传的是什么就是什么。
function foo() { console.log(this); // window }foo.call(null); function bar() { 'use strict'; console.log(this); // null }bar.call(null);

另外注意,对属性方法 call,this 也和普通函数的 call 一样,指向指定参数。
const foo = { bar: function () { console.log(this); // {value: 'baz'} } }; foo.bar.call({value: 'baz'});

6. bind
bind 会创建一个绑定了指定 this 的新函数,注意:只会生效一次。
function foo() { console.log(this); // { value: 'bar' } }const bar = foo.bind({ value: 'bar' }); bar();

另外注意,对属性方法 bind,this 也和普通函数的 bind 一样,指向指定参数。而且,bind 之后再 call,不能修改 this 指向。
const foo = { bar: function () { console.log(this); // {value: 'baz'} } }; const baz = foo.baz.bind({value: 'baz'}); baz.call({value: 'bazzz'});

bind 生成的新函数,尽量不要用作构造函数,一般也只有面试题会这么干,这样做确实不会报错,this 会指向新构造的对象。
7. 箭头函数
箭头函数是 ES6 新增的一种函数,不管是单独调用,还是作为成员方法调用,this 都是指向定义时所在普通函数的 this ,全局定义时指向全局对象。注意:call, apply, bind 对它无效,它也不能用作构造函数。
function foo() { console.log(this); // { value: 'bar' }setTimeout(() => { console.log(this); // { value: 'bar' } }); }foo.call({ value: 'bar' });

注意,有几种特别的情况:
  • 箭头函数作为属性方法,这种情况还是属于全局定义,this 指向全局对象。
const foo = { bar: () => { console.log(this); // window }, }; foo.bar();

  • 箭头函数作为原型方法,这种情况还是属于全局定义,this 指向全局对象。
function Foo() { console.log(this); // Foo{} }Foo.prototype.bar = () => { console.log(this); // window }const f1 = new Foo(); f1.bar();

  • 箭头函数作为类属性,这种情况 this 指向对象实例,和上面的原型式定义相比时,可能会让人迷惑。其实这种情况相当于在构造函数内的定义:this.bar = ...
class Foo { bar = () => { console.log(this); // Foo{} } }const f1 = new Foo(); f1.bar();

特别补充一下,场景 3 有个示例是在 class 中定义方法,使用的普通函数,这两种声明方式其实有着本质区别,普通函数声明是定义在原型中的,而箭头函数声明是定义在示例对象中的。
8. 其他补充
除此之外,还有一些特殊场景,this 指向全局对象,冴羽 大佬是根据 ECMAScript 规范去解读的(见参考文章),我自己总结一下:进行了求值计算的就和属性弹出一样,属于普通函数调用,其实还是判断是否是函数调用还是方法调用。
const foo = { bar: function () { console.log(this); // window } }; (foo.bar = foo.bar)(); (false || foo.bar)(); (foo.bar, foo.bar)();

总结 总结几个要点:
  • 判断是普通函数还是属性方法调用;
  • 箭头函数只看定义所在函数;
  • 构造函数指向构造对象;
  • call, apply, bind 看指定参数;
  • 优先级,bind 之后 new,new 优先,bind 之后 call,bind 优先。
参考文章 this - JavaScript | MDN
Gentle Explanation of "this" in JavaScript
JavaScript深入之从ECMAScript规范解读this · Issue #7 · mqyqingfeng/Blog

    推荐阅读