深入浅出JavaScript执行机制(下)

前言

本文是【JavaScript专栏】系列文章。

前面两篇文章 深入浅出JavaScript执行机制(上)深入浅出JavaScript执行机制(中) 已经介绍了 【变量提升、JS 代码执行流、调用栈、块级作用域、作用域链、词法作用域、闭包】 等相关工作原理。本文将从 JavaScript 执行上下文的视角讲一下 this

引言

先看下一段代码,它的执行结果是什么:

js 复制代码
var bar = {
    myName: "juejin.cn",
    printName: function () {
        console.log(myName)
    }    
}
let myName = "juejin"
bar.printName()

根据前两篇文章的介绍,相信大家已经知道结果了,在 printName 函数里面使用的变量 myName 是属于全局作用域下面的,所以最终打印出来的值都是"juejin"。这是因为作用域链是由词法作用域决定的,而词法作用域是由代码结构来确定的。

但是按照常理来说,调用 bar.printName() 时,访问到的 myName 变量应该是 bar 对象中的,因为这是一个整体,大多数面向对象语言都是这样设计的。面向对象语言中:在对象内部的方法中使用对象内部的属性是一个非常普遍的需求 ,但是 JavaScript 的作用域机制并不支持这一点,基于这个需求,JavaScript 又设计出来另外一套 this 机制。

为了实现在 printName 函数中访问到 bar 对象的 myName 属性, printName 函数可以这样改写:

js 复制代码
 printName: function () {
   console.log(this.myName)
 }  

注意一点:作用域链this 是两套不同的系统,它们之间基本没太多联系。

JavaScript 中的 this 是什么

前面说了,执行上下文中包含 变量环境、词法环境、外部环境(outer),其实还有个 this。

图中可以看出,this 是和执行上下文绑定的,也就是说每个执行上下文中都有一个 this。

执行上下文分为:全局执行上下文、函数执行上下文、eval 执行上下文。所以对应的 this 有:全局执行上下文中的 this、函数执行上下文中的 this、eval 中的 this。

全局执行上下文中的 this

在控制台中输入console.log(this)来打印出来全局执行上下文中的 this,最终输出的是 window 对象.

所以:全局执行上下文中的 this 是指向 window 对象的。这也是 this 和作用域链的唯一交点,作用域链的最底端包含了 window 对象,全局执行上下文中的 this 也是指向 window 对象。

函数执行上下文中的 this

js 复制代码
function foo(){
  console.log(this)
}
foo()

foo 函数内部打印出来 this 值,也是 window 对象,这说明在默认情况下调用一个函数,其执行上下文中的 this 也是指向 window 对象的。但是可以通过以下的方式去设置函数执行上下文中的 this 值。

1、通过函数的 call 方法设置 this

通过函数的 call 方法来设置函数执行上下文的 this 指向。 如下:

js 复制代码
let bar = {
  myName : "zhihu",
  test1 : 1
}
function foo(){
  this.myName = "juejin"
}
foo.call(bar)
console.log(bar)   // 'juejin'
console.log(myName) // Uncaught ReferenceError: myName is not defined

从输出结果中看出:foo 函数内部的 this 已经指向了 bar 对象。bar.myName = 'juejin',同时在全局执行上下文中打印 myName,提示该变量未定义。

当然还可以使用 apply、bind 方法可以设置函数执行上下文的 this。

2、通过对象调用方式设置 this

js 复制代码
var myObj = {
  name : "juejin", 
  showThis: function(){
    console.log(this)
  }
}
myObj.showThis()

通过 myObj 对象来调用 showThis 方法,最终输出的 this 值是指向 myObj。

所以:使用对象来调用其内部的一个方法,该方法的 this 是指向对象本身的。

再来看下这段代码:

js 复制代码
var myObj = {
  name : "极客时间",
  showThis: function(){
    this.name = "极客邦"
    console.log(this)
  }
}
var foo = myObj.showThis
foo()

发现 this 又指向全局对象 window。

所以:在全局环境中调用一个函数,函数内部的 this 指向的是全局变量 window。

通过构造函数设置 this

js 复制代码
function CreateObj(){
  this.name = "juejin"
}
var myObj = new CreateObj()

使用 new 关键字创建了对象 myObj,此时的构造函数 CreateObj 中的 this 到底指向了谁?

new CreateObj(),主要做了以下工作:

  • 创建一个新对象 obj
  • 将构建函数中的this绑定到新建的对象obj上;
  • 返回新对象 obj
js 复制代码
  var obj = {}
  CreateObj.call(obj)
  return obj

所以,此时的构造函数 CreateObj 中的 this 指向新对象本身。

this 的设计缺陷

分析一下以下代码:

js 复制代码
var myObj = {
  name : "juejin", 
  showThis: function(){
    console.log(this)
    function bar(){console.log(this)}
    bar()
  }
}
myObj.showThis()

可能会很自然地觉得,bar 中的 this 应该和其外层 showThis 函数中的 this 是一致的,都是指向 myObj 对象的,这很符合人的直觉.

但实际情况是:函数 bar 中的 this 指向的是全局 window 对象,而函数 showThis 中的 this 指向的是 myObj 对象。

所以这是 this 的一点设计缺陷:嵌套函数中的 this 不会从外层函数中继承。

可以通过两种方式去解决这个问题:

1、声明一个变量 self 用来保存 this

js 复制代码
var myObj = {
  name : "zhihu", 
  showThis: function(){
    console.log(this)
    const self = this
    function bar(){
      self.name = "juejin"
    }
    bar()
  }
}
myObj.showThis()
console.log(myObj.name)
console.log(window.name)

最终 myObj 中的 name 属性值变成了"juejin"。其实,这个方法的的本质是把 this 体系转换为了作用域的体系

2、使用箭头函数;

js 复制代码
var myObj = {
  name : "zhihu", 
  showThis: function(){
    console.log(this)
    const bar = ()=> {
      this.name = "juejin"
      console.log(this)
    }
    bar()
  }
}
myObj.showThis()
console.log(myObj.name)
console.log(window.name)

使用 ES6 中的箭头函数也能得到我们想要的结果,也就是箭头函数 bar 里面的 this 是指向 myObj 对象的。这是因为 ES6 中的箭头函数并不会创建其自身的执行上下文,所以箭头函数中的 this 取决于它的外部函数。

总结

  • 全局执行上下文中的 this,指向 window 对象;
  • 函数执行上下文中的 this:
    • 当函数被正常调用时,在严格模式下,this 值是 undefined,非严格模式下 this 指向的是全局对象 window;
    • 当函数作为对象的方法调用时,函数中的 this 就是该对象, 如: obj.foo() ;
    • 嵌套函数中的 this 不会继承外层函数的 this 值;
  • 设置函数中的 this:
    • 通过调用函数的 call、apply、bind 方法可以改变 this 的指向;
    • 通过对象调用方法来设置;
    • 通过构造函数来设置;
  • 箭头函数没有自己的执行上下文,所以箭头函数的 this 就是它外层函数的 this;
相关推荐
GISer_Jing8 分钟前
React 18的createRoot与render全面对比
前端·react.js·前端框架
我叫汪枫9 分钟前
React Hooks原理深度解析与高级应用模式
前端·react.js·前端框架
我叫汪枫9 分钟前
深入探索React渲染原理与性能优化策略
前端·react.js·性能优化
Ares-Wang15 分钟前
Vue3》》eslint Prettier husky
开发语言·javascript·ecmascript
阿智@1121 分钟前
推荐使用 pnpm 而不是 npm
前端·arcgis·npm
EveryPossible31 分钟前
静态箭头连线
开发语言·javascript·ecmascript
伍哥的传说32 分钟前
QRCode React 完全指南:现代化二维码生成解决方案
前端·javascript·react.js·qrcode.react·react二维码生成·qrcodesvg·qrcodecanvas
IT_陈寒37 分钟前
Vite 5.0 终极优化指南:7个配置技巧让你的构建速度提升200%
前端·人工智能·后端
listhi52041 分钟前
Map对象在JavaScript循环中的使用
开发语言·前端·javascript
安卓开发者1 小时前
鸿蒙Next Web组件生命周期详解:从加载到销毁的全流程掌控
前端