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

目录
  • 变量提升的原理:JavaScript的执行顺序
    • 第一部分:变量提升部分的代码
    • 第二部分:代码执行部分
    • 代码执行阶段
  • 调用栈:栈溢出的原理
  • 如何利用调用栈
    • 1.使用浏览器查看调用栈的信息
    • 2.小心栈溢出
  • 块级作用域:var、let以及const
    • 作用域
    • 小结
  • 作用域链和闭包
    • 块级作用域中的变量查找
    • 闭包
    • 闭包怎么回收
    • 小练
  • this:从执行上下文分析this
    • 全局执行上下文的this
    • 函数执行上下文的this
      • 1.通过call
      • 2.通过对象调用
      • 3.通过构造函数设置
  • this的缺陷以及应对方案
    • 1.嵌套函数的this不会从外层函数中继承
    • 2.普通函数中的this指向全局对象window
  • 总结

变量提升的原理: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的资料请关注我们其它相关文章!

(0)

相关推荐

  • js 执行上下文和作用域的相关总结

    前言 如果你是或者你想成为一名合格的前端开发工作者,你必须知道JavaScript代码在执行过程,知道执行上下文.作用域.变量提升等相关概念,并且熟练应用到自己的代码中.本文参考了你不知道的JavaScript,和JavaScript高级程序设计,以及部分博客. 正文     1.JavaScript代码的执行过程相关概念 js代码的执行分为编译器的编译和js引擎与作用域执行两个阶段,其中编译器编译的阶段(预编译阶段)分为分词/词法分析.解析/语法分析.代码生成三个阶段.   (1)在分词/词法

  • Vue.js slot插槽的作用域插槽用法详解

    目录 没有插槽的情况 Vue2.x 插槽 有插槽的情况 具名插槽 没有slot属性 插槽简单实例应用 作用域插槽 ( 2.1.0 新增 ) Vue3.x 插槽 插槽 作用域插槽 没有插槽的情况 <div id="app"> <child> <span>1111</span> </child> </div> <script> // 注册子组件 Vue.component("child"

  • JavaScript中的this指向和自定义属性详解

    目录 1.this关键字 2.自定义属性 3.综合案例1:tab选项卡的实现 附录 总结 1.this关键字 this指向的是当前元素 全局函数中的this指向window对象 代码中声明了一个全局函数,是属于浏览器窗口对象的,所以this表示的就是浏览器窗口对象window function fn() { consolo.log(this); } fn() 标签事件属性中的this指向window对象 如果将一个全局函数在一个标签属性中调用,如下: <button onclick="fn

  • JS 箭头函数的this指向详解

    箭头函数是ES6中的新增特性,他没有自己的this,其this指向从外层代码库继承. 使用箭头函数时要注意一下几点: 箭头函数不能用作构造函数,用的话会抛出一个错误 无法使用arguments参数,如果要用的话就用rest 无法使用yield命令,所以箭头函数无法用作Generator函数 因为没有自己的this,所以没法通过bind.call.apply来改变this指向 但是这不代表箭头函数的this指向是静态的,我们可以通过改变它外层代码库的this指向来控制 箭头函数的this从外层代码

  • JavaScript中作用域链的概念及用途讲解

    从零开始讲解JavaScript中作用域链的概念及用途 引言 之前我写过一篇关于JavaScript中的对象的一篇文章,里面也提到了作用域链的概念,相信大家对这个概念还是没有很深的理解,并且这个概念也是面试中经常问到的,因为这个概念实在太重要了,在我们平时写代码时,也可能会因为作用域链的问题,而出现莫名其妙的bug,导致我们花费大量的时间都查找不出原因.所以我就准备单独写一篇关于作用域链的文章,来帮大家更好地理解这个概念. 正文 一.执行环境 首先,我们要引入一个概念,叫做执行环境(下面简称环境

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

    目录 变量提升的原理:JavaScript的执行顺序 第一部分:变量提升部分的代码 第二部分:代码执行部分 代码执行阶段 调用栈:栈溢出的原理 如何利用调用栈 1.使用浏览器查看调用栈的信息 2.小心栈溢出 块级作用域:var.let以及const 作用域 小结 作用域链和闭包 块级作用域中的变量查找 闭包 闭包怎么回收 小练 this:从执行上下文分析this 全局执行上下文的this 函数执行上下文的this 1.通过call 2.通过对象调用 3.通过构造函数设置 this的缺陷以及应对方

  • PHP实现链式操作的原理详解

    在一个类中有多个方法,当你实例化这个类,并调用方法时只能一个一个调用,类似: db.php <?php class db { public function where() { //code here } public function order() { //code here } public function limit() { //code here } } index.php <?php $db = new db(); $db->where(); $db->order()

  • JavaScript对象原型链原理详解

    这篇文章主要介绍了JavaScript对象原型链原理详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 一个js对象,除了自己设置的属性外,还会自动生成proto.class.extensible属性,其中,proto属性指向对象的原型. 对象的属性也有writable.enumerable.configurable.value和get/set的配置方法. 对象的创建方式有三种: 一.使用字面量直接创建. 二.基于原型链创建. 分析上图,要点如

  • js Proxy的原理详解

    什么是代理模式 引入一个现实生活中的案例 我们作为用户需要去如何评估一个房子的好坏.如何办理住房手续等一些列繁琐的事物吗?显然,用户肯定不愿意这样做.用户最关心的是结果,用户对房子提出需求以及提供对等价值的金钱就可以获得满意的房子,这就是结果. 那么谁为用户去解决一系列繁琐的买房过程呢?当然就是"房屋中介"了!房屋中介的作用就是在房地产开发经营与消费的供求市场中,为交易物体提供评估.交易.代理.咨询等服务及善后服务的机构. 结合案例理解代理模式的定义 在某些情况下,一个对象不适合或者不

  • Vue中slot插槽作用与原理详解

    目录 1.作用 2.插槽内心 2.1.默认插槽 2.2.具名插槽(命名插槽) 2.3.作用域插槽 实现原理 1.作用 父组件向子组件传递内容 扩展.复用.定制组件 2.插槽内心 2.1.默认插槽 把父组件中的数组,显示在子组件中,子组件通过一个slot插槽标签显示父组件中的数据. 子组件 <template> <div class="slotChild"> <h4>{{msg}}</h4> <slot>这是子组件插槽默认的值&

  • Node.Js中实现端口重用原理详解

    本文介绍了Node.Js中实现端口重用原理详解,分享给大家,具体如下: 起源,从官方实例中看多进程共用端口 const cluster = require('cluster'); const http = require('http'); const numCPUs = require('os').cpus().length; if (cluster.isMaster) { console.log(`Master ${process.pid} is running`); for (let i =

  • JavaScript原型继承和原型链原理详解

    这篇文章主要介绍了JavaScript原型继承和原型链原理详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 在讨论原型继承之前,先回顾一下关于创建自定义类型的方式,这里推荐将构造函数和原型模式组合使用,通过构造函数来定义实例自己的属性,再通过原型来定义公共的方法和属性. 这样一来,每个实例都有自己的实例属性副本,又能共享同一个方法,这样的好处就是可以极大的节省内存空间.同时还可以向构造函数传递参数,十分的方便. 这里还要再讲一下两种特色的构造

  • Qiankun原理详解JS沙箱是如何做隔离

    目录 前言 复习一下沙箱 SanpshotSandbox LegacySandbox ProxySandbox 隔离原理 XXX is undefined 总结 前言 相信大家也知道 qiankun 有 SnapshotSandbox, LegacySandbox 和 ProxySandbox 这些沙箱,而它们又可以分为单例和多例两种模式,网上也有很多文章对其进行介绍. 但这些文章的关注点都是沙箱的环境恢复做的事,那 JS 的隔离到底是怎么做到的呢? 换个问法,当我写 window.a = 1

  • java并发等待条件的实现原理详解

    前言 前面介绍了排它锁,共享锁的实现机制,本篇继续学习AQS中的另外一个内容-Condition.想必学过java的都知道Object.wait和Object.notify,同时也应该知晓这两个方法的使用离不开synchronized关键字.synchronized是jvm级别提供的同步原语,它的实现机制隐藏在jvm实现中.作为Lock系列功能中的Condition,就是用来实现类似 Object.wait和Object.notify 对应功能的. 使用场景 为了更好的理解Lock和Condit

  • JS中实现浅拷贝和深拷贝的代码详解

    (一)JS中基本类型和引用类型 JavaScript的变量中包含两种类型的值:基本类型值 和 引用类型值,在内存中的表现形式在于:前者是存储在栈中的一些简单的数据段,后者则是保存在堆内存中的一个对象. 基本类型值 在JavaScript中基本数据类型有 String , Number , Undefined , Null , Boolean ,在ES6中,又定义了一种新的基本数据类型 Symbol ,所以一共有6种. 基本类型是按值访问的,从一个变量复制基本类型的值到另一个变量后,这两个变量的值

随机推荐