前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >浏览器工作原理 - 浏览器中的 JavaScript

浏览器工作原理 - 浏览器中的 JavaScript

作者头像
Cellinlab
发布2023-05-17 14:38:00
发布2023-05-17 14:38:00
67500
代码可运行
举报
文章被收录于专栏:Cellinlab's BlogCellinlab's Blog
运行总次数:0
代码可运行

变量提升

声明和赋值

代码语言:javascript
代码运行次数:0
运行
复制
var myname = 'cellinlab';

可以将上面代码看成两部分组成:

代码语言:javascript
代码运行次数:0
运行
复制
var myname; // 声明部分
myname = 'cellinlab'; // 赋值部分

对于函数来说:

代码语言:javascript
代码运行次数:0
运行
复制
function foo () {
  console.log('foo');
}

var bar = function () {
  console.log('bar');
}

函数 foo() 是一个完整的函数声明,没有涉及赋值操作;第二个函数,先声明了变量 bar,再把 function () {} 赋值给 bar。可以理解为:

变量提升

变量提升,是指在 JavaScript 代码执行的过程中,JavaScript 引擎将变量的声明部分和函数的声明部分提升到代码的顶部的“行为”。变量被提升后,会给变量设置一个默认值,默认值为 undefined

从字面上看,“变量提升”意味着变量和函数的声明会在物理层面移动到代码的前面。但是,实际上,并不是这样的。实际上变量和函数的声明在代码中的位置是不变的,而是在编译阶段被 JavaScript 引擎放入内存中

一段 JavaScript 代码在执行前需要被 JavaScript 引擎编译,编译完之后,才会进入执行阶段。

代码执行流程 之 编译阶段

代码语言:javascript
代码运行次数:0
运行
复制
showName();
console.log(myname);
var myname = 'cellinlab';
function showName() {
  console.log('showName called');
}

上面代码可以被拆成两部分来看,声明部分:

代码语言:javascript
代码运行次数:0
运行
复制
var myname = undefined;
function showName() {
  console.log('showName called');
}

执行部分的代码:

代码语言:javascript
代码运行次数:0
运行
复制
showName();
console.log(myname);
myname = 'cellinlab';

可以看出,输入一段代码,经过编译后,会生成两部分内容:执行上下文可执行代码

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

或者说,在执行上下文中存在一个变量环境的对象(Variable Environment),该对象中保存了变量提升的内容。

可以简单理解是这个样子的:

代码语言:javascript
代码运行次数:0
运行
复制
VariableEnvironment:
  myname -> undefined
  showName -> function () {
    console.log('showName called');
  }

变量环境对象生成的过程:

代码语言:javascript
代码运行次数:0
运行
复制
showName();
console.log(myname);
var myname = 'cellinlab';
function showName() {
  console.log('showName called');
}
  1. 在 line 1 和 line 2 ,由于这两行代码不是声明操作,所以 JavaScript 引擎不会做任何处理;
  2. 在 line 3 中,使用了 var 声明,因此 JavaScript 引擎将在环境对象中创建一个名为 myname 的属性,并将其初始化为 undefined
  3. 在 line 4 中,JavaScript 引擎发现一个通过 function 定义的函数,所以将函数定义存储到堆(Heap)中,并将函数的引用存储到环境对象中的 showName 属性中;

接下来,JavasScript 引擎会把声明以外的代码编译为字节码:

代码语言:javascript
代码运行次数:0
运行
复制
showName();
console.log(myname);
myname = 'cellinlab';

代码执行流程 之 执行阶段

JavasScript 引擎按顺序逐行执行“可执行代码”:

  1. 当遇到 showName 函数时,JavaScript 引擎便开始在变量环境对象中查找该函数,由于变量环境对象中存在该函数的引用,所以 JavaScript 引擎开始执行该函数,输出 showName called
  2. 接下来,输出 myname 的值,JavaScript 引擎在变量环境对象中查找该属性,找到 myname 且其值为 undefined,所以 JavaScript 引擎输出 undefined
  3. 接下来,将 'cellinlab' 赋值给 myname
代码语言:javascript
代码运行次数:0
运行
复制
VariableEnvironment:
  myname -> 'cellinlab'
  showName -> function () {
    console.log('showName called');
  }

同名变量和函数的处理

代码语言:javascript
代码运行次数:0
运行
复制
function showName() {
  console.log('cell');
}
showName();
function showName() {
  console.log('cellinlab');
}
showName();

  • 编译阶段,遇到第一个 showName 函数,引擎会将该函数存到变量环境对象中。当遇到第二个 showName 函数时,会继续存放,但是发现已经存在一个同名函数,此时,新来的函数会将之前的函数覆盖掉,变量环境对象中的 showName 函数体的内容被更新为新的函数体。
  • 执行阶段,从变量环境对象中查找函数,找到同名函数,执行新的函数体,输出 cellinlab

所以,如果一段代码中定义了两个同名函数,那么,最后生效的是晚点定义的函数。

调用栈

调用栈就是用来管理函数调用关系的一种数据结构。

函数调用

函数调用就是运行一个函数,具体方法就是使用函数名后加括号:

代码语言:javascript
代码运行次数:0
运行
复制
var a = 2;
function add () {
  var b = 10;
  return a + b;
}
add();

在执行到函数 add() 之前,JavaScript 引擎会为代码创建全局执行上下文,包含声明的函数和变量。

代码中的全局变量和函数都保存在全局上下文的变量环境中。

执行上下文准备好之后,便开始执行全局代码,当执行到 add 时,JavaScript 引擎识别出这是个函数调用,会进行:

  1. 全局执行上下文 中,取出 add 函数代码;
  2. add 函数代码进行编译,并创建该函数的执行上下文可执行代码
  3. 执行代码,输出结果。

在执行 JavaScript 时,可能存在多个执行上下文,JavaScript 引擎通过来管理执行上下文。

JavaScript 的调用栈

在执行上下文创建好后,JavaScript 引擎会将执行上下文压入栈中,通常将用来管理执行上下文的栈称执行上下文栈,也叫调用栈

代码语言:javascript
代码运行次数:0
运行
复制
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);

上面代码执行流程:

  1. 创建全局上下文,将其压入栈底;
  • 变量 a、函数 addaddAll 都保存到了全局上下文的变量环境对象中
  • 全局上下文压入到调用栈后,JavaScript 引擎开始执行全局代码
  1. 调用 addAll 函数,当调用该函数时,JavaScript 引擎会编译该函数,并为其创建一个执行上下文,最后还将该函数的执行上下文压入栈:
  • addAll 函数的执行上下文创建好之后,便进入函数代码的执行阶段
  1. 当执行到 add 函数调用语句时,同样会为其创建执行上下文,并将其压入调用栈
  • add 函数返回时,该函数的执行上下文就会从栈顶弹出,并将 result 的值设置为 add 函数的返回值,也就是 9
  • 紧接着 addAll 执行最后一个相加操作后并返回,addAll 的执行上下文也会弹出
  • 最后,调用栈中只剩下全局上下文,程序执行完毕

可以看出,调用栈是 JavaScript 引擎追踪函数执行的一个机制,当一次有多个函数被调用时,通过调用栈就能追踪到哪个函数正在被执行,以及各函数之间的关系。

块作用域

作用域

作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。

在 ES6 之前,作用域只有两种:

  • 全局作用域:其中的内容在代码中的任何地方都能访问,其生命周期与页面的生命周期相同,只要页面存在,其内容就存在;
  • 函数作用域:在函数内部定义的变量或函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束后,函数内部定义的变量也会被销毁。

块级作用域是一对大括号包裹的一段代码,如函数、判断语句、循环语句,甚至单独的一个 {} 都可以被看做是一个块级作用域。对于支持块作用域的语言,代码块内部定义的变量在代码块外部是访问不到的,并且等该代码块中的代码执行完之后,代码块中定义的变量会被销毁。

因为,在 ES6 之前,是不支持块级作用域的。没了块级作用域,再把作用域内部的变量统一提升无疑是最快速、最简单的设计,但是,这导致了函数中的变量无论是在哪里声明的,在编译阶段都会被提取到执行上下文的变量环境中,所以,这些变量在整个函数体内部的任何地方都是能被访问的。

# 变量提升带来的问题

变量容易在不被察觉的情况下被覆盖掉

代码语言:javascript
代码运行次数:0
运行
复制
var myname = 'cellinlab';
function showName () {
  console.log(myname);
  if (0) {
    var myname = 'cell';
  }
  console.log(myname);
}
showName(); 
// undefined
// undefined
// undefined

复制🤏

  • 执行上下文和调用栈的状态
  • showName 函数的执行上下文创建后,JavaScript 引擎便开始执行 showName 函数内部的代码
  • 调用栈中有两个 myname 变量:一个在全局上下文中,其值是 cellinlab;另一个在 showName 函数的执行上下文中,其值是 undefined
  • 在函数执行过程中,JavaScript 会优先从当前的执行上下文中查找变量,由于变量提升,当前的执行上下文中就包含了变量 myname 值是 undefined,故 输出 undefined

本应销毁的变量没有被销毁

代码语言:javascript
代码运行次数:0
运行
复制
function foo () {
  for (var i = 0; i < 7; i++) {
  }
  console.log(i);
}
foo(); // 7
  • 在创建执行上下文是,i 被提升,当 for 循环结束时,i 并没有被销毁。

# ES6 如何解决变量提升带来的缺陷

ES6 引入了 letconst 关键字,从此 JavaScript 也有了 块作用域。

代码语言:javascript
代码运行次数:0
运行
复制
function varTest () {
  var x = 1;
  if (ture) {
    var x = 2;
    console.log(x); // 2
  }
  console.log(x); // 2
}
varTest();

使用 let :

代码语言:javascript
代码运行次数:0
运行
复制
function letTest () {
  let x = 1;
  if (ture) {
    let x = 2;
    console.log(x); // 2
  }
  console.log(x); // 1
}

let 支持块级作用域,在编译阶段,JavaScript 引擎并不会把 if 块中的变量提升,使其在全函数可见范围内。

# JavaScript 是如何支持块级作用域的

代码语言:javascript
代码运行次数:0
运行
复制
function foo () {
  var a = 1;
  let b = 2;
  {
    let b = 3;
    var c = 4;
    let d = 5;
    console.log(a);
    console.log(b);
  }
  console.log(b);
  console.log(c);
  console.log(d);
}
foo();
// 1 3 2 undefined undefined
  1. 编译并创建执行上下文
  • 函数内部通过 var 声明的变量,在编译阶段全都被存放到变量环境里面
  • 通过 let 声明的变量,在编译阶段会被存放到 词法环境
  • 在函数的作用域内部,通过 let 声明的变量并没有被存放到词法环境中
  1. 继续执行代码,当执行到代码块里面时:
  • 当进入函数的作用域块时,作用域块中通过 let 声明的变量,会被存放在词法环境的一个单独的区域中,该区域中的变量不会影响块作用域外的变量
  • 实质上,在词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶,并在执行完该作用域后弹出
  • 在读取值时,会现在词法环境找,找不到再去变量环境中找
  • 等作用域块执行结束后,其内部定义的变量就会从词法环境的栈顶弹出,最终执行上下文如下

# 作用域和闭包

代码语言:javascript
代码运行次数:0
运行
复制
function bar () {
  console.log(myName);
}
function foo () {
  var myName = 'cell';
  bar();
}
var myName = 'cellinlab';
foo();
// cellinlab

在每个执行上下文的变量环境中,都包含一个外部引用,用来指向外部的执行上下文,可以称为 outer

当一段代码使用了一个变量时,JavaScript 引擎首先会在 “当前的执行上下文” 中查找该变量。如果当前的环境变量中没有找到,会继续在 outer 所指向的执行上下文中查找。

bar 函数 和 foo 函数的 outer 都是指向全局上下文的,也就意味着如果在 bar 函数或者 foo 函数中使用了外部变量,那么 JavaScript 引擎回去全局执行上下文中查找。把这个查找的路径称为 作用域链

不过,有个疑问,foo 函数中调用的 bar 函数,为什么 bar 函数的外部引用是全局上下文,而不是 foo 函数的执行上下文?这需要了解词法作用域,JavaScript 执行过程中,其作用域链是由词法作用域决定的。

# 词法作用域

词法作用域指作用域由代码中函数声明的位置来决定的,所以词法作用域就是静态作用域,通过它能够预测代码在执行过程中如何查找标识符。

上图中的词法作用域链是:foo 函数 -> bar 函数 -> main 函数 -> 全局作用域。

词法作用域是代码阶段就决定好的,和函数怎么调用没有关系

# 闭包

代码语言:javascript
代码运行次数:0
运行
复制
function foo () {
  var myName = 'cell';
  let test1 = 1;
  const test2 = 2;
  var innerBar = {
    getName: function () {
      console.log(test1);
      return myName;
    },
    setName: function (name) {
      myName = name;
    }
  };
  return innerBar;
}
var bar = foo();
bar.setName('cellinlab');
console.log(bar.getName());
// 1
// cellinlab

根据词法作用域的规则,内部函数 getNamesetName 总是可以访问它们的外部函数 foo 中的变量。当 innerBar 对象返回给全局变量 bar 时,虽然 foo 函数已经执行结束,但是 getNamesetName 函数依然可以使用 foo 函数中的变量。在函数执行完后,其执行上下文弹出了,但是由于返回的方法中使用了 foo 中的变量 myNametest1,所以这两个变量依然存在于内存中,这就是闭包。

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

# 闭包回收

通常,如果引用闭包的函数是一个全局变量,那么闭包会一直存在直到页面关闭;如果这个闭包以后不再使用的话,就会造成内存泄漏。

如果引用闭包的函数是局部变量,等函数销毁后,在下次 JavaScript 引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么 JavaScript 引擎的垃圾回收器就会回收这块内存。

需要注意:如果闭包会一直使用,那么它可以作为全局变量存在;如果使用频率不高,而且占用内存较大的话,就尽量让它成为一个局部变量。

# this

# JavaScript 中的 this 是什么

this 是和执行上下文绑定的,即每个执行上下文中都有一个 this。执行上下文主要有三种:全局执行上下文、函数执行上下文和 eval 执行上下文,那对应的 this 就有三种。

# 全局执行上下文中的 this

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

# 函数执行上下文中的 this

代码语言:javascript
代码运行次数:0
运行
复制
function foo() {
  console.log(this);
}
foo();
// window

默认情况下调用一个函数,其执行上下文中的 this 是指向 window 对象的。可以通过一些方法来设置函数执行上下文中的 this 值:

  1. 通过函数的 call 方法设置(还有 bindapply)
代码语言:javascript
代码运行次数:0
运行
复制
let bar = {
  myName: 'cell',
  test1: 1,
};
function foo () {
  this.myName = 'cellinlab';
}
foo.call(bar);
console.log(bar.myName); // cellinlab
  1. 通过对象调用方法设置
代码语言:javascript
代码运行次数:0
运行
复制
var myObj = {
  name: 'cellinlab',
  showName: function () {
    console.log(this.name);
  }
}
myObj.showName(); // cellinlab
  • 使用对象来调用其内部的一个方法,该方法的 this 指向对象本身
代码语言:javascript
代码运行次数:0
运行
复制
var name = 'default';
var myObj = {
  name: 'cellinlab',
  showName: function () {
    console.log(this.name);
    this.name = 'cell';
    console.log(this.name);
  }
};
var foo = myObj.showName;
foo(); 
// default
// cell
  • 在全局环境中调用一个函数,函数内部的 this 指向对象本身
  1. 通过构造函数设置
代码语言:javascript
代码运行次数:0
运行
复制
function CreateObj () {
  this.name = 'cellinlab';
}
var myObj = new CreateObj();
console.log(myObj.name); // cellinlab
  • 当执行 new CreateObj() 时,JavaScript 引擎会:
    1. 创建一个空对象 tempObj
    2. 调用 CreateObj.call,并将 tempObj 作为 call 的参数,这样当 CreateObj 的执行上下文创建时,它的 this 指向的就是 tempObj
    3. 执行 CreateObj 函数,此时的 CreateObj 函数上下文中的 this 指向了 tempObj 对象;
    4. 返回 tempObj 对象
  • 可以理解为:
代码语言:javascript
代码运行次数:0
运行
复制
var tempObj = {};
CreateObj.call(tempObj);
var myObj = tempObj;

# this 的设计缺陷及应对方案

嵌套函数中的 this 不会从外层函数中继承

代码语言:javascript
代码运行次数:0
运行
复制
var name = 'default';
var myObj = {
  name: 'cellinlab',
  showName: function () {
    console.log(this.name);
    function foo () {
      console.log(this.name);
    };
    foo();
  }
};
myObj.showName();
// cellinlab
// default
  • foo 中的 this 指向的是全局 window 对象,而函数 showName 中的 this 指向的是 myObj 对象
  • 可以通过在 showName 中声明式地引用 this 来解决这个问题,这种方案的本质是将 this 体系转换为了作用域的体系
代码语言:javascript
代码运行次数:0
运行
复制
var name = 'default';
var myObj = {
  name: 'cellinlab',
  showName: function () {
    console.log(this.name);
    const self = this;
    function foo () {
      console.log(self.name);
    };
    foo();
  }
};
myObj.showName();
// cellinlab
// cellinlab
  • 也可以使用箭头函数解决这个问题,因为箭头函数不会创建自身的执行上下文,所以箭头函数中的 this 取决于它的外部函数
代码语言:javascript
代码运行次数:0
运行
复制
var name = 'default';
var myObj = {
  name: 'cellinlab',
  showName: function () {
    console.log(this.name);
    const self = this;
    const foo = () => {
      console.log(self.name);
    };
    foo();
  }
};
myObj.showName();
// cellinlab
// cellinlab

普通函数中的 this 默认指向全局对象 window

如果要让函数执行上下文中的 this 指向某个对象,最好的方法是使用 call 来做显示绑定。

也可以通过 严格模式 来解决,严格模式下,默认执行一个函数,其函数的执行上下文中的 thisundefined

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020/5/3,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

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

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 变量提升
    • 声明和赋值
    • 变量提升
    • 代码执行流程 之 编译阶段
    • 代码执行流程 之 执行阶段
    • 同名变量和函数的处理
  • 调用栈
    • 函数调用
    • JavaScript 的调用栈
  • 块作用域
    • 作用域
    • # 变量提升带来的问题
    • # ES6 如何解决变量提升带来的缺陷
    • # JavaScript 是如何支持块级作用域的
  • # 作用域和闭包
    • # 词法作用域
    • # 闭包
    • # 闭包回收
  • # this
    • # JavaScript 中的 this 是什么
    • # 全局执行上下文中的 this
    • # 函数执行上下文中的 this
    • # this 的设计缺陷及应对方案
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档