JS作用域作用链及this使用原理详解
2022-08-10 11:46:36 来源:易采站长站 作者:
目录
变量提升的原理:JavaScript的执行顺序第一部分:变量提升部分的代码第二部分:代码执行部分代码执行阶段调用栈:栈溢出的原理如何利用调用栈1.使用浏览器查看调用栈的信息2.小心栈溢出块级作用域:var、let以及const作用域小结作用域链和闭包块级作用域中的变量查找闭包闭包怎么回收小练this:从执行上下文分析this全局执行上下文的this函数执行上下文的this1.通过call2.通过对象调用3.通过构造函数设置this的缺陷以及应对方案1.嵌套函数的this不会从外层函数中继承2.普通函数中的this指向全局对象window总结变量提升的原理: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初始化
经过上面的步骤后 变量环境对象就生成了 现在已经有了执行上下文和可执行代码了 接下来就是代码执行阶段了
代码执行阶段
总所周知>
- 执行到
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>console.track()查看)

2.小心栈溢出
当我们在写递归的时候>
块级作用域:var、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声明的变量)

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

小结
块级作用域就是通过词法环境的栈结构来实现的>
作用域链和闭包
在开始作用域链和闭包的学习之前>
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
作用域链是由词法作用域决定的
词法作用域就是指作用域是由代码中函数声明的位置来决定的 所以词法作用域是静态的作用域 通过它就能够预测代码在执行过程中如何查找表示符
所以词法作用域是代码阶段就决定好的 和函数怎么调用的没有关系
块级作用域中的变量查找
我们来看下下面这个例子
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()
我们知道 如果是let或者const定义的 就会储存在词法环境中 所以寻找也是从该执行上下文的词法环境找 如果找不到 就去变量环境 还是找不到则去outer指向的执行上下文寻找 如下图

闭包
JavaScript>
举个例子
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())
首先我们看看当执行到 foo 函数内部的return innerBar这行代码时调用栈的情况 你可以参考下图:

从上面的代码可以看出 innerBar 是一个对象 包含了 getName 和setName的两个方法 这两个方法都是内部定义的 且都引用了函数内部的变量
根据词法作用域的规则 getName和setName总是可以访问到外部函数foo中的变量 所以当foo执行结束时 getName和setName依然可以以后使用变量myName和test 如下图所示

可以看出 虽然foo从栈顶弹出 但是变量依然存在内存中 这个时候 除了setName和getName 其他任何地方都不能访问到这两个变量 所以形成了闭包
那如何使用这些闭包呢 可以通过bar来使用 当调用了bar.seyName时 如下图

可以使用chrome的Clourse查看闭包情况
闭包怎么回收
通常>
如果引用闭包的函数是各局部变量 等函数销毁后 在下次JavaScript引擎执行垃圾回收的时候 判断闭包这块内容不再被使用了 就会回收
所以在使用闭包的时候 请记住一个原则:如果该闭包一直使用 可以作为全局变量而存在 如果使用频率不高且占内存 考虑改成局部变量
小练
var per = {
name: 'curry';
callName: function() {
console.log(name);
}
}
function askName(){
let name = 'davic';
return per.callName
}
let name = 'james';
let _callName = askName()
_callName();
per.callName();
//打印两次james
//只需要确定好调用栈就好 调用了askName()后 返回的是per.callName 后续就和askName没关系了(出栈) 所以结果就是调用了两次per.callName 根据词法作用域规则 结果都是james 也不会形成闭包
this:从执行上下文分析this
相信大家都有被this折磨的时候>this 这里先抛出结论:this是和执行上下文绑定的 每个执行上下文都有一个this
接下来 我将带大家一起理清全局执行上下文的this和函数执行上下文的this、
全局执行上下文的this
全局执行上下文的this和作用域链的最底端一样>window对象
函数执行上下文的this
我们通过一个例子来看一下
function func() {
console.log(this)//window对象
}
func();
默认情况下调用一个函数 其执行上下文的this也是指向window对象
那如何改变执行上下文的this值呢 可以通过apply call 和bind实现 这里讲下如何使用call来改变
1.通过call
let per = {
name: 'james',
address: 'Lakers'
}
function callName() {
this.name = 'curry'
}
callName.call(per);
console.log(per)//name: 'curry', address: 'Lakers'
可以看到这里this的指向已经改变了
2.通过对象调用
var person = {
name: 'james';
callName: function() {
console.log(this.name)
}
}
person.callName();//james
使用对象来调用其内部方法 该方法的this指向对象本身的
person.callName() === person.callName.call(person)
这个时候我们如果讲对象赋给另一个全局变量 this又会怎样变化呢
var person = {
name: 'james';
callName: function() {
this.name = 'curry';
console.log(this.name);
}
}
var per1 = person;//this又指向window
- 在全局环境中调用一个函数 函数内部的
this指向全局变量window通过一个对象调用内部的方法 该方法的this指向对象本身
3.通过构造函数设置
当使用new关键字构建好了一个新的对象>this其实就是对象本身
this的缺陷以及应对方案
1.嵌套函数的this不会从外层函数中继承
var person = {
name: 'james',
callName: function() {
console.log(this);//指向person
function innerFunc() {
console.log(this)//指向window
}
innerFunc()
}
}
person.callName();
//如何解决
1.使用一个变量保存
let _this = this //保存指向person的this
2.使用箭头函数
() => {
console.log(this)//箭头函数不会创建其自身的执行上下文 所以箭头函数中的this指向外部函数
}
2.普通函数中的this指向全局对象window
在默认情况下调用一个函数>this默认就是指向全局对象window
总结
相信看到这里>
以上就是JS作用域作用链及this使用原理详解的详细内容,更多关于JS作用域作用链this的资料请关注易采站长站其它相关文章!













闽公网安备 35020302000061号