Js 中的 this 指向

前面学习了 js 作用域和闭包, 都是比较不好理解的点, 那简单来说就是, js 的作用域, 主要针对函数, 它是在定义的时候, 在语法解析阶段就确定了的, 和函数的调用没有关系, js 寻找变量的方式就是沿着作用域链查找.

而 闭包表现为一个函数可以引用它的父级作用域中的自由变量, 即便父级已经看上去被 "销毁", 这个设计在 js 中一级被内置了, 因为在定义函数的时候, js 引擎就已经确定了函数及其父级作用域的自由变量, 我们称为 变量环境 VE. 单从定义角度说, js 的所有函数都是 闭包, 因为它都具备访问父级作用域的能力, 但严格来说, 它虽然有宝剑, 但不用的话, 那我们也不认为它是闭包.

本篇开始来将 js 中的 this 指向, 也是很让人头大, 它的方式和作用域恰好相反, 即 js 中的 this 指向 由调用方动态决定 , 这就得具体情况具体分析啦.

特此说明, 本篇涉及的代码案例都来之 b 站博主 coderwhy 大佬公开视频整理

为啥需要this

在常见的面向对象语言中, 几乎都有 this 关键子, 通常在类的方法中, this 指向调用对象本身. 而在 js 中的 this 则显得更加灵活和强大, 以至于很难理解有时候.

// this 可以用 对象名替代 (闭包)

var obj = {
  name: 'youge',

  // 方法
  eating: function() {
    console.log(obj.name + ' is eating')
  },

  running: function() {
    console.log(obj.name + " is running")
  }
}

obj.eating()
obj.running()

这样通过 对象名.属性/方法() 是可以访问到对象本身的, (因为是闭包嘛, 能访问父级作用域)

但是如果我们还有类似的对象, 这样就存在大量重复代码的编写了.

// 用对象.属性/方法 则大量重复修改
var obj2 = {
  name: 'youge',

  // 方法
  eating: function() {
    console.log(obj2.name + ' is eating')
  },

  running: function() {
    console.log(obj2.name + " is running")
  }
}

obj2.eating()
obj2.running()

只是一个小小的对象名改动, 则需将涉及的名字都要改一边, 就很麻烦.

但这时, 用 this 的话就非常高效率了.

// 用 this 则内部引用对象不用修改, 直接操作实例对象即可

var obj3 = {
  name: 'youge',

  // 方法
  eating: function() {
    console.log(this + ' is eating')
  },

  running: function() {
    console.log(this.name + " is running")
  }
}

obj3.eating()
obj3.running()

可见在 js 中, 用 this 最直观的一个好处是它能显著提高我们的编码效率, 但是呢, this 指向谁, 却变得非常难以理解.

this 在全局作用域下, 指向 window / {}

在大多情况下, this 都是出现在函数中, 但其实 this 也能出现在全局作用域中.

// 全局下, 浏览器中 this 指向 window 对象
// Node中, this 指向为 {} 空对象
 console.log(this) 

在浏览器环境下, 这个全局对象就是 window 对象, this 指向 window; 在 Node 环境中 this 指向为空对象.

这个和函数调用是有关系的, 在 node 中每个文件都会被当做一个 模块, 在模块加载, 编译 时会将他们都放到一个函数, 然后再执行这个函数, 通过 函数.call({}) 的方式, 此时它默认传递的就出一个空对象, 因此这时的 this 就指向了空对象. (Node 源码就是这么写的, 也没啥为什么啦)

this 在函数中的指向, 由调用情况而定

在大多数的场景中, 几乎很少在全局作用域中使用 this, 通常都是在函数中调用.

通过之前的作用域学习, 我们知道所有的函数在被调用时, 都会创建一个 执行上下文 FEC, 这个上下文记录着 函数调用栈, AO 等信息, 这里还有补充一点, this 也是其中的一条记录, 但它的指向确是 动态的, 是函数执行时确定的, 而不是编译的时候确定的, 哎呀, 就很折磨人, 不像作用域是编译阶段确定的.

同一函数的不同 this 指向

// 同一函数的不同调用方式, this 指向不同
// this 指向和函数调用位置有关, 和定义位置无关

function foo() {
  console.log(this)
}

// 1. 直接调用, this 指向全局
foo()

// 2. 对象中调用, this 指向对象本身
var obj = {
  name: 'youge',
  foo: foo
}

obj.foo()

// 3. 通过 apply 调用, this 指向传入的对象
foo.apply('abc')

可以很直观得到一些启示:

  • 函数在调用时, js 会默认给 this 一个绑定值
  • this 的指向, 和定义的位置无关, 和被调用的位置有关
  • this 是在运行时 动态绑定的, 而作用域是在定义编译阶段确定的

this 的绑定规则大致有如下几种:

  • 规则01: 默认绑定 (函数独立执行)
  • 规则02: 隐式绑定 (有调用主体)
  • 规则02: 显示绑定 (call, apply, bind)
  • 规则04: new 绑定 (对象)
  • ...

规则1: 默认绑定 - window

当函数独立调用时, 即函数没有被绑定到某个对象时, this 指向全局对象. 在 浏览器中就是 window, 在 node 中就是 GO 对象了.

// case1

function foo() {
  console.log(this) // window
}

foo() // window
 // case2
function foo1() {
  console.log(this) 
 }

 function foo2() {
  console.log(this)
  foo1() // 独立
 }

 function foo3() {
  console.log(this)
  foo2() // 独立
 }

 
 foo3() // 都是独立函数调用 -> window

// case3 

var obj = {
  name: 'youge',
  foo: function() {
    console.log(this)
  }
}

var bar = obj.foo
bar()  // 独立调用 -> window

// case4

function foo() {
  console.log(this)
}

var obj = {
  name: 'youge',
  foo: foo
}

var bar = obj.foo 
bar() // 独立调用 -> window
 // case05

 function foo() {
  return function () {
    console.log(this)
  }
 }

 var fn = foo()
 fn() // 独立调用 -> window 

注意理解过程, 而非瞎记一些错误结论, 比如 闭包里面的 this 就是 window, 这个不一定的.

 // case06

 function foo() {
  return function () {
    console.log(this)
  }
 }

 var fn = foo()

 var obj = { 
  name: 'youge', 
  eating: fn
 }

obj.eating() // 这里指向 obj, 隐式绑定

因此, 对于默认绑定来说, 理解什么是独立调用则非常关键.

规则02: 隐式绑定 - obj

当函数被调用的位置, 是通过某个对象进行调用的, 即 obj.fn() 这时的 this 指向该对象.

// case1

function foo() {
  console.log(this)
}

// 独立调用 - window
// foo()

var obj = {
  name: 'youge',
  foo: foo 
}

obj.foo() // 隐式调用, this -> obj 


隐式调用特征相对明显, 对象.函数() 那这个函数里面的 this 就是指向这个 对象啦.

// case2

var obj = {
  name: 'youge',
  foo: function () {
    console.log(this.name + ' is foo')
  }
}

obj.foo() // 隐式绑定, this -> obj 
var foo = obj.foo 
foo() // 独立调用, this -> window

这个 obj.fn() 时, js 引擎会自动将函数中的 this 指向为该对象, 这就是这样设计的.

// case3

var obj1 = {
  name: 'obj1',
  foo: function() {
    console.log(this)
  }
}

var obj2 = {
  name: 'youge',
  bar: obj1.foo
}

obj2.bar() // 隐式绑定, this -> obj2

不管这样变化, 只是函数是被 对象.函数() 调用的, 那函数里面的 this 就是指向该对象.

显示绑定 - call / apply

通常是针对 call / apply / bind 这几个手动绑定 this 的场景而言的.

隐式绑定的前提条件是, 必须在调用对象内部有一个对函数的引用, 若没有, 则在调用时, 会报找不到该函数的错误,

而通过这样的正确引用方式, 间接地将 this 绑定到该对象.

若不愿在函数内部包含这个函数的引用, 却又希望在该对象上强制调用, 则可用内置 call / apply / bind 方法.

函数在 js 中也是一个特殊的对象, 因此函数也有方法

foo = new Function(); foo.call(); foo.apply()

function foo() {
  console.log('foo 函数被调用')
}

// 直接调用, this -> window
foo()

// 方法调用, foo 是一个对象, this -> 可自控
foo.call()
foo.apply()
foo.bind()

区别在于直接调用时, 函数的 this 指向的是全局对象 (window), 而通过方法调用则是可以传参控制 this 指向的

var obj = {
  name: 'obj',
  // 不想这里引用 foo 时, 必须将其添加为一个属性
  foo: foo // 这样就不用写了
}

foo.call(obj) // this -> obj
foo.apply(obj) // this -> obj

补充一下 call 和 this 的区别, 就是函数的参数传递不同:

  • Call: 拆分单个参数传递, 逗号隔开
  • Apply: 数组传递, 将参数拼接为一个数组传递
// call, apply 的区别

function sum(a, b, c) {
  console.log(a + b + c)
}

// call: 参数逐个传递
sum.call('', 1, 2, 3)    // 6

// apply: 参数数组传递
sum.apply('', [1, 2, 3]) // 6

这种通过 函数对象的 call/apply 方法调用时, 第一个参数传递的就是要让 this 指向的对象, 这个绑定则称为显示绑定啦.

还有一种显式的绑定, 是通过函数对象的 bind 方法.

// bind

function foo() {
  console.log(this)
}

// 重复调用则相对麻烦
foo.call('obj')
foo.call('obj')
foo.call('obj') 

// 显示绑定的优先级 > 默认绑定
var newFoo = foo.bind('aaa') // aaa
newFoo()

规则04: new 绑定 指向新对象

在 js 中, 函数可以当做一个类的构造函数来使用, 即用 new 关键子来实例一个对象, 此时 this 指向该实例对象.

js 中的 new 关键字调用构造函数Fn, 会做如下4个事情:

  • 先创建一个全新的对象 obj = {} 或者 new Object()
  • 让对象内置的 [[prototype]] 指向 **构造函数的显示 prototype **, 即: obj.__ proto __ == Fn.prototype
  • This 绑定为当前创建的对象
  • 执行函数代码, 若函数无返回其他对象, 则返回这个刚创建的对象

这些都是 js 引擎内部帮我们做的, 就了解学习即可, 理解的关键点是要去明白原型链的东西, 这个后面再细说吧.

理解 js 原型的关键点:

a. 每个对象都有一个内置的 [[prototpype]] 指针, 指向其原型对象

b. 每个函数既是函数, 也是一个特殊的对象 (二义性)

c. 每个函数对象都有一个显示的 prototype 属性 (指针)

d. 被构造函数 new 出来的对象 obj, 其 obj 的 [[prototype]] === 构造函数.prototype

有点逻辑推理的感觉, 但我觉得它真的设计非常巧妙和高级, 而且逻辑自洽, 有点厉害.

// 函数可以被 new 调用, 生成对象 (构造器)

function Person(name, age) {
  
  // 1. 自动创建一个对象 obj = {}
  // 2. this 绑定这个 obj;  this = obj 

  this.name = name 
  this.age = age 

  // 3. obj.__proto__ === Person.prototype
  // 3. return this 
}

var p1 = new Person('youge', 20)
var p2 = new Person('yaya', 18)

console.log(p1) // Person { name: 'youge', age: 20 }

在 js 中没有其他面向对象语言的 class 设计, 就是通过函数模拟, 函数原型链来转换实现的啦. 当我们通过一个 new 关键字来调用一个函数时 (构造器), 这时的 this 是在调用这个构造器函数创建出来的对象, 这个过程就是 new 绑定.

内置函数的 this

有的时候我们会调用一些 js 的内置函数, 或者第三方库的内置函数, 它通常会要求:

  • 内置函数要求传入另外一个函数 (函数作为参数)
  • 执行过程通常是自动的, 内部细节通常会被隐藏

这种情况下的 this 到底指向什么就不太好理解了, 因为如果看不到它的实现原理的话, 就只能硬猜啦.

// setTimeout 中的 this -> window ?

setTimeout(function() {

}, 2000)

// 内部实现, 隐藏的
function cjSetTimeout(fn, duration) {
  // 会调用传入的函数, 但不知道具体咋调用的
  // 这就很难判断 this 了

  // 1. 独立调用 -> this: window
  fn()

  // 2. 显示绑定 -> this: 'abc'
  fn.call('abc')
}

对于 setTimeout 来说, 里面传入的函数调用, 从现象去反推, 就是函数独立调用的, 因此 this 指向就是 window .

继续, 对于标签元素的一下事件监听操作等, 这个 this 通常指向该事件对象.

<!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>
    .box {
      width: 200px;
      height: 200px;
      background-color: gold;
    }
  </style>
</head>
<body> 
  <div class="box"></div>

  <!-- 监听点击 -->
  <script>
    var boxDiv = document.querySelector('.box')
    boxDiv.onclick = function() {

      console.log(this)
    }

    // 多个事件函数, 内部会用数组存起来调用
    boxDiv.addEventListener('click', function() {
      // fn.call(boxDiv)
      console.log(this)
    })

    boxDiv.addEventListener('click', function() {
      console.log(this)
    })

    boxDiv.addEventListener('click', function() {
      console.log(this)
    })
  </script>
</body>
</html>

继续, 对于 数组.forEach/map/filter/find 等高阶函数来说, 传入函数的 this 通常指向其迭代元素.

// 数组高阶函数方法, this -> 被迭代元素

var names = ['youge', 'cj', 'yaya']
names.forEach(function(item) {
  console.log(item, this)
}, 'abc')

规则优先级

回顾前面的四条规则, 默认绑定(独立调用), 隐式绑定(对象.函数), 显示绑定 (call / apply), 和 new 绑定, 当一个函数的调用位置同时对应了多条规则, 那优先级如何呢.

  • 默认规则优先级最低
  • 显示绑定 > 隐式绑定
  • new 绑定 > bind > 显示绑定

New 绑定 和 call, apply 是不会出现在同时的, 因此不存在优先级比较, 但 new 绑定可以和 bind 一起用, new 的优先级会更一些.

// 显示绑定 > 隐式绑定

var obj = {
  name: 'youge',
  foo: function() {
    console.log(this)
  }
}

// 隐式绑定 -> obj
obj.foo()

// 显示绑定 -> cj
obj.foo.call('cj')  // cj
obj.foo.apply('cj')

// bind
var bar = obj.foo.bind('yaya') // yaya
bar()


// bind 优先级高
function foo() {
  console.log(this)
}

var obj2 = {
  name: 'yg',
  foo: foo.bind('yg') // yg
}

obj2.foo()

很容易得出显示绑定的优先级, 高于 隐式绑定啦.

// new 绑定优先级 > 隐式绑定

var obj = {
  name: 'youge',
  foo: function() {
    console.log(this)
  }
}

// new > 隐式 (obj.fn())

var f = new obj.foo()  // foo {}

可以得出 new 绑定也是高于 隐式绑定的.

那 new 绑定 和 显示绑定, 孰高? 但 new 关键字是不能和 call / apply 一起来使用的, 二者都会调用函数, 冲突.

但是 bind 还是能比较的.

// new 绑定 > call/apply

function foo() {
  console.log(this)
}

var bar = foo.bind('cj')

// 此时的 bar 函数是 绑定了 cj 的, 然后再来 new 一下看看变化
// 输出 foo, 说明 new > bind

var obj = new bar()

规则之外

上面讨论的场景几乎已经能应付平时的大多场景了, 但还是有一些特例出现, 是跳出规则之外的, 常见的大致有这几种情况:

  • 显示绑定中, 传输 null / undefined, 会变成 默认规则
  • 间接函数引用, 会使用默认规则
  • 箭头函数则没有 this, 需要从父级作用域找
// 显示绑定, 传入 null/undefined -> window

function foo() {
  console.log(this)
}

// 显示绑定, 正常传参, this -> 传参
foo.call('cj') // cj
foo.apply({})  // {}

// 显示绑定, 传 null / apply, this -> window
foo.call(null)  // window
foo.apply(undefined) // window

var bar = foo.bind(null)
bar()  // window

创建一个函数的 间接引用 这时也会使用默认绑定规则.

// 间接函数引用 -> window

var obj1 = {
  name: 'obj1',
  foo: function() {
    console.log(this)
  }
}

var obj2 = {
  name: 'obj2'
};
 
// 间接绑定, 正常人应该不会这么干
// obj2.bar = obj1.foo
// obj2.bar() // obj2
 

// 这里调用 js 引擎会作为独立函数调用 -> window
(obj2.bar= obj1.foo)()

在箭头函数中, 直接没有了 this, 得从父级作用域去找.

  • 箭头函数不会绑定 this, arguments 属性
  • 箭头函数不能作为构造函数来使用 (不能用 new 哦)

可以这样说, 箭头函数的出现, 就是为了解决这个 令人迷惑的 this 问题, 顺带简化一下代码, 提高编程体验.

// 箭头函数写法优化

var nums = [10, 30, 15]

// 1. 如果只有一个参数, () 可省略
nums.forEach(item => {
  console.log(item)
})

// 2. 如果只有一行执行体, {} 可省略, 结果自动返回值
nums.forEach(item => console.log(item))

var newNums = nums.filter(num => num % 2 === 0)
console.log(newNums) // [10, 30]

// map/filter/reduce 进行链式组合调用, spark 中用的很多
var arr = nums.filter(num => num % 2 === 0)
  .map(num => num * num)
  .reduce((preValue, curValue) => preValue + curValue)

console.log(arr) // 1000

// 3. 如果执行体只有返回一个对象, 则需加 () 包裹

// js 中 对象, 函数体都是 {}, 包裹的话方便引擎解析区分
var bar = () => ({ name: 'youge', age: 18})
console.log(bar())

回来这个 this 的问题, 结论是, 箭头函数不绑定 this, 而是根据其外层作用域去找 this 决定 (作用域链)

// 箭头函数中, 不绑定 this

var foo = () => { 
  console.log(this)
}

// 不论怎么调用 foo 箭头函数, this 都不绑定, 找父级
foo()

// 都是 window / {}
var obj = {foo: foo}
obj.foo()
foo.call('abc')

箭头函数的好处, 举个最常见的应用场景: 发网络请求:

// 箭头函数优化-前: 发网络请求

var obj = {
  data: [],
  getData: function() {
    // 发异步网络请求, 将结果放到上面的 data 属性中
    setTimeout(function(){
      var res = [1, 2, 3]
      // 绑定到属性: 这样写是错误的, 这里的 this 是指向 全局
      this.data = res 
    }, 2000);
  }
}

obj.getData()

之间说过, 在 setTimeout() 中传入函数是会被当做独立函数调用, 因此, 这里的 this 指向的是 window 而非期望的 obj 对象.

this.data = res 等价于 window.data = res

没有箭头函数之前的解决办法就在 this 会改变之前, 先用一个变量, 比如 that 存下 this, 此时的 this 是指向 obj 的.

然后在后续的调用中, 用 that 来进行操作即可.

var that = this // 这里的 this 是 obj

setTimeout(function(){
  var res = [1, 2, 3]
  
  that.data = res 
}, 2000);

从 es6 引入箭头函数之后, 就不存在这个 this 指向问题了, 它直接会去父级作用域中找.

// 箭头函数优化后: 发网络请求

var obj = {
  data: [],
  getData: function() {
    // 发异步网络请求, 将结果放到上面的 data 属性中
    setTimeout(() => {
      var res = [1, 2, 3]
      // 这里的 this 在箭头函数中, 它会去上级作用域 obj 中找
      this.data = res 
    }, 2000);
  }
}

obj.getData()
 

this 练习题

这里先空着吧, 后续再补充一下.

至此, 关于 js 中的 this 指向就基本清晰了, 小结一下就是 默认绑定, 隐式绑定, 显示绑定, new 绑定, 内置函数绑定 , 绑定优先级, 规则之外的 null , 间接函数调用, 还有箭头函数无 this 等这些内容, 就还是有点麻烦的, 当他们组合在一起看的时候, 有点晕头转向, 但也只能这样啦.

posted @ 2025-08-02 23:46  致于数据科学家的小陈  阅读(18)  评论(0)    收藏  举报