scope JS深入理解闭包/作用域、作用域链/执行上下文和执行栈

前言:深入理解scope作用域和闭包,就要先理解什么是执行上下文和执行栈 。作用域、作用域链、闭包是的难点也是重点,其实理解起来也不难,学习过其他语言,比如C语言就可以很好的类比 。
一 知识储备
深入学习程序内部的执行机制 , 就要彻底理解执行上下文和执行栈 。
了解一些专业概念
二 执行上下文和执行栈 1 什么是执行上下文?
执行上下文是评估和执行代码环境的抽象概念 。每当控制器转到可执行代码的时候,它都是在执行上下文中运行 , 即执行环境中的变量,函数声明,参数 , 作用域链,this等信息 。
//组成代码展示const ExecutionContextObj = {VO: window,// 变量对象ScopeChain: {}, // 作用域链this: window};
2 执行上下文三种类型3 什么是执行栈?
执行栈,也叫调用栈,被用来存储代码运行时创建的所有执行上下文 。
栈是一种数据结构,遵循后进先出的原则 。
当引擎第一次遇到脚本时 , 它会创建一个全局的执行上下文并且压入当前执行栈 。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部 。引擎会执行那些执行上下文位于栈顶的函数 。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文 。
function fn1() {console.log('fn1被调用了 -- 创建了fn1的函数执行上下文,压入栈');fn2(); console.log('fn2执行完成,fn2的执行上下文会从栈中弹出');}function fn2() {console.log('fn2被调用了 -- 创建了fn2的函数执行上下文,压入栈');}fn1();console.log('fn1执行完成 , fn2的执行上下文会从栈中弹出');
运行结果
//fn1被调用了 -- 创建了fn1的函数执行上下文,压入栈//fn2被调用了 -- 创建了fn2的函数执行上下文,压入栈//fn2执行完成,fn2的执行上下文会从栈中弹出//fn1执行完成 , fn2的执行上下文会从栈中弹出
4 引擎创建执行上下文
执行上下文有两个阶段
在代码执行前是创建阶段,发生三件事情
this绑定创建()词法环境组件创建()变量环境组件
ExecutionContext = {ThisBinding = ,LexicalEnvironment = { ... },VariableEnvironment = { ... },}
(1)创建阶段
在全局执行上下文中,this 的值指向全局对象 。(在浏览器中,this引用对象)
在函数执行上下文中 , this的指向取决于函数是如何被调用的 。
let obj = {fn: function() {console.log(this);}}let win = obj.fn;obj.fn(); //this指向objwin(); // this指向window
官方ES6文档概述:词法环境是一种规范类型,基于代码的词法嵌套结构来定义标识符和具体变量和函数的关联 。一个词法环境由环境记录器和一个可能的引用外部词法环境的空值组成 。
:标识符指的是变量/函数的名字,而变量是对实际对象[包含函数类型对象]或原始数据的引用 。
词法环境有两个组成部分
声明式环境记录器:存储变量和函数声明的实际位置对象环境记录器:可以访问其外部词法环境(作用域)

scope  JS深入理解闭包/作用域、作用域链/执行上下文和执行栈

文章插图
词法环境有两种类型
全局环境:是一个没有外部环境的词法环境,其外部环境引用为 null 。拥有一个全局对象( 对象)及其关联的方法和属性(例如数组方法)以及任何用户自定义的全局变量,this 的值指向这个全局对象 。
函数环境:用户在函数中定义的变量被存储在环境记录中,包含了 对象 。对外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境 。

在全局环境中,环境记录器是对象环境记录器
在函数环境中,环境记录器是声明式环境记录器
在ES6中,词法环境组件和变量环境组件之间的一个区别是前者用于存储函数声明和变量let和const绑定,而后者仅用于存储变量var绑定 。
(2)执行阶段
在此阶段,完成对所有这些变量的分配 , 最后执行代码 。(在执行阶段,如果引擎不能在源码中声明的实际位置找到 let 变量的值,它会被赋值为
三 作用域(Scope) 1 什么是作用域?
作用域指程序中定义变量的区域,它可以决定当前执行代码的变量的可访问权限 。按照我自己的理解来说,作用域就是代码中某些特定的变量、函数在特定的独立区域可以被访问到 。
举个例子
function OutFun() {var inVariable = "internal variable";}OutFun();console.log(inVariable);
为什么会报错呢?这就跟变量的作用域有关系了!这里先不解释,看完整篇笔记就懂了 。
2 作用域有什么作用呢?
我们给变量起名字的时候,有时候代码量太多,一不小心就重名了 。就像全世界这么多人,总有很多人重名一个道理,那如何保证识人的唯一性呢 。对的!可以通过区域划分 。这些人生活在世界的各个角落,区域是不一样的 。所以我们可以通过A住在B城,另外一个A住在C城,从而把A和A区分开来 。作用域也是一个道理,不同作用域下的同名变量不会起冲突的原因就是,重名变量各自在不同的区域被访问到,起到了隔离的作用 。
3 作用域的分类
在ES6之前,只有全局作用域和函数作用域 。在ES6之后,通过提供关键字let/const来体现块级作用域 。
(1)全局作用域
在代码中的任何地方都可以被访问到的对象就拥有全局作用域,那什么情况下是拥有全局作用域的呢?
var outVariable = "I am outermost variable"; //最外层变量function outFun() { //最外层函数var innerVariable = "I am internal variable"; //内层变量function innerFun() { //内层函数console.log(innerVariable);}innerFun();}console.log(outVariable);outFun();console.log(innerVariable);innerFun();
为什么控制台会有这样的打印信息呢?
在代码中我们在最外层函数()里面嵌套了一层内层函数(),变量在全局作用域有声明 , 所以没有报错 。变量在函数内部被声明,而在全局作用域没有声明,所以在全局作用域下取值会报错 。()函数是外层函数 , 在全局作用域下被声明被调用 , 不会报错 。()函数的声明嵌套在()函数里面,属于内层函数,在全局作用域调用会报错 。要深入理解这些,建议结合计算机组成原理,了解全局变量和局部变量在计算机内部存储的分配和使用 。
function outFun() {variable = "我是没有被定义直接赋值的变量";var invariable = "我是内部变量";}outFun();console.log(variable);console.log(invariable);
scope  JS深入理解闭包/作用域、作用域链/执行上下文和执行栈

文章插图
window.name;window.location;window.top;
弊端
频繁使用全局变量在全局作用域起作用,会污染内存空间 , 引起变量重名的冲突,造成资源浪费 。全局变量内存的分配一直要等到程序执行结束才会被释放 。如果一个变量只需要在代码第一行使用之后就不会再次被使用,但是它命名到全局作用域 , 那么这一个全局变量就会污染内存空间,一直霸占内存位置,但实际上没有任何的使用价值了 。(占着茅坑不拉屎)
解决方案—函数作用域
(2)函数作用域
函数作用域是指声明在函数内部的变量,和全局作用域相反,是局部作用域 。在固定的函数代码段里面才可以被访问和使用 。函数执行完之后内存就会释放为执行这个函数开辟的内存空间,就解决了全局变量会引起污染的问题 。
function Fun() {var name = "gaby";function SayHi() {alert(name);}SayHi();}alert(name);SayHi();
SayHi()函数是内层函数,嵌套在Fun()外层函数里面,所以会造成脚本错误 。函数作用域顾名思义就是在函数{}括起来的内部可以被访问有作用 。
函数的作用域是分层级的,内层作用域的变量可以访问外部作用域的变量,反之则不行 。
每一个变量都有生命周期
来看代码例子,加深理解 。
function add1(a) {var b = a + a;function add2(c) {console.log(a, b, c);}add2(b * 3);}add1(2);
根据下图控制台的信息显示 , add2()这个最内层的函数可以依次访问到变量a,变量b,变量c 。
【scopeJS深入理解闭包/作用域、作用域链/执行上下文和执行栈】给上面的代码再增加一行,我们再看一下控制台的信息 。
function add1(a) {var b = a + a;function add2(c) {console.log(a, b, c);}add2(b * 3);console.log(a, b, c);}add1(2);
根据下图控制台信息显示,控制台报错了,原因就是add1()这个外层函数不能访问add2()这个内层函数的变量 。
由上面的例子可以看到,函数变量的访问顺序是按层级划分的 , 一层一层查找 。
需要注意的是 , 并不是所有用{}大括号括起来的代码都是函数作用域 , 比如循环if和,不会像函数,创建一个新的作用域 。
(3)块级作用域
ES6的新特性 , 通过let和const关键字声明,所声明的变量在指定的块级作用域外无法被访问 。
在什么环境下会被创建呢?
使用let和const的时候需要注意几个点
(1)声明的变量不会提升到代码块的顶部,需要手动将声明放置到顶部 , 方便变量在整个代码块内都可以使用
function getName(condition) {if (condition) {let name = "gaby";return name;} else {//name在此处不可用return null;}//name在此处不可用}