返回顶部
首页 > 资讯 > 前端开发 > JavaScript >JS作用域作用链及this使用原理详解
  • 437
分享到

JS作用域作用链及this使用原理详解

JS作用域作用链thisJS作用域作用链 2022-11-13 14:11:17 437人浏览 八月长安
摘要

目录变量提升的原理:javascript的执行顺序第一部分:变量提升部分的代码第二部分:代码执行部分代码执行阶段调用栈:栈溢出的原理如何利用调用栈1.使用浏览器查看调用栈的信息2.小

变量提升的原理: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

其实 在词法作用域内部 维护了一个小型的栈结构 栈底是函数最外层的变量 进入一个作用域块后 便会将过海作用域内部耳朵变量压到栈顶 当作用域执行完之后 就会弹出(通过letconst声明的变量)

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

小结

块级作用域就是通过词法环境的栈结构来实现的 而变量提升是通过变量环境来实现 通过这两者的结合 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指向的执行上下文查找

可以看到callNamefindName的outer都是指向全局上下文的 所以当在callName中找不到personName的时候 会去全局找 而不是调用callNamefindName中找 所以输出的是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 是一个对象 包含了 getNamesetName的两个方法 这两个方法都是内部定义的 且都引用了函数内部的变量

根据词法作用域的规则 getNamesetName总是可以访问到外部函数foo中的变量 所以当foo执行结束时 getNamesetName依然可以以后使用变量myNametest 如下图所示

可以看出 虽然foo从栈顶弹出 但是变量依然存在内存中 这个时候 除了setNamegetName 其他任何地方都不能访问到这两个变量 所以形成了闭包

那如何使用这些闭包呢 可以通过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确实也是比较难理解和令人头疼的问题 接下来我将从执行上下文的角度来分析JavaScript中的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

总结

相信看到这里 大家对于作用域 作用域链 执行上下文和this都有了更深的理解 笔者后期还会更新更多关于浏览器的原理和实践 感兴趣的小伙伴可以点波关注一起学习 文中错误之处请在评论区指出!

以上就是JS作用域作用链及this使用原理详解的详细内容,更多关于JS作用域作用链this的资料请关注编程网其它相关文章!

--结束END--

本文标题: JS作用域作用链及this使用原理详解

本文链接: https://lsjlt.com/news/172311.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • JS作用域作用链及this使用原理详解
    目录变量提升的原理:JavaScript的执行顺序第一部分:变量提升部分的代码第二部分:代码执行部分代码执行阶段调用栈:栈溢出的原理如何利用调用栈1.使用浏览器查看调用栈的信息2.小...
    99+
    2022-11-13
    JS作用域作用链this JS作用域作用链
  • js作用域及作用域链工作引擎
    目录前言一、作用域(scope)1.作用域的分类2.函数体作用域3.块级作用域二、预编译三、作用域链前言 我们需要先知道的是引擎,引擎的工作简单粗暴,就是负责javascript从头...
    99+
    2024-04-02
  • js作用域及作用域链工作引擎怎么应用
    本文小编为大家详细介绍“js作用域及作用域链工作引擎怎么应用”,内容详细,步骤清晰,细节处理妥当,希望这篇“js作用域及作用域链工作引擎怎么应用”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。一、作用域(scope...
    99+
    2023-07-02
  • Javascript的作用域、作用域链以及闭包详解
    一、javascript中的作用域 ①全局变量-函数体外部进行声明 ②局部变量-函数体内部进行声明 1)函数级作用域 javascript语言中局部变量不同于C#、Java等高级语言...
    99+
    2024-04-02
  • js作用域链怎么使用
    这篇文章主要介绍js作用域链怎么使用,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!说明只要是代码,至少有一个作用域。写在函数内部的局部作用域。如果函数中有函数,在这个功能域中可以诞生另一个功能域。根据内部函数可以访问...
    99+
    2023-06-20
  • javascript作用域和作用域链详解
    目录一、javascript的作用域1、全局作用域2、局部作用域二、javascript的作用域链三、作用域链和优化四、改变作用域链1、with语法改变作用域链2、catch语法总结...
    99+
    2024-04-02
  • js词法作用域与this实例详解
    目录前言实践总结前言 静态作用域又叫做词法作用域,采用词法作用域的变量叫词法变量。词法变量有一个在编译时静态确定的作用域。词法变量的作用域可以是一个函数或一段代码,该变量在这段代码区...
    99+
    2024-04-02
  • js中作用域和作用域链及预解析的示例分析
    小编给大家分享一下js中作用域和作用域链及预解析的示例分析,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!变量---->局部...
    99+
    2024-04-02
  • js中的this作用域全解析
    目录this作用域问题函数式调用方法调用模式在数组中的特例构造器调用模式call、apply、bind特殊情况——箭头函数综合例题this作用域问题 一般来说...
    99+
    2022-11-13
    js中this this作用域 js this作用域
  • JS作用域链怎么用
    这篇文章主要为大家展示了“JS作用域链怎么用”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“JS作用域链怎么用”这篇文章吧。具体内容如下1、所有全局变量和函数都是...
    99+
    2024-04-02
  • 图解JavaScript作用域链底层原理
    目录前言 作用域 1.什么是作用域 2.[[Scopes]]属性 3.作用域链 4.图解查找变量原理 总结 前言 在学习JavaScript时大家一定都知道,外部空间不能访问内部变...
    99+
    2024-04-02
  • 详解SpringBootStarter作用及原理
    目录前言什么是 StarterStarter 的作用spring 整合组件spring-boot 整合组件Starter 原理前言 有没有在入行后直接基于 SpringBoot 开发...
    99+
    2023-05-17
    SpringBoot Starter作用原理 SpringBoot Starter作用 SpringBoot Starter原理
  • JS难点同步异步和作用域与闭包及原型和原型链详解
    目录JS三座大山同步异步同步异步区别作用域、闭包函数作用域链块作用域闭包闭包解决用var导致下标错误的问题投票机闭包两个面试题原型、原型链原型对象原型链完整原型链图JS三座大山 同步...
    99+
    2024-04-02
  • 怎么使用作用域与作用域链
    这篇文章主要讲解了“怎么使用作用域与作用域链”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“怎么使用作用域与作用域链”吧!一、作用域如果说执行上下文是代码的执...
    99+
    2024-04-02
  • JavaScript作用域与作用域链使用重点讲解
    作用域和作用域链方面的知识是JS的重点,去面试十个有八个都会问你这块的知识,所以说这块是特别特别的重要,下面我们好好理解一下作用域和作用域链到底是个什么: 先上一段代码: var a...
    99+
    2022-11-13
    JS作用域与作用域链 JS作用域 JS作用域链
  • JS作用域和作用域链的区别是什么
    本篇内容介绍了“JS作用域和作用域链的区别是什么”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!作用域(Sc...
    99+
    2024-04-02
  • JavaScript闭包原理及作用详解
    目录简介闭包的用途柯里化实现公有变量缓存封装(属性私有化)闭包的原理垃圾收集简介实际开发中的优化简介 说明 本文介绍JavaScript的闭包的作用、用途及其原理。 闭包的定义 闭包...
    99+
    2024-04-02
  • JavaScript详细解析之作用域链
    以上就是JavaScript详细解析之作用域链的详细内容,更多请关注编程网其它相关文章!...
    99+
    2022-11-22
    JavaScript 前端
  • JavaScript函数执行、作用域链以及内存管理详解
    目录前言函数执行全局执行上下文函数执行上下文作用域链内存管理引用计数标记清除前言 在我们平常编写JavaScript代码的时候,难免会用到函数,函数里面会有各种变量,这些变量的作用的...
    99+
    2023-01-08
    JS函数执行 js作用域链 js内存管理
  • C++中this指针理解及作用
    目录01、C++程序到C程序的翻译02、this指针的作用03、this指针和静态成员函数04、小结01、C++程序到C程序的翻译 想要理解C++语言中的this指针,下面我们做一个...
    99+
    2024-04-02
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作