您现在的位置是:网站首页> 编程资料编程资料
JS作用域作用链及this使用原理详解_JavaScript_
2023-05-24
382人已围观
简介 JS作用域作用链及this使用原理详解_JavaScript_
变量提升的原理:JavaScript的执行顺序
变量提升:JavaScript代码执行过程中 JavaScript引擎把变量的声明部分和函数的声明部分提升到代码开头的行为 (变量提升后以undefined设为默认值)
callName(); function callName() { console.log('callName Done!'); } console.log(personName); var personName = 'james'; //变量提升后 类似以下代码 function callName() { console.log('callName Done!'); }; var personName = undefined; callName();//callName已声明 所以正常输出calName Done! console.log(personName);//undefined personName = 'james'; //代码所作改变: 1.将声明的变量和函数移到了代码顶部 2.去除变量的var 声明 JavaScript代码的执行流程:有些人认为 变量提升就是将声明部分提升到了最前面的位置 其实这种说法是错的 因为变量和函数声明在代码中的位置是不会变的 之所以会变量提升是因为在编译阶段被JavaScript引擎放入内存中(换句话来说 js代码在执行前会先被JavaScript引擎编译 然后才会进入执行阶段)流程大致如下图

那么编译阶段究竟是如何做到变量提升的呢 接下来我们一起来看看 我们还是以上面的那段代码作为例子
第一部分:变量提升部分的代码
function callName() { console.log('callName Done!') } var personName = undefined; 第二部分:代码执行部分
callName(); console.log(personName); personName = 'james'
执行图如下

可以看到 结果编译后 会在生成执行上下文和可执行代码两部分内容
执行上下文:JavaScript代码执行时的运行环境(比如调用一个函数 就会进入这个函数的执行上下文 确定函数执行期间的this、变量、对象等)在执行上下文中包含着变量环境(Viriable Environment)以及词法环境(Lexicol Environment) 变量环境保存着变量提升的内容 例如上面的myName 以及callName
那既然变量环境保存着这些变量提升 那变量环境对象时怎么生成的呢 我们还是用上面的代码来举例子
callName(); function callName() { console.log('callName Done!'); } console.log(personName); var personName = 'james'; - 第一、三行不是变量声明 JavaScript引擎不做任何处理
- 第二行 发现了function定义的函数 将函数定义储存在堆中 并在变量环境中创建一个
callName的属性 然后将该属性指向堆中函数的位置 - 第四行 发现
var定义 于是在变量环境中创建一个personName的属性 并使用undefined初始化
经过上面的步骤后 变量环境对象就生成了 现在已经有了执行上下文和可执行代码了 接下来就是代码执行阶段了
代码执行阶段
总所周知 js执行代码是按照顺序一行一行从上往下执行的 接下来还是使用上面的例子来分析
- 执行到
callName()是 JavaScript引擎便在变量环境中寻找该函数 由于变量环境中存在该函数的引用 于是引擎变开始执行该函数 并输出"callName Done!" - 接下来执行到
console.log(personName); 引擎在变量环境中找到personName变量 但是这时候它的值是undefined于是输出undefined - 接下来执行到了
var personName = 'james'这一行 在变量环境中找到personName并将其值改成james
以上便是一段代码的编译和执行流程了 相信看到这里你对JavaScript引擎是如何执行代码的应该有了更深的了解
Q:如果代码中出现了相同的变量或者函数怎么办?
A:首先是编译阶段 如果遇到同名变量或者函数 在变量环境中后面的同名变量或者函数会将之前的覆盖掉 所以最后只会剩下一个定义
function func() { console.log('我是第一个定义的') } func(); function func() { console.log('我是将你覆盖掉的') } func(); //输出两次"我是将你覆盖掉的" 调用栈:栈溢出的原理
你在日常开发中有没有遇到过这样的报错

根据报错我们可以知道是出现了栈溢出的问题 那什么是栈溢出呢?为什么会栈溢出呢?
Q1:什么是栈呢?
A1:一种后进先出的数据结构队列
Q2:什么是调用栈?
A2:代码中通常会有很多函数 也有函数中调用另一个函数的情况 调用栈就是用来管理调用关系的一种数据结构
当我们在函数中调用另一个函数(如调用自身的递归)然后处理不当的话 就很容易产生栈溢出 比如下面这段代码
function stackOverflow(n) { if(n == 1) return 1; return stackOverflow(n - 2); } stackOverflow(10000);//栈溢出 既然知道了什么是调用栈和栈溢出 那代码执行过程中调用栈又是如何工作的呢?我们用下面这个例子来举例
var personName = 'james'; function findName(name, address) { return name + address; } function findOneDetail (name, adress) { var tel = '110'; detail = findName(name, address); return personName + detail + tel }; findOneDetail('james', 'Lakers') 可以看到 我们在findOneDetail中调用了findName函数 那么调用栈是怎么变化的
第一步:创建全局上下文 并将其压入栈底

接下来开始执行personName = 'james'的操作 将变量环境中的personName设置为james
第二步:执行findOneDetail函数 这个时候JavaScript会为其创建一个执行上下文 最后将其函数的执行上下文压入栈中

接下来执行完tel = ‘110'后 将变量环境中的tel设置为110
第三步:当执行detail = findName()时 会为findName创建执行上下文并压入栈中

接下来执行完findName函数后 将其执行上下文弹出调用栈 接下来再弹出findOneDetail的执行上下文以及全局执行上下文 至此整个JavaScript的执行流程结束
所以调用栈是JavaScript引擎追踪函数执行的一个机制 当一次有多个函数被调用时 通过调用栈就能追踪到哪个函数正在被执行以及各函数之间的调用关系
如何利用调用栈
1.使用浏览器查看调用栈的信息
点击source并打上断点刷新后就可以再Call Stack查到调用栈的信息(也可以通过代码中输入console.track()查看)

2.小心栈溢出
当我们在写递归的时候 很容易发生栈溢出 可以通过尾调用优化来避免栈溢出
块级作用域:var、let以及const
作用域
作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期
我们都知道 使用var会产生变量提升 而变量提升会引发很多问题 比如变量覆盖 本应被销毁的变量依旧存在等等问题 而ES6引入了let 和const两种声明方式 让js有了块级作用域 那let和const时如何实现块级作用域的呢 其实很简单 原来还是从理解执行上下文开始
我们都知道 JavaScript引擎在编译阶段 会将使用var定义的变量以及function定义的函数声明在对应的执行上下文中的变量环境中创建对应的属性 当时我们发现执行上下文中还有一个词法环境对象没有用到 其实 词法环境对象便是关键之处 我们还是通过举例子来说明一下
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() - 第一步:执行并创建上下文

- 函数内部通过var声明的变量 在编译阶段全都被存放到变量环境里面了
- 通过let声明的变量 在编译阶段会被存放到词法环境(Lexical Environment)中
- 在函数的作用域内部 通过let声明的变量并没有被存放到词法环境中
- 接下来 第二步继续执行代码 当执行到代码块里面时 变量环境中a的值已经被设置成了1 词法环境中b的值已经被设置成了2
这时候函数的执行上下文就如下图所示:

可以看到 当进入函数的作用域块是 作用域块中通过let声明的变量 会被放到词法环境中的一个单独的区域中 这个区域并不邮箱作用域块外面的变量 (比如声明了b = undefined 但是不影响外面的b = 2)
其实 在词法作用域内部 维护了一个小型的栈结构 栈底是函数最外层的变量 进入一个作用域块后 便会将过海作用域内部耳朵变量压到栈顶 当作用域执行完之后 就会弹出(通过let和const声明的变量)

当作用域块执行完之后 其内部定义的变量就会从词法作用域的栈顶弹出

小结
块级作用域就是通过词法环境的栈结构来实现的 而变量提升是通过变量环境来实现 通过这两者的结合 JavaScript引擎也就同时支持了变量提升和块级作用域了。
作用域链和闭包
在开始作用域链和闭包的学习之前 我们先来看下这部分代码
function callName() { console.log(personName); } function findName() { var personName = 'james'; callName(); } var personName = 'curry'; findName();//curry //你是否以为输出james 猜想callName不是在findName中调用的吗 那findName中已经定义了personName = 'james' 那为什么是输出外面的curry呢 这其实是和作用域链有关的 在每个执行上下文的变量环境中 都包含了一个外部引用 用来执行外部的执行上下文 称之为outer
当代码使用一个变量时 会先从当前执行上下文中寻找该变量 如果找不到 就会向outer指向的执行上下文查找

可以看到callName和findName的outer都是指向全局上下文的 所以当在callName中找不到personName的时候 会去全局找 而不是调用callName的findName中找 所以输出的是curry而不是james
作用域链是由词法作用域决定的
词法作用域就是指作用域是由代码中函数声明的位置来决定的 所以词法作用域是静态的作用域 通过它就能够预测代码在执行过程中如何查找表示符
所以词法作用域是代码阶段就决定好的 和函数怎么调用
相关内容
- Vue+Echarts报错Cannot set properties of undefined (setting ‘plate‘)_vue.js_
- Vue中的echarts图表如何实现loading效果_vue.js_
- vue中的任务队列和异步更新策略(任务队列,微任务,宏任务)_vue.js_
- Vue3中slot插槽基本使用_vue.js_
- Vue3+ts+setup getCurrentInstance使用时遇到的问题以及解决办法_vue.js_
- vue中的computed 和 vm.$data 原理解析_vue.js_
- Vue3系列教程之插槽slot详解_vue.js_
- 使用vue3+ts+setup获取全局变量getCurrentInstance的方法实例_vue.js_
- vuex 设计思路和实现方式_vue.js_
- vue3+ts如何通过lodash实现防抖节流详解_vue.js_
