前言

我们研究的 this ,都是研究函数私有上下文中的 this。
因为全局上下文中的 this,都是 window。

全局上下文中,不管是不是严格模式 this 都是 window。

块级上下文中没有自己的 this, 在此上下文中的 this,都是所处环境(上级上下文)中的 this。
ES6 箭头函数也没有自己的 this,在上下文中遇到的 this,也是所处环境(上级上下文)中的 this。

注意 this 并不是执行上下文,EC 是执行上下文,this 是执行主体。

this 是执行主体

在正式讨论 this 之前,我们先搞清楚一个概念「this 是执行主体」。

为什么说「this 是执行主体」?通俗来讲,是谁把他执行了,而不是在哪里执行,也不是在哪里定义,所以说 this 是谁和在哪里执行以及在哪里定义是没有直接的关系的。想要搞定 this,可以按照以下的总结规律来分析。

dom 元素进行事件绑定

不管是 dom0 级事件还是 dom2 级事件,当事件行为触发,绑定的方法执行,方法中的 this 就是当前 dom 元素本身。

  1. <body>
  2. <script>
  3. document.onclick = function() {
  4. console.log(this); // this -> document 对象
  5. }
  6. document.addEventListener('click', function () {
  7. console.log(this); // this -> document 对象
  8. });
  9. </script>
  10. </body>

普通函数执行

当方法执行,看执行函数之前是否存在 「.」运算符。

有「.」运算符

运算符之前是什么,那么 this 就是什么。

  1. <body>
  2. <script>
  3. const fn = function() {
  4. console.log(this); // this -> obj对象
  5. }
  6. let obj = {
  7. name: "张三",
  8. fn
  9. };
  10. obj.fn();
  11. </script>
  12. </body>

没有「.」运算符

严格模式下,this 是 undefined,非严格模式下,this 是 window。

  • 非严格模式
    1. <body>
    2. <script>
    3. const fn = function() {
    4. console.log(this); // this -> window
    5. }
    6. let obj = {
    7. name: "张三",
    8. fn
    9. };
    10. fn();
    11. </script>
    12. </body>
  • 严格模式

    1. <body>
    2. <script>
    3. "use strict";
    4. const fn = function() {
    5. console.log(this); // this -> undefined
    6. }
    7. let obj = {
    8. name: "张三",
    9. fn
    10. };
    11. fn();
    12. </script>
    13. </body>

    匿名函数执行

    匿名函数的执行,分为立即执行函数或者回调函数的执行,这两种情况下的 this 都是指向window(非严格模式)/undefined(严格模式)。除非做过特殊处理。

    立即执行函数

  • 非严格模式

    1. <body>
    2. <script>
    3. (function() {
    4. console.log(this); // this -> window
    5. })();
    6. </script>
    7. </body>
  • 严格模式

    1. <body>
    2. <script>
    3. (function() {
    4. console.log(this); // this -> undefined
    5. })();
    6. </script>
    7. </body>

    这里有一个小知识点: 包围函数(function(){}) 的第一对括号向脚本返回未命名的函数,随后一对空括号立即执行返回的未命名函数,括号内为匿名函数的参数。可以用它创建命名空间,只要把自己所有的代码都写在这个特殊的函数包装内,那么外部就不能访问,除非你允许(变量前加上window,这样该函数或变量就成为全局)。各JavaScript库的代码也基本是这种组织形式。

    • !function () { // }();
    • ~function () { // }();
    • -function () { // }();
    • +function () { // }();
    • void function () { // }();
    • (function (){//}());
    • (function (){//})();

    这几种立即执行函数的写法都是等价的效果。

回调函数

一般情况,把函数作为参数传给另外一个函数,在另外一个函数将其执行,这个函数就是回调函数。例如:

  1. const fn = function (callback) {
  2. callback();
  3. }
  4. fn(function() {});
  5. setTimeout(function() {
  6. console.log(this); // this -> window
  7. }, 1000);

在比如 setTimeout,不管是严格模式还是非严格模式下 setTimeout 回调函数的 this 都是指向 window。

  1. setTimeout(function() {
  2. console.log(this); // this => window
  3. }, 1000);

但是还有一些回调函数可以做特殊处理,this 并不是总指向 window。

  1. const arr = [1, 2];
  2. const obj = {
  3. name: 'juejin'
  4. };
  5. // 正常情况
  6. arr.forEach(function(item, index) {
  7. console.log(); // this -> 严格模式 undefine,非严格模式下 window
  8. });
  9. // 特殊处理
  10. arr.forEach(function(item, index) {
  11. console.log(); // this -> obj(无论严格模式还是非严格模式下)
  12. }, obj);

arr.forEach(callback(currentValue [, index [, array]])[, thisArg]),thisArg 可选参数。当执行回调函数 callback 时,用作 this 的值。

构造函数执行

对于构造函数,只要记住构造函数执行,this 执行当前类的实例。构造函数体中的 THIS 在“构造函数执行”的模式下,是当前类的一个实例,并且 THIS.XXX=XXX 是给当前实例设置的私有属性。而原型上的方法中的 THIS 不一定都是实例,主要看执行的时候,「.」运算符前面的内容。

  1. function Func() {
  2. this.name = "F";
  3. console.log(this); // this -> Func
  4. }
  5. Func.prototype.getNum = function getNum() {
  6. console.log(this); // 原型上的方法中的 THIS 不一定都是实例,主要看执行的时候,「.」运算符前面的内容。
  7. };
  8. let f = new Func;
  9. f.getNum();
  10. f.__proto__.getNum();
  11. Func.prototype.getNum();

特殊的箭头函数

箭头函数中没自身的this,所用的 this 都是上下文中的 this 。这个在文章开头就说过了

  • 箭头函数没有this
  • 箭头函数没有prototype
  • 箭头函数没有constructor
  • 箭头函数不能被new执行
  • 箭头函数没有arguments,如果想用实参集合只能使用…args。

普通函数执行:

  • 形成私有上下文 (和AO)
    • 初始化作用域链
    • 初始化THIS
    • 初始化ARGUMENTS
    • 形参赋值
    • 变量提升
    • 代码执行

箭头函数执行:

  • 形成私有上下文 (和AO)

    • 形参赋值
    • 代码执行
    • 代码执行的时候遇到 this 直接找上级上下文中的 this。
      1. let obj = {
      2. i: 0,
      3. // func:function(){}
      4. func() {
      5. // THIS:OBJ
      6. let _this = this;
      7. setTimeout(function () {
      8. // THIS: WINDOW 回调函数中的THIS一般都是WINDOW(但是有特殊情况)
      9. _this.i++;
      10. console.log(_this);
      11. }, 1000);
      12. }
      13. };
      14. obj.func();

      非基本情况的 call / apply / bind

      call/apply/bind 都属于非基本情况,因为存在这几种场景,this 是可以特殊定制的。
    • call/apply:第一个参数就是改变 this 的指向,写谁就是谁,在非严格模式下,null/undefined 指向的是 window。

      小知识点:call/apply 的唯一区别就是,传递参数不一样,apply 第二个参数是数组,call的参数是一个一个传递。 call 的性能要比 apply 好一些(尤其是传递给函数的参数超过三个的时候),不要问我为什么?因为我也不知道,国外大佬实验的结果。

  • bind:call/apply 都是改变 this 的同时就把函数执行了,但是 bind 不是立即执行函数,属于预先改变 this 和传递一些内容,利用的是柯理化的思想。

练习题

  1. let x = 3;
  2. let obj = {
  3. x: 5
  4. };
  5. obj.fn = (function() {
  6. this.x *= ++x;
  7. return function(y){
  8. this.x *= (++x) + y;
  9. console.log(x);
  10. };
  11. })();
  12. let fn = obj.fn;
  13. obj.fn(6);
  14. fn(4);
  15. console.log(obj.x, x);

这一道练习题结合了函数的运行机制、闭包、this 执行的知识点。如果这将 let 改成 var 还有一点变量提升的味道在里面。如果你看了我之前的几篇前端基石的文章,可能很快就能在脑海中构建起运行图。
前端基石:this 的基本几种情况 - 图1
这段练习代码如果稍微改动一下,将 let 改成 var ,结果又会如何了,思考一下?

  1. var x = 3;
  2. var obj = {
  3. x: 5
  4. };
  5. obj.fn = (function() {
  6. this.x *= ++x;
  7. return function(y){
  8. this.x *= (++x) + y;
  9. console.log(x);
  10. };
  11. })();
  12. var fn = obj.fn;
  13. obj.fn(6);
  14. fn(4);
  15. console.log(obj.x, x);

总结

本文介绍了一个基础小知识,关于 this 的几种基本情况。

  • dom 事件绑定时的 this
  • 普通函数执行时的 this
  • 匿名函数执行时的 this
  • 构造函数执行时的 this
  • 箭头函数执行时的 this
  • 非基本情况的 call / apply / bind

最后在通过一个练习题,不仅巩固 this 这个基础知识,还能温顾之前的闭包、函数执行、变量提升等知识点,如果你对这些还不了解,可以看看我之前的 「前端基石」系列文章。

参考