JS深入--词法作用域、执行上下文与闭包
文章目录
- 词法作用域
- 执行上下文与词法环境
- 闭包
- 闭包练习
- 作用域链
- REF
个人博客文章同步地址
词法作用域
JS 使用的是词法作用域(或称为静态作用域),函数的作用域在定义的时候就决定了,与词法作用域相对的是动态作用域,动态作用域会在运行时确定的。
一个《JS权威指南》中的例子:
var scope = "global scope";
function checkscope(){var scope = "local scope";function f(){return scope;}return f();
}
checkscope();
var scope = "global scope";
function checkscope(){var scope = "local scope";function f(){return scope;}return f;
}
checkscope()();
两段代码的执行结果都是:“local scope”
这也表明了JS 的作用域是静态作用域。
引用《JavaScript权威指南》的回答就是:
JavaScript 函数的执行用到了作用域链,这个作用域链是在函数定义的时候创建的。嵌套的函数
f()
定义在这个作用域链里,其中的变量 scope 一定是局部变量,不管何时何地执行函数f()
,这种绑定在执行f()
时依然有效。
执行上下文与词法环境
JS 中有一个执行调用栈和执行上下文的概念,执行栈是一个先进后出的数据结构,JS在执行每个函数的时候都会创建一个函数执行上下文,并将其压入执行栈中,当函数执行完后才将其从执行栈中弹出。执行栈最开始压入的是全局执行上下文,可以看作是最外层的 JS 代码环境。
函数执行上下文是在函数被执行的时候才创建的。
执行上下文的生命周期可以分为两个阶段:
- 创建阶段
- 执行阶段
一个执行上下文会由几部分组成:
- 词法环境(Lexical Environment)
- 变量环境(Variable Environment),也是一种词法环境
- this 绑定(This Binding)
词法环境 & 变量环境中又包含了:
- 环境记录(EnvironmentRecord),一个存储所有局部变量作为其属性的对象。
- 指向外部环境的指针(outer),全局环境执行上下文中的 outer 为
null
词法环境和变量环境保存了函数中定义的变量和函数的标识,而指向外部环境的指针串起来简单理解起来就是我们所说的作用域链了。也就是函数在定义的时候(还未执行)的作用域链。
在执行阶段,这三个部分都会被确定,词法环境和变量环境中的变量会被初始化,直到执行阶段才会被真正赋值。
网上文章中经常看到的变量对象(Variable Object,AO)和活跃对象(Activation Object,AO) 实际上是 ES1/ES3 中的内容,在 ES5 及以后的版本中已经不存在 AO 及一系列相关概念了,取而代之的是一个叫词法环境(Lexical Environment)的定义。
关于词法环境,大可以参阅:
- ECMA-262-5 in detail. Chapter 3.2. Lexical environments: ECMAScript implementation.
- csdn ECMA-262-5 词法环境:ECMA实现
在 ES6 中,词法环境和变量环境的区别在于 词法环境用于存储函数和使用 let
和 const
声明的变量,而变量环境仅用于存储使用 var
声明的变量。
对于函数执行上下文来说,函数传入的参数也会被保存在词法环境中。
举个例子:
let a = 20;
const b = 30;
var c;function multiply(e, f) { var g = 20; return e * f * g;
}c = multiply(20, 30);
对应的执行上下文:
// 首先会有一个全局执行上下文
GlobalExectionContext = {ThisBinding: <Global Object>,LexicalEnvironment: { EnvironmentRecord: { Type: "Object", // 标识符绑定在这里 a: < uninitialized >, b: < uninitialized >, multiply: < func > } outer: <null> },VariableEnvironment: { EnvironmentRecord: { Type: "Object", // 标识符绑定在这里 c: undefined, } outer: <null> }
}FunctionExectionContext = { ThisBinding: <Global Object>,LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", // 标识符绑定在这里 Arguments: {0: 20, 1: 30, length: 2}, }, outer: <GlobalLexicalEnvironment> },VariableEnvironment: { EnvironmentRecord: { Type: "Declarative", // 标识符绑定在这里 g: undefined }, outer: <GlobalLexicalEnvironment> }
}
执行上下文在创建阶段就绑定的函数和变量也就是我们观察到的变量提升/函数提升的原因。
注意到词法环境和变量环境中变量的初始化分别是 uninitialized
和 undefined
,因此对于 let
和 const
声明的变量,若在赋值之前使用的话会报 Reference Error
,而对于 var
声明的变量则会是 undefined
。
而函数的变量提升优先级会高于变量提升,这是因为在执行上下文创建阶段:
- 对函数的所有形参(若是函数上下文)
- 由名称和对应值组成的一个变量对象的属性被创建
- 若没有实参,属性值设为
undefined
- 对函数声明
- 由名称和对应值(函数对象)组成的一个变量对象的属性被创建
- 如果变量对象已经存在相同名称的属性,则完全替换这个属性
- 对变量声明
- 由名称和对应值(
undefined
或uninitialized
)组成的一个变量对象的属性被创建 - 对于
var
声明的变量,如果变量名称和已经声明的形式参数或函数相同,则变量声明不会干扰已存在的这类属性;而若是let
或const
声明的对象,若已存在同名对象则会报错。
- 由名称和对应值(
可以使用几个例子来进行验证:
- 函数提升优先级大于变量提升:
console.log(a) // 打印函数 afunction a() {}var a = 3;
- 对函数声明,若已存在同名属性则会完全替换:
console.log(a) // 打印第二个定义的函数 afunction a() {}function a() {console.log(a)
}console.log(a) // 打印第二个定义的函数 a
- 对于使用
var
的变量声明,若已存在同名属性,则不会进行干扰:
console.log(a)function a() {}var a = 3;
var a = 4;// 代码可以正常运行...
再看一道题:
function foo() {console.log(a);a = 1;
}foo(); // ???function bar() {a = 1;console.log(a);
}
bar(); // ???
foo 执行时会报错:ReferenceError: a is not defined,这是因为变量 a 并没有使用 var 声明,因此它在执行上下文创建阶段不会进行初始化(undefined),进而报错。
bar 执行后会打印 1。虽然 a 也没有使用 var 关键字申明,但在打印 a 的时候已经对其进行了赋值,因此不会报错。
另外,对于这类在函数中没有使用关键字声明的变量,会被设置为全局变量,导致全局环境污染以及内存泄漏:
function bar() {a = 1;console.log(a);
}
bar();console.log(window.a); // 1
闭包
引用红皮书上对闭包的陈述:
闭包是指有权访问另一个函数作用域中的变量的函数。
有两个要点:
- 闭包是函数
- 它可以访问另一个函数的作用域中的变量
根据这个定义,其实 JS 中的所有函数都是闭包,不过我们通常说闭包指的是在一个函数中返回另一个函数的情况,这个被返回的函数我们就叫它闭包。
这种情况下,即使外部函数的调用已结束,但闭包仍能访问到其内部的变量和参数。
来看一个例子:
function makeCounter() {let count = 0;return function() {return count++;};
}let counter = makeCounter();
在这个例子中,counter
就是我们一般所指的闭包,且每次调用 counter
,就能获得加 1 后的 count
值。
在每次 makeCounter()
调用的开始,都会创建一个新的词法环境对象,以存储该 makeCounter
运行时的变量。
根据我们上面对执行上下文的介绍可以知道,对于这个例子,它有两层嵌套的词法环境:
当执行 makeCounter
函数时,创建了一个匿名函数,此时这个匿名函数还未运行。
而所有函数在创建时都会有一个指针指向创建它的外部环境:[[Environment]]
(也就是我们上面说的 outer
),即使 makeCounter
结束,这个引用也仍然存在。
现在,当 counter()
中的代码查找 count
变量时,它首先搜索自己的词法环境(为空,因为那里没有局部变量),然后是外部 makeCounter()
的词法环境,并且在哪里找到就在哪里修改。即在变量所在的词法环境中更新变量。
闭包所指向的词法环境能够存在与 JS 引擎的垃圾回收机制也有关系:
通常,函数调用完成后,会将词法环境和其中的所有变量从内存中删除。因为现在没有任何对它们的引用了。与 JavaScript 中的任何其他对象一样,词法环境仅在可达时才会被保留在内存中。
但是,如果有一个嵌套的函数在函数结束后仍可达,则它将具有引用词法环境的
[[Environment]]
属性。
关于闭包所访问的外部环境中的变量,在实际中,JavaScript 引擎会试图优化它。它们会分析变量的使用情况,如果从代码中可以明显看出有未使用的外部变量,那么就会将其删除。
因此,在 V8(Chrome,Edge,Opera)中的一个重要的副作用是,此类变量在调试中将不可用。
具体可看:实际开发中的优化
闭包练习
- Counter 是独立的吗?
function makeCounter() {let count = 0;return function() {return count++;};
}let counter = makeCounter();
let counter2 = makeCounter();alert( counter() ); // 0
alert( counter() ); // 1alert( counter2() ); // ?
alert( counter2() ); // ?
答案是 0, 1
因为每次执行 makeCounter 时,都会创建一个新的执行上下文,所以 counter、counter2 中的 [[Environment]] 所引用的词法作用域是不同的
- Counter 对象
这里通过构造函数创建了一个 counter 对象。
它能正常工作吗?它会显示什么呢?
function Counter() {let count = 0;this.up = function() {return ++count;};this.down = function() {return --count;};
}let counter = new Counter();alert( counter.up() ); // ?
alert( counter.up() ); // ?
alert( counter.down() ); // ?
能够正常工作,且答案分别是: 1, 2, 1。
因为内部函数 up 和 down 都是在同一个词法环境中创建的,所以它们可以共享对同一个 count 变量的访问。
- sum
编写一个像 sum(a)(b) = a+b
这样工作的 sum
函数。
sum(1)(2) = 3
sum(5)(-1) = 4
为了使第二个括号有效,第一个(括号)必须返回一个函数。
function sum(a) {return function(b) {return a + b; // 从外部词法环境获得 "a"};}
- filter 方法
编写 inBetween
和 inArray
方法,使得 Array.prototype.filter
能够像下面这样工作:
arr.filter(inBetween(3,6))
—— 只挑选范围在3
到6
的值。arr.filter(inArray([1,2,3]))
—— 只挑选与[1,2,3]
中的元素匹配的元素。
/* .. inBetween 和 inArray 的代码 */
let arr = [1, 2, 3, 4, 5, 6, 7];alert( arr.filter(inBetween(3, 6)) ); // 3,4,5,6alert( arr.filter(inArray([1, 2, 10])) ); // 1,2
要写这题首先要知道 filter 函数的工作原理
function inBetween(l, r) {return function(val) {return l <= val && val <= r;};
}
//
function inArray(arr) {return function(val) {// 或使用 arr.includes(val)return arr.indexOf(val) !== -1;};
}
- 函数数组
我们想使用一个数组保存一些函数,当调用这些函数时输出我们函数定义时想要保存的值,不过下面的代码出了问题,将它们修复:
function makeArmy() {let shooters = [];let i = 0;while (i < 10) {let shooter = function() { // 创建一个 shooter 函数,alert( i ); // 应该显示其编号};shooters.push(shooter); // 将此 shooter 函数添加到数组中i++;}// 返回函数数组return shooters;
}let army = makeArmy();// ……所有的 shooter 显示的都是 10,而不是它们的编号 0, 1, 2, 3...
army[0](); // 编号为 0 的 shooter 显示的是 10
army[1](); // 编号为 1 的 shooter 显示的是 10
army[2](); // 10,其他的也是这样。
首先,为什么会是这样的结果呢?
这是因为我们定义的每个函数 shooter 都是在同一个词法作用域中的(makeArmy 的词法作用域),那么当我们调用这些函数时,访问的都是同一个 i,而我们在执行完 makeArmy 后 i 已经加到 10 了。
那么解决方案就很明显了:创建 10 个不同的变量来保存这些值。
方法1:使用中间变量
function makeArmy() {let shooters = [];let i = 0;while (i < 10) {let temp = i; // 每个 while 循环中都创建一个新的变量 temp 来保存 i 当前的值let shooter = function() {console.log( temp );};shooters.push(shooter);i++;}return shooters;
}
方法2:使用 for 循环
function makeArmy() {let shooters = [];// for 循环也是同样道理,每次循环中都是一个新的作用域for (let i = 0; i < 10; ++i) {let shooter = function() {console.log( i );};shooters.push(shooter);}return shooters;
}
作用域链
当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
作用域链的查找方式和原型链有点类似,不同点是:原型链查找若找不到指定的变量,则返回
undefined
,而作用域链查找,未找到变量值的话则会报错:ReferenceError
JS 中使用的是词法作用域,在函数定义的时候(还未执行)就会确定了一个初始的作用域链,然后在函数执行的时候会再次进行更新。
函数内部有一个属性 [[scope]]
,用于保存函数所能访问到的作用域,作用域的顶端是全局作用域。
当函数执行进入执行上下文时,会将当前环境对象加入作用域链顶端。
用伪代码来表示就是:
Scope = [curentEnv].concat([[scope]]);
另外,引用一个回答帮助理解:
在源代码中当你定义(书写)一个函数的时候(并未调用),js引擎也能根据你函数书写的位置,函数嵌套的位置,给你生成一个[[scope]],作为该函数的属性存在(这个属性属于函数的)。即使函数不调用,所以说基于词法作用域(静态作用域)。
然后进入函数执行阶段,生成执行上下文,执行上下文你可以宏观的看成一个对象,(包含vo,scope,this),此时,执行上下文里的scope和之前属于函数的那个[[scope]]不是同一个,执行上下文里的scope,是在之前函数的[[scope]]的基础上,又新增一个当前的AO对象构成的。
函数定义时候的[[scope]]和函数执行时候的scope,前者作为函数的属性,后者作为函数执行上下文的属性。
其中,AO 可以看作是我们上面所说的 词法环境
REF
- JavaScript深入之词法作用域和动态作用域
- JavaScript深入之变量对象
- 理解JavaScript 中的执行上下文和执行栈
- JavaScript深入之作用域链
- 变量作用域,闭包 – 现代 JS 教程
- 函数作用域和闭包
JS深入--词法作用域、执行上下文与闭包相关推荐
- js学习笔记(执行上下文、闭包、this部分)
1.函数的准备工作 函数在执行会进行一些准备工作,如创建一个"执行上下文"环境:执行上下文可以理解为当前代码的执行环境,它会形成一个作用域: 每个碰到可执行代码的时候都会进行这些& ...
- 作用域,上下文,闭包
作用域 作用域决定了你的代码里的变量和其他资源在各个区域中的可见性,为代码提供了一个安全层级,用户只能访问他们当前需要的东西. 在 JavaScript 中有两种作用域: 全局作用域:定义在函数之外的 ...
- 读书笔记-你不知道的JS上-词法作用域
JS引擎 编译与执行 Javascript引擎会在词法分析和代码生成阶段对运行性能进行优化,包含对冗余元素进行优化(例如对语句在不影响结果的情况下进行重新组合). 对于Javascript来说,大部分 ...
- 深入学习js之——词法作用域和动态作用域
开篇 当我们在开始学习任何一门语言的时候,都会接触到变量的概念,变量的出现其实是为了解决一个问题,为的是存储某些值,进而,存储某些值的目的是为了在之后对这个值进行访问或者修改,正是这种存储和访问变量的 ...
- JS基础篇之作用域、执行上下文、this、闭包
前言:JS 的作用域.执行上下文.this.闭包是老生常谈的话题,也是新手比较懵懂的知识点.当然即便你作为老手,也未必真的能理解透彻这些概念. 一.作用域和执行上下文 作用域: js中的作用域是词法作 ...
- 你不知道的JS之作用域和闭包(二)词法作用域
原文:你不知道的js系列 词法作用域(Lexical Scope) Lex time 一个标准的编译器的第一个阶段就是分词(token化) 词法作用域就是在词法分析时定义的作用域.换句话说,词法作用域 ...
- js变量后面加问号是什么_js没那么简单(1)-- 执行上下文
前言 我为什么写这个文章?也许换个耳熟能详的话题会有更多人看吧.之前发了个tls感觉阅读量不行. 要讲ecma语法吗?我觉得还是不了吧,毕竟这些繁琐,枯燥,而且门槛低. 那讲什么好?讲一点我自己觉得大 ...
- 一、 函数调用栈,执行上下文及变量对象
前言 为什么会有这篇文章? 在书籍或博客上,我们经常会看到「作用域链」.「闭包」.「变量提升」等概念,说明一个问题 -- 它们很重要. 但很多时候,对于这些概念,看的时候觉得自己已经明白了,可过不了多 ...
- 一篇文章让你理解面试难点:执行上下文(干货满满(附面试题))
在JavaScript的运行过程中,经常会遇到一些"奇怪"的行为,不理解为什么JavaScript会这么工作. 这时候可能就需要了解一下JavaScript执行过程中的相关内容了. ...
最新文章
- ICCV2021旷视研究院入选9篇paper介绍(检测+点云+图像配准等)
- 改变宇宙之前,GPT-3最先改变的可能是OpenAI
- 【编译原理】关于NFA和DFA-集合定义的探索
- jQuery单选按钮监听事件
- android 关于多任务下载问题
- tomcat使用manager GUI应用和script分别reload应用的注意事项
- java双层for循环
- .NET Core 如何调试 CPU 爆高?
- 大话数据结构——算法
- C语言有参函数调用时参数值传递问题
- mysql中的函数编程_MySQL
- Axure RP一个专业的高速原型设计工具
- /etc/resolv.conf
- snowflake算法
- 全球时报英语新闻爬虫
- 云课堂智慧职教网页版登录入口_云课堂智慧职教登录入口
- 清明去哪玩儿? 可视化工具帮你锁定旅游TOP10!
- 土地购买[Usaco2008 Mar]
- Markdown文档书写方法(工具+示例+验证)
- oracle 日期 区别,oracle中日期类型 to_date 和to_timestamp什么区别啊?