let 是如何变成 var 的

2022-12-20 0 410

let 和 const 是在 ES6 中新导入的URL,用以代替 var,我也早已有一两年没所用 var 了。假如我有几段用 var 写的旧标识符,那时将其自上而下代替为 let,流程是没难题的(如果别把表达式重新命名为 let)。但假如我有几段用 let 写的标识符,想将其转化成 var,流程会有难题吗?

标准答案是,非常大可能将会有难题。可能将又没人怪异了,我回去的 let 不必,干嘛要把它代替成 var 呢。其实你的标识符始终在偷偷地的将 let 变为 var,“元凶”是 Babel,更具体文本一点儿的不然,是 babel-plugin-transform-block-scoping 应用流程,其原因也很单纯,为的是兼容这些不全力支持 ES6 的应用流程,let 须要在校对前夕切换为 var。

那 let 又是是不是变为 var 的呢?责任编辑借那个难题,深入探讨呵呵以下几点文本:

var 和 let 的返回值的差别,let 间接变为 var 会有甚么样的难题。Babel 是甚么样表述返回值的。是不是甚么单纯的计划能把 let 变为 var。介绍 babel-plugin-transform-block-scoping 应用流程同时实现方式。

JS 中的返回值

在 ES6 以后,JS 的返回值多于三种:自上而下返回值和表达式返回值。在自上而下返回值中言明的表达式是函数调用,函数调用在任何人两个地方性都能采用,所以多于应用流程停用的这时候函数调用才会被封存。表达式返回值是在表达式外部表述的表达式或是表达式,此种表达式或是表达式根本无法在表达式外部被出访。表达式继续执行完结后就会被封存。

如下表所示,a,b,c 都是函数调用,d,e 是表达式表达式。

// 函数调用 var a = 1; while(true) { var b = 1 } function c(){ // 表达式表达式 var d = 1 var e = function(){} }

JS 此种返回值的设计是有缺陷的,因为它缺少了块级返回值。块级返回值是采用一对大括号包裹的几段标识符,比如表达式、判断语句、循环语句,甚至单独的两个 {} 都能被看作是两个块级返回值。let 和 const 是用以解决那个难题的,ES6 规定,通过 let 表述的表达式,返回值是在表述它的块级标识符以及其中包括的子块中。

let 和 var 在返回值的差别是 let 无法单纯的用 var 来代替的其原因。Babel 在 github 上的两个 issue 深入探讨了那个难题。我列举其中的两个样例。

if (true) { let foo = true; } if (true) { let foo; if (foo) { alert(foo); } }

这是几段用 let 写的标识符,在第二段 if 标识符中,由于 let 是块级返回值,它言明的 foo 并没赋值,所以 alert 不会继续执行。

if (true) { var foo = true; } if (true) { var foo; if (foo) { alert(foo); // bug } }

这是将 let 单纯的代替为 var 后的标识符,由于 foo 是自上而下返回值,alert 被继续执行。这就证明了,let 变为 var 后很容易导致原先的语义发生变化,let 是不能间接变为 var 的。

在介绍 Babel 是甚么样切换此种形式的标识符以后,首先得介绍 Babel 表示和处理返回值的方式。假如你对 Babel 外部的作用机制不感兴趣,能间接跳过下一节。

Babel 是甚么样生成返回值(Scope)的

我们之前分析过 Babel 的校对流程,@babel/parser 包会将标识符生成 AST,@babel/traverse 遍历那个 AST,并调用 Babel 应用流程修改 AST。在这两者之间,只不过还有两个阶段。那是根据 AST 生成返回值(Scope)。

Scope 并不是两个实体的概念,所以生成 AST 的这时候并不会同时生成 Scope。Scope 那个对象生成后也不会挂载到 AST 上。而是挂载在 NodePath 上,这样也方便应用流程查看 Scope 的信息。Scope 能做的事情有很多。比如我们在添加两个新的引用时须要确保新增加的引用名字和已有的所有引用不冲突。或是我们希望找出这些没被引用的表达式,用于缩减标识符体积。

Scope 能被表示为如下表所示形式:

{ path: path, block: path.node, parentBlock: path.parent, parent: parentScope, bindings: […] }

Scope 中除了一些常规对象,如当前的路径(path),节点(block)外,最最重要的是 bindings,表示该返回值内收集到的所有的表达式。

单个 binding 的结构如下表所示,包括表达式的类型,是否是常量,表达式是否被引用,表达式被引用的次数以及被引用的路径。有了 bindings 数据,我们就能精确的分析当前返回值下的表达式。

{ identifier: node, scope: scope, path: path, kind: var, referenced: true, references: 3, referencePaths: [path, path, path], constant: false, constantViolations: [path] }

我们详细看下 bindings 的生成过程。表达式的绑定并不是一件单纯的事情,须要结合语法考虑。Babel 中绑定表达式采用了和应用流程一样的方式,那就是表述了两个出访者,来收集表达式。那个出访者叫做 collectorVisitor。

packages/babel-traverse/src/scope/index.js

path.traverse(collectorVisitor, state);

collectorVisitor 中规定了在某种语法下,表达式应该绑定到哪里,以及某个表达式被这些 path 引用等等。它和应用流程的写法是一样的,通过 path.traverse 方式遍历 AST 的过程中继续执行。

const collectorVisitor = { For(path) {…}, Declaration(path) {…}, AssignmentExpression(path, state) {…}, UpdateExpression(path, state) {…}, UnaryExpression(path, state) {…}, BlockScoped(path) {…}, Block(path) {…}, Function(path) {…}, };

我们从中挑取一些规则来看看。以下是 for 循环的规则,这里的 For 并不仅仅表示 for 循环,能在 packages/babel-types/src/definitions/core.js 文件中看到,For 只不过是 ForOfStatement,ForInStatement,ForStatement 的别名。所以这条规则针对的是 for,for of 和 for in 语法。

在这三种语法中表述的表达式(指的是for() 括号中表述的表达式),假如表述的这时候采用的是 var,那么该表达式的返回值属于 for 语句所处的表达式,假如没这样两个表达式,那么它属于父级的 Program,能认为是自上而下返回值。

For(path) { // 遍历 for 下属节点 for (const key of (t.FOR_INIT_KEYS: Array)) { const declar = path.get(key); // 表达式言明是 var if (declar.isVar()) { // 父表达式返回值或自上而下返回值 const parentScope = path.scope.getFunctionParent() || path.scope.getProgramParent(); // 绑定表达式 parentScope.registerBinding(“var”, declar); } } },

再看下 BlockScoped,BlockScoped 表示 function a(){}, class A{} 以及 let 形成的返回值。这三者我们来分析下。class A{} 会产生两个表达式 A,那 A 的返回值是甚么样的呢,ES6 中规定,假如两个表达式是个类,那么其返回值默认和 let 一致。所以,类的返回值绑定规则也和 let 一致。let 是块返回值,会向上找到第两个块,然后绑定在该语句所属的 path 上。function 也会绑定在上级的第两个块返回值中。

BlockScoped(path) { let scope = path.scope; if (scope.path === path) scope = scope.parent; const parent = scope.getBlockParent(); parent.registerDeclaration(path); if (path.isClassDeclaration() && path.node.id) { const id = path.node.id; const name = id.name; path.scope.bindings[name] = path.scope.parent.getBinding(name); } },

经过 collectorVisitor 后,Babel 获得了当前标识符中每个节点路径的返回值(Scope)对象,知道每个返回值下绑定了这些表达式,这些表达式是否被引用以及每个引用具体文本的路径。

let 是甚么样变为 var 的

我们以后讲过,由于 JS 在 ES6 以后,缺乏块返回值,导致 let 无法间接代替为 var。那那个难题是不是解决呢?

首先是换个表达式名。JS 的此种做法很巧妙。let 和 var 行为的不一致根源在于返回值,但引发此种现象的其原因大部分是表达式名的重复,假如我们能保证每个表达式名都不重复,能解决百分之九十的难题。

我们以后示例中的标识符,由于不同的块中都表述了 foo,导致了 let 和 var 的不一致,所以我们在某个块中遇到 let 言明时,判断它是否和某个函数调用是同名,假如是,那么将其重重新命名。如下表所示是正确的切换标识符。

if (true) { var foo = true; } if (true) { var _foo; if (_foo) { alert(_foo); } }

还有百分之十的难题,在于表达式名没办法换,或是说换表达式名的代价太高。我们举例说明。如下表所示是一道常见的面试题,当我们在 for 循环中采用 let 表述的这时候,由于 let 是块返回值,作用范围会在for循环内,每循环一次,i 会形成一次闭包,也就意味着 i 的值是会被保存的。结果是输出 0 1 2。而采用 var 的这时候,var 是函数调用,不会形成闭包,最终输出 3,3,3。

// 输出 0 1 2 for (let i = 0; i < 3; i++) { setTimeout(function () { console.log(i); }, 1000) } // 输出 333 for (var i = 0; i < 3; i++) { setTimeout(function () { console.log(i); }, 1000) }

此种情况下,let 甚么样采用 var 做代替呢?单纯的表达式代替是不行的,因为这是个 for 循环,我们总不能每循环一次,就生成两个新的表达式。此种情况下,就须要人为的生成闭包了,我们须要改写那个方式,将 for 循环内层的方式切换为闭包的写法。借助此种方式,for 循环外部的 i 被保存下来,同时实现了 let 相同的效果。如下表所示所示。

for (var i = 0; i < 3; i++) { (function (i) { setTimeout(function () { console.log(i); }, 1000); })(i) }

为的是好理解,我们一般把上述的写法拆分,写成如下表所示形式,这也是 Babel 中的标准切换方式。

var _loop = function (i) { setTimeout(function () { console.log(i); }, 1000); }; for (var i = 0; i < 3; i++) { _loop(i); }

所以 let 变为 var 规则单纯讲有三点。

let 处于自上而下返回值,那么 let 间接变为 var。let 处于块返回值,那就要判断该表达式升级至上级的表达式返回值或是自上而下返回值后,是否会与同一返回值的表达式发生重名现象,假如重名,那就换个表达式名。同时修改所有引用那个表达式的标识符。let 处于循环语句中,形成类似闭包的效果,此时须要将循环外部的方式切换为立即继续执行表达式的写法,利用闭包的特性保存循环次数。

接下来我们看下那个过程的同时实现方式,对 Babel 应用流程不感兴趣的同学能跳过间接看总结。

babel-plugin-transform-block-scoping 插件

切换的规则很用以理解,但同时实现起来比较复杂。须要考虑到 JS 的各种语法,以及切换导致的返回值的变化等等。babel-plugin-transform-block-scoping 应用流程是负责块作用域切换的应用流程。

如下表所示是该应用流程的结构。块级返回值主要是由于 let 和 const 言明导致的。但单单处理 VariableDeclaration 并不够。比如我们还须要收集块级返回值下的表达式,用于判断重名,判断是否形成闭包等等。

export default declare((api, opts) => { return { name: “transform-block-scoping”, visitor: { VariableDeclaration(path) {…}, Loop(path, state) {…}, CatchClause(path, state) {…}, “BlockStatement|SwitchStatement|Program”(path, state) {…}, }, }; });

在不考虑改名和闭包的难题时,只须要关注 VariableDeclaration,遇到表达式言明的这时候,判断该表达式是否是块级返回值,假如是不然,将其转变为自上而下或表达式返回值。

VariableDeclaration(path) { const { node, parent, scope } = path; // 判断是否是块级返回值 if (!isBlockScoped(node)) return; // 将块级返回值切换为 var 返回值(即自上而下或是表达式返回值) convertBlockScopedToVar(path, null, parent, scope, true); },

具体文本的切换方式是,首先将 let 代替为 var,由于以后的返回值是绑定在块级返回值上的,既然早已代替为 var,那绑定的返回值也须要做相应的调整。获得上级的表达式返回值或是全局返回值,然后将移动当前表达式。只不过是在当前返回值上删除绑定,然后在新返回值上增加两个绑定。

这部分的标识符如下表所示所示。

function convertBlockScopedToVar( path, node, parent, scope, moveBindingsToParent = false, ) { node[t.BLOCK_SCOPED_SYMBOL] = true; // let 代替为 var node.kind = “var”; // Move bindings from current block scope to function scope. if (moveBindingsToParent) { // 获得表达式作用已或是自上而下返回值 const parentScope = scope.getFunctionParent() || scope.getProgramParent(); // 遍历当前块返回值上绑定的表达式 for (const name of Object.keys(path.getBindingIdentifiers())) { const binding = scope.getOwnBinding(name); if (binding) binding.kind = “var”; // 移动表达式 scope.moveBindingTo(name, parentScope); } } }

再看呵呵 Loop,也是循环的处理方式,babel-plugin-transform-block-scoping 应用流程为的是复用一些逻辑,封装了两个 BlockScoping 对象,用于处理块返回值的切换和改变。

Loop(path, state) { const { parent, scope } = path; path.ensureBlock(); // BlockScoping 对象是块返回值对象 // 封装了块返回值的切换方式 const blockScoping = new BlockScoping( path, path.get(“body”), parent, scope, throwIfClosureRequired, tdzEnabled, state, ); const replace = blockScoping.run(); if (replace) path.replaceWith(replace); }

对于两个循环,我们的处理方式有三种,没表达式名称冲突就不变,有冲突就重重新命名,假如块作用域中的表达式被外部表达式引用,也是形成了闭包,那就须要改写方式。这部分的逻辑非常复杂。为的是方便理解我们仅分析流程,细节须要自己去看。首先是 getLetReferences 方式,该方式中会分析该 path 下言明的表达式,并以 map 的方式记录下来,用于后续判断,为的是判断是否形成闭包,该方式中有表述了两个出访者,出访方式外部表达式,假如外部表达式引用了外部的 let,认为存在闭包。假如存在闭包不然,继续执行 wrapClosure 方式,也是给当前标识符包一层方式,形成闭包,假如不存在闭包,继续执行 remap 方式。remap 方式中会判断表达式名称是否冲突(指的是同一返回值下发生冲突),假如冲突不然,继续执行 scope.rename() 方式更换名称。

run() { // 收集返回值表达式,判断是否形成闭包 const needsClosure = this.getLetReferences(); // 如果块返回值恰好是表达式返回值或是自上而下返回值,就不须要额外的变化了。 if (t.isFunction(this.parent) || t.isProgram(this.block)) { this.updateScopeInfo(); return; } // 假如形成闭包,改写标识符,否则重重新命名 if (needsClosure) { this.wrapClosure(); } else { this.remap(); } }

BlockStatement|SwitchStatement|Program 的处理方式和 Loop 的思路是一致的,标识符也是完全复用,就不赘述了。

babel-plugin-transform-block-scoping 应用流程是 Babel 的应用流程中算是比较复杂的应用流程,复杂的其原因在于其表面虽然是语法的变化,但本质确是返回值的变化,而返回值的改变须要非常的小心,否则很可能将导致切换前后的语义变化。责任编辑中举了一些 let 切换为 var 的例子,但这些示例是不够的,有很多特殊的场景并没覆盖,我们在 babel-plugin-transform-block-scoping 应用流程源码下的 test 文件下能看到那个应用流程的测试文件,这里大概有五十个测试文件,也就意味着有五十多种情形须要切换。有兴趣的小伙伴自行查看。

总结

let 和 const 是 ES6 新增的表达式言明URL, 和 var 的主要差别在于返回值的不同,var 全力支持自上而下返回值和表达式返回值,而 let 和 const 是块返回值。Babel 采用 babel-plugin-transform-block-scoping 应用流程完成 let 到 var 的切换,本质是块返回值到表达式返回值或自上而下返回值的切换。

Babel 在生成 AST 后,表述了两个出访者(visitor),用于遍历 AST 的过程中,根据语法规则生成返回值对象(Scope),该对象中表述了该返回值下绑定的表达式,这些表达式是否被引用以及每个引用具体文本的路径。这些信息是后续修改返回值以及表达式更换绑定的基础。

let 间接变为 var 很容易导致语义变化,须要结合返回值和上下文来进行判断,假如变为 var 后,和同层返回值的表达式名称发生冲突,能修改名称解决。假如 let 被内层返回值的方式引用,则须要修改标识符,形成闭包。

babel-plugin-transform-block-scoping 应用流程是 Babel 中负责块级返回值切换的应用流程,同时实现了收集返回值下表达式,判断是否存在闭包,let 节点修改等功能。我们在自己编写应用流程的这时候,最好能多看几个 Babel 的原生应用流程,不仅能介绍 Babel 校对的原理,还能学一些 AST 操作的方式。

责任编辑涉及到的一些 Babel 应用流程相关的知识,假如有疑问想多介绍呵呵 Babel,能阅读以下文章。

Babel 校对流程分析Babel AST 生成之路甚么样写两个 Babel 应用流程Babel 应用流程是甚么样生效的

假如您觉得有所收获,请点个赞吧!

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务