首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >浏览器原理学习笔记02—浏览器中的JavaScript执行机制

浏览器原理学习笔记02—浏览器中的JavaScript执行机制

原创
作者头像
CS逍遥剑仙
发布于 2020-05-02 13:57:50
发布于 2020-05-02 13:57:50
1.5K1
举报
文章被收录于专栏:禅林阆苑禅林阆苑

浏览器原理学习笔记02—浏览器中的JavaScript执行机制

Write By CS逍遥剑仙 我的主页: www.csxiaoyao.com GitHub: github.com/csxiaoyaojianxian Email: sunjianfeng@csxiaoyao.com

1. JavaScript 代码的执行流程

1.1 编译阶段

输入代码经过编译后会生成两部分内容:执行上下文 和 可执行代码。

执行上下文(Execution context)是 JavaScript 执行一段代码时的运行环境,比如调用一个函数,就会进入这个函数的执行上下文,确定该函数在执行期间用到的 this、变量、对象以及函数等。

编译阶段代码中的变量和函数会被存放到执行上下文中的 变量环境对象 中,即变量提升(Hoisting)。

Tips: 之所以执行变量提升,是因为js代码需要被编译 变量提升,变量默认值初始化为 undefined 出现同名函数,后面的函数会覆盖前面的函数 如果变量和函数同名,编译阶段变量的声明会被忽略(函数优先级高于变量)

如果出现变量和函数同名,下面的 demo 输出 1。

代码语言:txt
复制
showName()
function showName () { console.log(1) }
var showName = function () { console.log(2) }

1.2 执行阶段

在代码执行阶段 JavaScript 引擎会从变量环境中去查找自定义的变量和函数。

2. JavaScript 调用栈

2.1 创建执行上下文的场景

  • 执行全局代码,创建页面生存周期内 唯一 的 全局执行上下文
  • 函数调用,创建函数执行上下文,函数执行结束后销毁
  • 使用 eval 函数时,eval 代码被编译并创建执行上下文

2.2 调用栈执行过程

调用栈(call stack)是用于管理执行上下文的数据结构,符合后进先出的规则,是 JavaScript 引擎追踪函数执行的一个机制。

代码语言:txt
复制
var a = 2
function add (b, c) {
  return b + c
}
function addAll (b, c) {
  var d = 10
  result = add(b, c)
  return  a + result + d
}
addAll(3, 6)

2.3 利用浏览器查看调用栈信息

有两种方式:debug 和 console,此处 (anonymous) 即全局执行上下文。

2.4 栈溢出(Stack Overflow)

调用栈是有大小限制的,当入栈的执行上下文超过一定数目 JavaScript 引擎就会报错,尤其在递归时很容易出现栈溢出,可以通过将递归调用改成其他形式,或使用定时器将任务拆解等方式来避免栈溢出。

3. 作用域(scope)

ES6 之前作用域只有两种:全局作用域 和 函数作用域,不支持 块级作用域 (即大括号包裹的代码,如函数、判断语句、循环语句,甚至单独的一个{}),ES6 引入了 let 和 const 支持了 块级作用域。

代码语言:txt
复制
function foo(){
    var a = 1
    let b = 2
    {
      let b = 3
      var c = 4
      let d = 5
      console.log(a) // 1
      console.log(b) // 3
    }
    console.log(b) // 2,若使用var声明,则输出3
    console.log(c) // 4
    console.log(d) // Uncaught ReferenceError: d is not defined
}   
foo()

块级作用域借助 词法环境 的栈结构来实现,而变量提升通过变量环境来实现。使用了 let 和 const 之后的调用栈执行过程也会发生变化。var 声明的变量编译阶段全都被存放到 变量环境 里,通过 let 声明的变量编译阶段会被存放到 词法环境(Lexical Environment) 中。

单个执行上下文中的变量查找见下图中步骤 3,而块级作用域(跨执行上下文)的变量查找见4.3。

4. 作用域链和闭包

4.1 作用域连

代码语言:txt
复制
function bar() {
    console.log(myName) // 此时的 myName 使用"全局上下文"还是使用"foo函数上下文"中的值?
}
function foo() {
    var myName = "1"
    bar()
}
var myName = "2"
foo() // 2

虽然调用栈中,从栈顶向下查找理论上应该是使用 foo 函数上下文的,但实际 Bar 中的 myName 应该使用全局上下文的, JavaScript 执行过程中的 作用域链是由词法作用域决定的,而词法作用域是代码阶段决定的,和函数调用没有关系,词法作用域后面详解。bar 函数的作用域链上级是全局作用域,这是由 bar 函数在代码中的位置决定的,和在 foo 函数或其他位置调用无关。

在每个执行上下文的变量环境中,都包含了一个外部引用 outer 指向外部的执行上下文,查找变量时首先会在当前执行上下文中查找,若未找到则继续在 outer 所指向的执行上下文中查找(如查找 myName 变量时在 bar 函数执行上下文中未找到,则在 outer 指向的全局执行上下文中查找)。

4.2 词法作用域确定 outer 指向

词法作用域(静态作用域) 由代码结构(代码中函数声明的位置)决定,和函数调用关系无关。

4.3 块级作用域中(跨执行上下文)的变量查找

单个执行上下文的变量查找上一节已经叙述,若当前执行上下文中未找到变量,则会沿 作用域链 查找,图中按照1,2,3,4,5的顺序最终在全局执行上下文中找到 test = 1。

4.4 闭包

4.4.1 定义

在 JavaScript 中,根据词法作用域规则,内部函数 总是可以访问其 外部函数 中 声明的变量,当通过调用一个外部函数(foo)返回一个内部函数(getName/setName)后,即使该外部函数已经执行结束了,内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量(test1/myName)的集合称为闭包(foo函数的闭包)。

代码语言:txt
复制
function foo() {
    var myName = "1"
    let test1 = 1
    const test2 = 2
    var innerBar = {
        getName:function(){
            console.log(test1)
            return myName
        },
        setName:function(newName){
            myName = newName
        }
    }
    return innerBar
}
var bar = foo()
bar.setName("2")
bar.getName()
console.log(bar.getName())
4.4.2 使用

在开发者工具中可以看到,当调用 bar.getName 时,右边 Scope 项展示了从 "Local–>Closure(foo)–>Global" 的完整作用域链。Local 表示当前的 getName 函数的作用域,Closure(foo) 是指 foo 函数的闭包,Global 指全局作用域。JavaScript 引擎会沿着 当前执行上下文–>foo 函数闭包–>全局执行上下文 的顺序来查找 myName 变量。

4.4.3 回收

闭包的回收取决于引用闭包的函数是全局变量还是局部变量,若使用频率不高而又占用内存较大,应尽量使其成为一个局部变量,以便使用完后 JavaScript 引擎自动垃圾回收。

5. 完整的执行上下文 & this

5.1 this 和 作用域链 的区别

作用域链查找变量的例子:

代码语言:txt
复制
var bar = {
    myName:"test bar",
    printName: function () {
        console.log(myName) // 作用域链查找 myName 变量
    }    
}
function foo() {
    let myName = "test foo"
    return bar.printName
}
let myName = "test outer"
let _printName = foo()
_printName() // test outer
bar.printName() // test outer

在对象内部方法中使用对象内部的属性是一个非常普遍的需求,但 JavaScript 的作用域机制并不支持,因此JavaScript 又设定了另外一套 this 机制。作用域链 和 this 是两套不同的系统,它们之间基本没联系。

代码语言:txt
复制
printName: function () {
    console.log(this.myName) // 通过 this 查找 bar 的属性 myName
}

5.2 this 和 执行上下文 的联系

完整的执行上下文中包含了 变量环境、词法环境、外部环境outer 和 this,this 是和执行上下文绑定的(每个执行上下文包含一个 this)。对应三种执行上下文:全局执行上下文、函数执行上下文和 eval 执行上下文,this 也只有三种——全局执行上下文中的 this、函数中的 this 和 eval 中的 this。

5.3 全局执行上下文中的 this

控制台中输入 console.log(this) 得到 window 对象,这也是 this 和作用域链的唯一交点,作用域链最底端包含了 window 对象,全局执行上下文中的 this 也是指向 window 对象。

5.4 函数执行上下文中的 this

函数执行上下文中 this 的指向与调用函数的调用方式相关,而与调用函数的代码位置无关。

5.4.1 默认直接调用函数
代码语言:txt
复制
function foo () {
  console.log(this)
  function bar () {
    console.log(this)
  }
  bar() // window
}
foo() // window

默认直接调用函数,无论在哪调用,this 都指向 window 对象。

5.4.2 通过函数的 call / apply / bind 方法设置 this
代码语言:txt
复制
let bar = {
  myName: "test1"
}
function foo(){
  this.myName = "test2"
}
foo.call(bar) // foo 函数内部的 this 指向 bar 对象
console.log(bar) // {myName: "test2"}
console.log(myName) // Uncaught ReferenceError: myName is not defined
5.4.3 通过对象调用方法设置 this
代码语言:txt
复制
var myObj = {
  name: "test", 
  showThis: function(){
    console.log(this)
  }
}
myObj.showThis() // 输出:myObj,通过对象调用方法
var foo = myObj.showThis
foo() // 输出:window,默认直接调用函数

使用对象来调用其内部的一个方法,该方法的 this 指向对象本身。通过 myObj 对象来调用 showThis 方法,最终输出的 this 指向 myObj,也可以认为 JavaScript 引擎在执行 myObj.showThis() 时,将其转化为:

代码语言:txt
复制
myObj.showThis.call(myObj)
5.4.4 通过构造函数设置 this
代码语言:txt
复制
function CreateObj(){
  this.name = "test"
}
var myObj = new CreateObj()

当执行 new CreateObj() 创建对象 myObj 时 JavaScript 引擎做了如下四件事:

  1. 创建一个空对象 tempObj
  2. 调用 CreateObj.call 方法,将 this 指向 tempObj 对象
  3. 执行 CreateObj 函数内容,此时 CreateObj 函数执行上下文中的 this 已经指向 tempObj 对象
  4. 最后返回 tempObj 对象

用代码可以表达为:

代码语言:txt
复制
var tempObj = {}
CreateObj.call(tempObj)
// ...
return tempObj

5.5 this使用技巧

5.5.1 技巧1:缓存 this 解决嵌套函数中的 this 继承

function 函数嵌套及调用让 this 的指向难以理解,可通过缓存 this 来解决,本质是把 this 体系转换为作用域体系。

代码语言:txt
复制
var myObj = {
  name: "test1", 
  showThis: function(){
    var self = this // 缓存 this
    function bar(){
      // this.name = "test2" // this 指向全局 window 对象
      self.name = "test2" // self 指向 myObj
    }
    bar()
  }
}
myObj.showThis()
5.5.2 技巧2:使用箭头函数避免创建执行上下文

ES6 中的箭头函数不会创建其自身的执行上下文,所以箭头函数中的 this 取决于它的外部函数,箭头函数 bar 里的 this 指向 myObj 对象。

代码语言:txt
复制
var myObj = {
  name: "test1", 
  showThis: function(){
    var bar = () => {
      this.name = "test2" // this 指向 myObj
    }
    bar()
  }
}
myObj.showThis()
5.5.3 技巧3:使用严格模式修改 this 默认指向

默认情况直接调用一个函数,this 指向全局对象 window,可以通过启用 JavaScript 的 严格模式 设置函数的执行上下文中的 this 默认值是 undefined。如果要让 this 指向某个对象需要通过 call 来显式调用。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 浏览器原理学习笔记02—浏览器中的JavaScript执行机制
    • 1. JavaScript 代码的执行流程
      • 1.1 编译阶段
      • 1.2 执行阶段
    • 2. JavaScript 调用栈
      • 2.1 创建执行上下文的场景
      • 2.2 调用栈执行过程
      • 2.3 利用浏览器查看调用栈信息
      • 2.4 栈溢出(Stack Overflow)
    • 3. 作用域(scope)
    • 4. 作用域链和闭包
      • 4.1 作用域连
      • 4.2 词法作用域确定 outer 指向
      • 4.3 块级作用域中(跨执行上下文)的变量查找
      • 4.4 闭包
    • 5. 完整的执行上下文 & this
      • 5.1 this 和 作用域链 的区别
      • 5.2 this 和 执行上下文 的联系
      • 5.3 全局执行上下文中的 this
      • 5.4 函数执行上下文中的 this
      • 5.5 this使用技巧
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档