JS 上下文 this 指向总结

这个 js 语言中的 this 和其他面向对象的语言有本质的不同, 也更复杂, 它更多取决于函数在不同场景下的调用方式, 要理解它并总结出它的规律的话, 优先要从上下文 这个概念认知说起.

理解上下文

上下文 context 可理解为程序执行时的背景环境, 包含了在特定时刻程序所需要的所有信息. 包括变量的值, 函数的调用情况, 执行的位置等.

上下文的核心应用场景在于, 程序状态管理, 函数调用, 内存管理, 异常处理等. 本篇这里是以 JS 编程语言层面的上下文 this 指向总结, 就更多是一种规则梳理.

  • 函数中可以使用 this 关键字, 它表示函数的上下文
  • 与中文中的 类似, 函数中 this 指向必须通过 调用函数时 的 "前言后语" 来判断
  • 如果函数不调用, 则不能确定函数的上下文

规则01: 对象.方法(), this 指向该打点的对象

当出现 对象.方法() 的场景时, 方法里面的 this 就是这个对象.

// case 01
function fn() {
  console.log(this.a + this.b);
}

var obj = {
  a: 100,
  b: 200,
  fn: fn 
}

obj.fn()  // this 指向 obj

这里便是构成了 obj.fn() 的形式, 此时对象的方法 fn 中的 this 则指向该对象 obj 则最后输出 300.

// case 02
var obj1 = {
  a: 1,
  b: 2,
  fn: function () {
    console.log(this.a + this.b);
  }
}

var obj2 = {
  a: 3,
  b: 4,
  fn: obj1.fn 
}

obj2.fn() // this 指向 obj2

首先要注意对于 obj1 来说, 里面的 this 在方法没有被调用的时候, 是不知道具体指向的.

然后分析 obj2.fn() 是符合对象.方法()1的, 虽然这里的 fnobj1 的 fnthis 仍指向 obj1 则最后输出 7.

// case 03
function outer() {
  // 这里的 a, b 是内部变量, 其实是个干扰项
  var a = 1
  var b = 2
  // 外层函数返回一个对象
  return {
    a: 3,
    b: 4,
    fn: function () {
      console.log(this.a + this.b);
    }
  }
}

outer().fn() // this 指向 outer() 返回的对象

分析调用可知 outer() 返回的是一个对象, 对象再调用其方法 fn 所以还是适用于 对象.方法() 的形式, 因此这里的 this 便是指向其返回的对象, 则输出 7.

// case 04
function fn() {
  console.log(this.a + this.b);
}

var obj = {
  a: 1,
  b: 2,
  c: [{
    a: 3,
    b: 4,
    c: fn
  }]
}

var a = 5  // a 是全局变量
obj.c[0].c() // this 指向 c 里面的对象

分析调用可知, obj.c[0] 是一个对象, 然后再调用里面的 fn 方法, 则还是构成了 对象.方法() 形式, 则里面的 this 指向 c 里面的对象, 这个全局的 5 没有啥关系, 则最后输出 7.

规则02: 圆括号直接调用函数(), this 指向 window 对象

当出现普通 函数() 的场景时, 函数里面的 this 在浏览器中指向 window对象

nodejs 中则指向空对象 {} 本篇的所有分析均用浏览器哈, 不在 nodejs 中运行.

// case 01
var obj = {
  a: 1,
  b: 2,
  fn: function () {
    console.log(this.a + this.b);
  }
}

var a = 3
var b = 4

var fn = obj.fn
fn() 

从调用分析, 这里 fn 的调用首先是进行了一个函数的提取 obj.fn, 然后再调用则形成了 函数() 的形式, 则此时 this 在浏览器指向了 window 对象, 全局变量 a, b 都是其属性, 则最后输出7.

注意在 nodejs 里面上面的代码是不能运行的, 因为其没有 window 对象哦

// case 02
function fn() {
  return this.a + this.b
}

// 全局变量
var a = 1
var b = 2

var obj = {
  a: 3,
  b: fn(), // 函数调用()
  fn: fn 
}

var result = obj.fn() // 对象.方法()
console.log(result);

先执行 obj 的定义, 里面的 b 直接调用了函数 fnthis 指向 window 全局对象, 此时 b 的值为 1 + 2 = 3

然后从调用分析, obj.fn() 的形式是 对象.方法()this 指向 obj

// 此时的 obj
obj = {
    a: 3,
    b: 6,
    fn: fn
}

最后形成了 对象.方法 形式, 则最后输出6.

// case 03
var c = 1
var obj = {
  a: function () {
    var c = 3
    return this.b 
  },
  b: function () {
    var c = 4
    document.write(this.c)
  },
  c: 2
}

var obj1 = obj.a()
obj1()

从调用分析, obj.a() 形式为 对象.方法()this 指向 obj 对象

则此时 a 方法里面的 return this.b 的值为 objb 是个方法.

再进行调用 obj1 则形如 函数()this 指向了全局 window 则此时的 c 是1, 而非函数里面的变量 4,

因此最后输出为1.

规则03: 类数组对象 数组[下标] (), this 指向该类数组

当类数组里面的元素是 function 时,里面的 this 指向该类数组.

// case 01
var arr = ['a', 'b', 'c', function () {
  console.log(this[0]);
}]

arr[3]()

从调用分析, arr[3] () 满足形如 数组[下标] () 的形式, 则 this 指向该数组 arr, 最终输出 'a`.

对于类数组对象, 即所有键名为自然数序列 (从 0 开始), 且有 length 的对象, 最常见的是 arguments .

// case 02
function fn() {
  arguments[3]()
}

fn('a', 'b', 'c', function () {
  console.log(this[1]);
})

从调用分析, arguments[3] () 满足形如 数组[下标] () 的形式, 则 this 指向 arguments 即 fn 在调用时传递的实参数组, 下标1则输出 'b'.

// case 03
var a = 6
var obj = {
  a: 5,
  b: 2,
  c: [ 1, a, function () { document.write(this[1])} ]
}

obj.c[2]()

从调用分析, obj.c 是一个数组, 然后再进行下标调用, 即形如 数组[下标] () 的形式, this 指向数组本身.

下标为1 则指向了数组里面的 a , 这里指向了全局变量 a , 则最后输出了 6.

规则04: IIFE中的函数, this 指向 window 对象

IIFE 表示立即执行函数, 定义后立即调用. 这在项目中经常用于在页面加载完后, 立即执行获取后端接收的数据方法, 定义 + 调用 的方式来渲染页面.

// 写法1: 将函数用 () 包裹起来再调用 ()
(function () {
   console.log(123)
})();


// 写法2: 函数前面加 void, 最后再调用
void function fn() {
  console.log(123)
}()

// case 01
var a = 1
var obj = {
  a: 2,
  fn: (function () {
    var a = this.a
    return function () {
      console.log(a + this.a);
    }
  })() // IIFE, this 指向 window
}

obj.fn() // 对象.方法 this 指向 obj

从调用分析,

obj.fn 是一个立即执行函数, 会先执行, 此时的 this 指向全局 window, 则闭包里面的 this.a 为外面的 1.

obj.fn() 形如 对象.方法() ,此 this 指向 obj, 则 fn 返回的函数里面的 this.a 的值为 obj.a 的值为 2,

因此最后输出了3.

规则05: 用定时器, 延时器调用函数, this 指向 window 对象

通常在做一些异步任务 如想后端请求数据啥的, 就容易改变 this 指向, 通常的操作是可以用一个别的变量如叫 that 或者 self 来先指向 this 以保证 this 的指向不会改变. 当然这些前提是, 咱们能识别问题.

  • setInterval (函数, 时间)
  • setTimeout(函数, 时间)
// case 01
var obj = {
  a: 1,
  b: 2,
  fn: function () {
    console.log(this.a + this.b);
  }
}

var a = 3
var b = 4

setTimeout(obj.fn, 2000)

从调用分析, obj.fn 是一个函数, 然后外面被延时器调用, 2秒后执行, 则此时的 this 指向全局 window

则最后输出为7.

这里可以进行一个调用变化.

// case 02
var obj = {
    a: 1,
    b: 2,
    fn: function () {
      console.log(this.a + this.b);
    }
  }

  var a = 3
  var b = 4

  setTimeout(function () {
    obj.fn() // 这里真正调用
  }, 2000);

从调用分析, 这里的 setTimeout 并不是调用了函数, 只是将整体延迟了 2秒.

而真正调用函数的是 obj.fn() 形如 对象.方法 , 则 this 指向的是 obj, 则最后输出的是 3.

规则06: 事件处理函数, this 指向绑定事件的 DOM

Dom元素.onclick = function () { }

比如要实现一个效果, 当我们点击哪个盒子, 哪个盒子就变红, 要求使用同一个事件函数处理实现, 但不能用事件委托的方式.

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    div {
      float: left;
      margin-right: 10px;
      width: 200px;
      height: 200px;
      border: 1px solid #000;
    }
  </style>
</head>

<body>
  <div id="box1">box1</div>
  <div id="box2">box2</div>
  <div id="box3">box3</div>

  <script>
    function setColorRed() {
      // 这里的 this 指向前面绑定的 DOM 
      this.style.backgroundColor = 'red'
    }

    var box1 = document.getElementById('box1')
    var box2 = document.getElementById('box2')
    var box3 = document.getElementById('box3')

    box1.onclick = setColorRed
    box2.onclick = setColorRed
    box3.onclick = setColorRed

  </script>
</body>

</html>

注意这里的 this 是指向当前绑定的元素, 而 e.target 指的是内层触发的元素, 这俩不一样哦.

再对上面的案例做一个升级: 点击哪个盒子, 哪个盒子在 2秒 后就变红, 也是要求用一个事件函数来实现哦.

这咋一看似乎蛮简单:

function setColorRed() {
      // 直接放延迟函数是不行的, 
      // 因为它的 this 指向从当前 dom 变成了 window
      setTimeout(function () {
        this.style.backgroundColor = 'red'
      })
    }

这样其实是不行的, 因为又之前的规则5所知, 在延时器调用函数时, 里面的 this 指向的是 window对象.

这里最常用的一个巧妙办法是:

  • 先用一个变量比如叫 self 来保存原来的 this 指向的是 Dom, 进行备份
  • 然后在延时器中, 用 self 来替代 this 即可, 这样还是满足规则6的
function setColorRed() {
    // 这里的 this 指向当前 dom
    var self = this 
    setTimeout(function () {
    // 用 self 替换 this, 因为这里的 this 会指向 window 
    self.style.backgroundColor = 'red'
    }, 2000)
}

函数的 call 和 apply 方法能指定 this

在 js 中数组和函数都是对象 object , 既然是对象, 那就会有一些原型上的方法, 这里的 call / apply 作为函数对象的方法, 其功能是能指定函数的上下文 this

比如要统计语数英成绩, 对每个小朋友, 比如油哥:

var youge = {
  chinese: 80,
  math: 70,
  english: 60
}

有一个统计成绩的函数 sum

function sum() {
  console.log(this.chinese + this.math + this.english);
}

这两个怎么进行关联呢, 简单的方式将 sum 写进 youge 对象, 然后构造出 对象.方法() 的方式, 则 this 指向该对象.

var youge = {
  chinese: 80,
  math: 70,
  english: 60,
  sum: function () {
      console.log(this.chinese + this.math + this.english);
  }
}

youge.sum() // 对象.方法() this 指向 对象, 输出210

但 js 提供了更简单的方法, 即可通过 call 或者 apply 方法直接指定函数对象的 this

function sum() {
  console.log(this.chinese + this.math + this.english);
}

var youge = {
  chinese: 80,
  math: 70,
  english: 60
}

sum.call(youge)
sum.apply(youge)

这俩的主要区别在于当函数有额外的参数时候:

  • sum.call(youge, 1, 2, 3) 额外的参数是逗号展开的
  • sum.apply(youge, [1, 2, 3) 额外的参数是拼装成数组的

比如上例还有额外的竞赛甲方需要补充, 则便可用额外的参数啦.

function sum(a, b) {
  console.log(this.chinese + this.math + this.english + a + b);
}

var youge = {
  chinese: 80,
  math: 70,
  english: 60
}

// 额外的两项加分
sum.call(youge, 30, 10)
sum.apply(youge, [30, 10]) 

如果额外参数传递正确的话, 那都会输出总分是 250 啦.

上下文规则总结

规则 上下文 this指向
对象.方法() 对象
函数() window
数组[下标] () 数组
IIFE window
定时器 / 延时器 window
Dom事件处理函数 绑定 Dom 的元素
call / apply 任意指定
用 new 调用函数 秘密创建出的对象

最后一条是关于构造函数的, 这里就不展开了, 记住 this 指向是 new 出来的对象就行啦.

最关键的一条是要去动态分析函数的调用过程来确定上下文, 而非看定义哦.

posted @ 2024-04-06 18:34  致于数据科学家的小陈  阅读(6)  评论(0编辑  收藏  举报