浏览器原理2:JavaScript机制

JavaScript机制

[toc]

变量提升(Hoisting)

看代码的执行效果

showName()
console.log(myname)
var myname = '极客时间'
function showName() {
    console.log('函数showName被执行');
}

结果

image-20220103220807343

结论

  1. 在执行过程中,若使用了未声明的变量,那么JavaScript执行会报错。
  2. 在一个变量定义之前使用它,不会出错,但是该变量的值会为undefined,而不是定义时的值。
  3. 在一个函数定义之前使用它,不会出错,且函数能正确执行。

声明赋值

var myname = '极客时间'

这段代码你可以把它看成是两行代码组成的:

var myname    //声明部分
myname = '极客时间'  //赋值部分

[图片上传失败...(image-b5ceb0-1645949254743)]

函数的声明和赋值

function foo(){ //只有声明,无赋值
  console.log('foo')
}

var bar = function(){ //声明并赋值
  console.log('bar')
}

[图片上传失败...(image-108859-1645949254743)]

变量提升

是指在JavaScript代码执行过程中,JavaScript引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的undefined,而函数则会直接执行

[图片上传失败...(image-56ee2d-1645949254743)]

通过这段模拟的变量提升代码,相信你已经明白了可以在定义之前使用变量或者函数的原因——函数和变量在执行之前都提升到了代码开头

js代码执行流程

“变量提升”意味着变量和函数的声明会在物理层面移动到代码的最前面。但这并不准确。实际上变量和函数声明在代码里的位置是不会改变的,而且是在编译阶段被JavaScript引擎放入内存中。一段JavaScript代码在执行之前需要被JavaScript引擎编译,编译完成之后,才会进入执行阶段。

编译阶段

[图片上传失败...(image-a8df4d-1645949254743)]

以这段代码举例

showName()
console.log(myname)
var myname = '极客时间'
function showName() {
    console.log('函数showName被执行');
}

一段代码编译后,会生成执行上下文(Execution context)和可执行代码。执行上下文是JavaScript的运行时环境,包括用到的this,变量,对象及函数,执行上下文中存在环境变量对象(Viriable Environment),该对象保持了变量提升的内容,也就是声明的函数和变量。

所以上边代码的生成环境变量过程如下

  • 第1行和第2行,由于这两行代码不是声明操作,所以JavaScript引擎不会做任何处理;
  • 第3行,由于这行是经过var声明的,因此JavaScript引擎将在环境对象中创建一个名为myname的属性,并使用undefined对其初始化;
  • 第4行,JavaScript引擎发现了一个通过function定义的函数,所以它将函数定义存储到堆(HEAP)中,并在环境对象中创建一个showName的属性,然后将该属性值指向堆中函数的位置,这样就生成了变量环境对象。接下来JavaScript引擎会把声明以外的代码编译为字节码。

[图片上传失败...(image-89b6b8-1645949254743)]

执行阶段

  • JavaScript引擎从上到下一行行执行,当执行到showName函数时,JavaScript引擎便开始在变量环境对象中查找该函数,由于变量环境对象中存在该函数的引用,所以JavaScript引擎便开始执行该函数,并输出“函数showName被执行”结果。
  • 接下来打印“myname”信息,JavaScript引擎继续在变量环境对象中查找该对象,由于变量环境存在myname变量,并且其值为undefined,所以这时候就输出undefined。
  • 接下来执行第3行,把“极客时间”赋给myname变量,赋值后变量环境中的myname属性值改变为“极客时间”,变量环境如下所示
VariableEnvironment:
     myname -> "极客时间", 
     showName ->function : {console.log(myname)

如何出现相同的比那里或者函数,就会用最后声明的函数写入环境变量,执行时也就会只调用最后声明的函数。

出现相同的变量或者函数怎么办?

image-20220222082524525

同名函数,后边的会在变量环境中覆盖前一个函数,因为对函数的引用只有一个

js的上下文

  1. 当JavaScript执行全局代码的时候,会编译全局代码并创建全局执行上下文,而且在整个页面的生存周期内,全局执行上下文只有一份。
  2. 当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束之后,创建的函数执行上下文会被销毁。
  3. 当使用eval函数的时候,eval的代码也会被编译,并创建执行上下文。
var a = 2
function add(){
var b = 10
return  a+b
}
add()

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

image-20220222083957079

函数的调用过程

  • 全局执行上下文中,取出add函数代码(这类应该是引用,函数在堆上,引用在上下文里)。
  • 对add函数的这段代码进行编译,并创建该函数的执行上下文可执行代码
  • 执行代码,输出结果。
image-20220222084136372

当执行到add函数的时候,我们就有了两个执行上下文了——全局执行上下文和add函数的执行上下文,js通过栈来管理执行上下文,就会有栈溢出的问题.在执行上下文创建好后,JavaScript引擎会将执行上下文压入栈中,通常把这种用来管理执行上下文的栈称为执行上下文栈,又称调用栈

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)
image-20220222085353604

全局执行上下文压入到调用栈后,JavaScript引擎便开始执行全局代码了。当调用该函数时,JavaScript引擎会编译该函数,并为其创建一个执行上下文,最后还将该函数的执行上下文压入栈中。函数的执行上下文创建好之后,便进入了函数代码的执行阶段了。

当add函数返回时,该函数的执行上下文就会从栈顶弹出,并将result的值设置为add函数的返回值,也就是9。

作用域(scope)

作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。

ES的作用域只有两种:全局作用域和函数作用域。

  • 全局作用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。
  • 函数作用域就是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。
  • 其他语言则都普遍支持块级作用域。块级作用域就是使用一对大括号包裹的一段代码,通常快级作用域内的变量在代码块外部是访问不到的,并且等该代码块中的代码执行完成之后,代码块中定义的变量会被销毁。
ES6之前是不支持块级作用域的,把作用域内部的变量统一提升无疑是最快速、最简单的设计,不过这也直接导致了函数中的变量无论是在哪里声明的,在编译阶段都会被提取到执行上下文的变量环境中,所以这些变量在整个函数体内部的任何地方都是能被访问的,这也就是JavaScript中的变量提升。

作用域问题

var myname = "极客时间"
function showName(){
  console.log(myname);
  if(0){
   var myname = "极客邦"
  }
  console.log(myname);
}
showName()
#结果:
undefined 
undefined
image-20220222225655540

JavaScript会优先从当前的执行上下文中查找变量,由于变量提升,当前的执行上下文中就包含了变量myname,而值是undefined,所以获取到的myname的值就是undefined。

变量销毁问题

image-20220222225954800

重新定义变量问题

块中重新声明变量也会重新声明块外的变量

var x = 10;
// 这里输出 x 为 10
{ 
    var x = 2;
    // 这里输出 x 为 2
}
// 这里输出 x 为 2

ES6的let和const

let 声明的变量只在 let 命令所在的代码块内有效。

const 声明一个只读的常量,一旦声明,常量的值就不能改变。

function varTest() {
  var x = 1;
  if (true) {
    var x = 2;  // 同样的变量!
    console.log(x);  // 2
  }
  console.log(x);  // 2
}
对比
function letTest() {
  let x = 1;
  if (true) {
    let x = 2;  // 不同的变量
    console.log(x);  // 2
  }
  console.log(x);  // 1
}

因为let关键字是支持块级作用域的,所以在编译阶段,JavaScript引擎并不会把if块中通过let声明的变量存放到变量环境中,这也就意味着在if块通过let声明的关键字,并不会提升到全函数可见。所以在if块之内打印出来的值是2,跳出语块之后,打印出来的值就是1了。

而var关键字会进行变量提升,所以虽然函数内声明了两个x,但是最后提升为1个局部变量,并且执行if内时赋值为2.最终只生成了一个变量x,函数体内所有对x的赋值操作都会直接改变变量环境中的x值。

块级作用域的实现

案例
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()
执行上下文
image-20220223075616210
变量查找过程
image-20220223080954646

执行到作用域块中的console.log(a)这行代码时,就需要在词法环境和变量环境中查找变量a的值了,具体查找方式是:沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给JavaScript引擎,如果没有查找到,那么继续在变量环境中查找。

总结
  • 函数内部通过var声明的变量,在编译阶段全都被存放到变量环境里面了
  • 通过let声明的变量,在编译阶段会被存放到词法环境(Lexical Environment)
  • 在函数里的快内部的let变量,要在被执行的时候才放在词法环境中,但是会单独放在一个词法环境的区域中。
  • 作用域外面声明了变量b,在该作用域块内部也声明了变量b,当执行到作用域内部时,它们都是独立的存在。
  • 词法环境内部,维护了栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出,这就是词法环境的结构。这里的变量指let 和var。
  • 当作用域块执行结束之后,其内部定义的变量就会从词法环境的栈顶弹出

作用域链和闭包

案例
function bar() {
    console.log(myName)
}
function foo() {
    var myName = "极客邦"
    bar()
}
var myName = "极客时间"
foo()

执行上下文
image-20220223082126563

全局执行上下文和foo函数的执行上下文中都包含变量myName,那bar函数里面myName的值到底该选择哪个呢?

  1. 先查找栈顶是否存在myName变量,但是这里没有,然后就使用全局的执行上下文中数据

作用域链

  • 其实在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为outer

  • 当使用一个变量时,JavaScript引擎首先会在“当前的执行上下文”中查找,
    比如查找myName,如果在当前的变量环境中没有查找到,那么JavaScript引擎会继续在outer所指向的执行上下文中查找。我们把这个查找的链条就称为作用域链

  • 在JavaScript执行过程中,其作用域链是由词法作用域决定的。

image-20220223082355849

词法作用域

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

举例:

image-20220223082614805

词法作用域就是根据函数声明的位置来决定的,其中main函数包含了bar函数,bar函数中包含了foo函数,因为JavaScript作用域链是由词法作用域决定的,所以整个词法作用域链的顺序是:foo函数作用域—>bar函数作用域—>main函数作用域—>全局作用域

根据词法作用域,foo和bar的上级作用域都是全局作用域,所以如果foo或者bar函数使用了一个它们没有定义的变量,那么它们会到全局作用域去查找

块中查找变量
function bar() {
    var myName = "极客世界"
    let test1 = 100
    if (1) {
        let myName = "Chrome浏览器"
        console.log(test)
    }
}
function foo() {
    var myName = "极客邦"
    let test = 2
    {
        let test = 3
        bar()
    }
}
var myName = "极客时间"
let myAge = 10
let test = 1
foo()
输出1

栈情况

[图片上传失败...(image-27597e-1645949254744)]

ES6是支持块级作用域的,当执行到代码块时,如果代码块中有let或者const声明的变量,那么变量就会存放到该函数的词法环境中,块中找不到,也会像外部作用域去寻找,按照作用域链的方式去寻找,寻找顺序就是图片中的数字。

闭包

function foo() {
    var myName = "极客时间"
    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("极客邦")
bar.getName()
console.log(bar.getName())
输出
1
1
极客邦

innerBar是一个对象,包含getName和setName的两个方法,这两个方法都是在foo函数内部定义的,并且这两个方法内部都使用了myName和test1两个变量.

闭包的产生

执行到foo函数内部的return innerBar这行代码时调用栈

image-20220224224957075

根据词法作用域的规则,内部函数getName和setName总是可以访问它们的外部函数foo中的变量,所以当innerBar对象返回给全局变量bar时,虽然foo函数已经执行结束,但是getName和setName函数依然可以使用foo函数中的变量myName和test1。所以当foo函数执行完成之后,调用栈的状态如下。变量myName和test1,两个变量依然保存在内存中。除了setName和getName函数之外,其他任何地方都是无法访问数据,我们就可以把这个称为foo函数的闭包

image-20220224225734380
闭包的使用

当执行到bar.setName方法中的myName = "极客邦"这句代码时,JavaScript引擎会沿着“当前执行上下文–>foo函数闭包–>全局执行上下文”的顺序来查找myName变量,你可以参考下面的调用栈状态图:

image-20220224230307534

setName的执行上下文中没有myName变量,foo函数的闭包中包含了变量myName,所以调用setName时,会修改foo闭包中的myName变量的值

闭包展示
image-20220224230930445
闭包的回收

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

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

理解this

我们提到执行上下文中包含了变量环境、词法环境、外部环境,但其实还有一个this如下图:

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

执行上下文主要分为三种——全局执行上下文、函数执行上下文和eval执行上下文,所以对应的this也只有这三种——全局执行上下文中的this、函数中的this和eval中的this

image-20220227145107790
  • 全局执行上下文中的this是指向window对象的。这也是this和作用域链的唯一交点,作用域链的最底端包含了window对象,全局执行上下文中的this也是指向window对象。
  • 默认情况下调用一个函数,其执行上下文中的this也是指向window对象的。

通过函数的call设置this

你可以通过函数的call方法来设置函数执行上下文的this指向,比如下面这段代码,我们就并没有直接调用foo函数,而是调用了foo的call方法,并将bar对象作为call方法的参数。
你就能发现foo函数内部的this已经指向了bar对象。还可以使用bind和apply方法来设置函数执行上下文中的this
let bar = {
  myName : "极客邦",
  test1 : 1
}
function foo(){
  this.myName = "极客时间"
  console.log(this.myName)
}
foo.call(bar)
console.log(bar)
console.log(myName)

通过对象调用设置this

我们定义了一个myObj对象,该对象是由一个name属性和一个showThis方法组成的,然后再通过myObj对象来调用showThis方法。最终输出的this值是指向myObj的。

也可以认为JavaScript引擎在执行myObject.showThis()时,将其转化为了myObj.showThis.call(myObj)

var myObj = {
  name : "极客时间", 
  showThis: function(){
    console.log(this)
  }
}
myObj.showThis()
  • 在全局环境中调用一个函数,函数内部的this指向的是全局变量window。

  • 通过一个对象来调用其内部的一个方法,该方法的执行上下文中的this指向对象本身。

通过构造函数中设置

你可以像这样设置构造函数中的this,如下面的示例代码:

function CreateObj(){
  this.name = "极客时间"
}
var myObj = new CreateObj()
在这段代码中,我们使用new创建了对象myObj,那你知道此时的构造函数CreateObj中的this到底指向了谁吗?
其实,当执行new CreateObj()的时候,JavaScript引擎做了如下四件事:
  • 首先创建了一个空对象tempObj;

  • 接着调用CreateObj.call方法,并将tempObj作为call方法的参数,这样当CreateObj的执行上下文创建时,它的this就指向了tempObj对象;

  • 然后执行CreateObj函数,此时的CreateObj函数执行上下文中的this指向了tempObj对象;

  • 最后返回tempObj对象。

为了直观理解,我们可以用代码来演示下:
  var tempObj = {}
  CreateObj.call(tempObj)
  return tempObj
这样,我们就通过new关键字构建好了一个新对象,并且构造函数中的this其实就是新对象本身。

this的设计缺陷

嵌套函数中的this不会从外层函数中继承,函数bar中的this指向的是全局window对象,而函数showThis中的this指向的是myObj对象,因为bar没有使用上边的三个方式来设置this

var myObj = {
  name : "极客时间", 
  showThis: function(){
    console.log(this)
    function bar(){console.log(this)}
    bar()
  }
}
myObj.showThis()

改进如下,通过设置self来使用内部this。

var myObj = {
  name : "极客时间", 
  showThis: function(){
    console.log(this)
    var self = this
    function bar(){
      self.name = "极客邦"
    }
    bar()
  }
}
myObj.showThis()
console.log(myObj.name)
console.log(window.name)

改进方案2,通过箭头函数。ES6中的箭头函数并不会创建其自身的执行上下文,所以箭头函数中的this取决于它的外部函数。

var myObj = {
  name : "极客时间", 
  showThis: function(){
    console.log(this)
    var bar = ()=>{
      this.name = "极客邦"
      console.log(this)
    }
    bar()
  }
}
myObj.showThis()
console.log(myObj.name)
console.log(window.name)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容