算法刷题
大厂口试照旧很注重算法题的,尤其是字节跳动,算法是问的比力多的,关于算法,保举《LeetCode》和《算法的乐趣》,这两本我也有电子版,字节跳动、阿里、美团等大厂口试题(含答案+解析)、学习笔记、Xmind思维导图均可以分享给各人学习。
开源分享:【大厂前端口试题解析+焦点总结学习笔记+真实项目实战+最新解说视频】
写在最后
最后,对所以做Java的朋友提几点发起,也是我的个民气得:
- 疯狂编程
- 学习效果可视化
- 写博客
- 阅读优秀代码
- 心态调解
注意Preset的执行次序相反,详见官方文档
节点的上下文
访问者在访问一个节点时, 会无差别地调用 enter 方法,我们怎么知道这个节点在什么位置以及和其他节点的关联关系呢?
通过上面的代码,读者应该可以猜出几分,每个visit方法都吸收一个 Path 对象, 你可以将它当做一个‘上下文’对象,雷同于JQuery的 JQuery(const $el = $(‘.el’)) 对象,这里面包含了很多信息:
- 当前节点信息
- 节点的关联信息。父节点、子节点、兄弟节点等等
- 作用域信息
- 上下文信息
- 节点操作方法。节点增删查改
- 断言方法。isXXX, assertXXX
下面是它的主要结构:
export class NodePath<T = Node> {
constructor(hub: Hub, parent: Node);
parent: Node;
hub: Hub;
contexts: TraversalContext[];
data: object;
shouldSkip: boolean;
shouldStop: boolean;
removed: boolean;
state: any;
opts: object;
skipKeys: object;
parentPath: NodePath;
context: TraversalContext;
container: object | object[];
listKey: string; // 如果节点在一个数组中,这个就是节点数组的键
inList: boolean;
parentKey: string;
key: string | number; // 节点地点的键或索引
node: T; // 当前节点
scope: Scope; // 当前节点地点的作用域
type: T extends undefined | null ? string | null : string; // 节点类型
typeAnnotation: object;
// … 另有很多方法,实现增删查改
}
复制代码
你可以通过这个手册来学习怎么通过 Path 来转换 AST. 反面也会有代码示例,这里就不展开细节了
副作用的处置处罚
实际上访问者的工作比我们想象的要复杂的多,上面树模的是静态 AST 的遍历过程。而 AST 转换本身是有副作用的,比如插件将旧的节点更换了,那么访问者就没有必要再向下访问旧节点了,而是继承访问新的节点, 代码如下。
traverse(ast, {
ExpressionStatement(path) {
// 将 `console.log(‘hello’ + v + ‘!’)` 更换为 `return ‘hello’ + v`
const rtn = t.returnStatement(t.binaryExpression(‘+’, t.stringLiteral(‘hello’), t.identifier(‘v’)))
path.replaceWith(rtn)
},
}
复制代码
上面的代码, 将console.log(‘hello’ + v + ‘!’)语句更换为return “hello” + v;, 下图是遍历的过程:
我们可以对 AST 进行任意的操作,比如删除父节点的兄弟节点、删除第一个子节点、新增兄弟节点… 当这些操作’污染’了 AST 树后,访问者需要记录这些状态,相应式(Reactive)更新 Path 对象的关联关系, 包管精确的遍历次序,从而得到精确的转译效果。
作用域的处置处罚
访问者可以确保精确地遍历和修改节点,但是对于转换器来说,另一个比力棘手的是对作用域的处置处罚,这个责任落在了插件开发者的头上。插件开发者必须非常谨慎地处置处罚作用域,不能粉碎现有代码的执行逻辑。
const a = 1, b = 2
function add(foo, bar) {
console.log(a, b)
return foo + bar
}
复制代码
比如你要将 add 函数的第一个参数 foo 标识符修改为a, 你就需要递归遍历子树,查出foo标识符的所有引用, 然后更换它:
traverse(ast, {
// 将第一个参数名转换为a
FunctionDeclaration(path) {
const firstParams = path.get(‘params.0’)
if (firstParams == null) {
return
}
const name = firstParams.node.name
// 递归遍历,这是插件常用的模式。这样可以制止影响到外部作用域
path.traverse({
Identifier(path) {
if (path.node.name === name) {
path.replaceWith(t.identifier(‘a’))
}
}
})
},
})
console.log(generate(ast).code)
// function add(a, bar) {
// console.log(a, b);
// return a + bar;
// }
复制代码
慢着,似乎没那么简朴,更换成 a 之后, console.log(a, b) 的举动就被粉碎了。所以这里不能用 a,得换个标识符, 譬如c.
这就是转换器需要考虑的作用域问题,AST 转换的前提是包管步调的精确性。 我们在添加和修改引用时,需要确保与现有的所有引用不辩说。Babel本身不能检测这类异常,只能依靠插件开发者谨慎处置处罚。
Javascript采用的是词法作用域, 也就是根据源代码的词法结构来确定作用域:
在词法区块(block)中,由于新建变量、函数、类、函数参数等创建的标识符,都属于这个区块作用域. 这些标识符也称为绑定(Binding),而对这些绑定的使用称为引用(Reference)
在Babel中,使用Scope对象来体现作用域。 我们可以通过Path对象的scope字段来获取当前节点的Scope对象。它的结构如下:
{
path: NodePath;
block: Node; // 所属的词法区块节点, 比方函数节点、条件语句节点
parentBlock: Node; // 所属的父级词法区块节点
parent: Scope; // ⚛️指向父作用域
bindings: { [name: string]: Binding; }; // ⚛️ 该作用域下面的所有绑定(即该作用域创建的标识符)
}
复制代码
Scope 对象和 Path 对象差不多,它包含了作用域之间的关联关系(通过parent指向父作用域),网络了作用域下面的所有绑定(bindings), 另外还提供了丰富的方法来对作用域仅限操作。
我们可以通过bindings属性获取当前作用域下的所有绑定(即标识符),每个绑定由Binding类来体现:
export class Binding {
identifier: t.Identifier;
scope: Scope;
path: NodePath;
kind: “var” | “let” | “const” | “module”;
referenced: boolean;
references: number; // 被引用的数目
referencePaths: NodePath[]; // ⚛️获取所有应用该标识符的节点路径
constant: boolean; // 是否是常量
constantViolations: NodePath[];
}
复制代码
通过Binding对象我们可以确定标识符被引用的情况。
Ok,有了 Scope 和 Binding, 如今有本领实现安全的变量重命名转换了。 为了更好地展示作用域交互,在上面代码的基础上,我们再增加一下难度:
const a = 1, b = 2
function add(foo, bar) {
console.log(a, b)
return () => {
const a = ‘1’ // 新增了一个变量声明
return a + (foo + bar)
}
}
复制代码
如今你要重命名函数参数 foo, 不仅要考虑外部的作用域, 也要考虑下级作用域的绑定情况,确保这两者都不辩说。
上面的代码作用域和标识符引用情况如下图所示:
来吧,接受挑衅,试着将函数的第一个参数重新命名为更短的标识符:
// 用于获取唯一的标识符
const getUid = () => {
let uid = 0
return () => `_${(uid++) || ‘’}`
}
const ast = babel.parseSync(code)
traverse(ast, {
FunctionDeclaration(path) {
// 获取第一个参数
const firstParam = path.get(‘params.0’)
if (firstParam == null) {
return
}
const currentName = firstParam.node.name
const currentBinding = path.scope.getBinding(currentName)
const gid = getUid()
let sname
// 循环找出没有被占用的变量名
while(true) {
sname = gid()
// 1️⃣首先看一下父作用域是否已界说了该变量
if (path.scope.parentHasBinding(sname)) {
continue
}
// 2️⃣ 查抄当前作用域是否界说了变量
if (path.scope.hasOwnBinding(sname)) {
// 已占用
continue
}
// 再查抄第一个参数的当前的引用情况,
// 如果它地点的作用域界说了同名的变量,我们也得放弃
if (currentBinding.references > 0) {
let findIt = false
for (const refNode of currentBinding.referencePaths) {
if (refNode.scope !== path.scope && refNode.scope.hasBinding(sname)) {
findIt = true
break
}
}
if (findIt) {
continue
}
}
break
}
// 开始更换掉
const i = t.identifier(sname)
currentBinding.referencePaths.forEach(p => p.replaceWith(i))
firstParam.replaceWith(i)
},
})
console.log(generate(ast).code)
// const a = 1,
// b = 2;
// function add(_, bar) {
// console.log(a, b);
// return () => {
// const a = ‘1’; // 新增了一个变量声明
// return a + (_ + bar);
// };
// }
复制代码
上面的例子虽然没有什么实用性,而且另有Bug(没考虑label),但是正好可以展现了作用域处置处罚的复杂性。
Babel的 Scope 对象实在提供了一个generateUid方法来生成唯一的、不辩说的标识符。我们使用这个方法再简化一下我们的代码:
traverse(ast, {
FunctionDeclaration(path) {
const firstParam = path.get(‘params.0’)
if (firstParam == null) {
return
}
let i = path.scope.generateUidIdentifier(‘_’) // 也可以使用generateUid
const currentBinding = path.scope.getBinding(firstParam.node.name)
currentBinding.referencePaths.forEach(p => p.replaceWith(i))
firstParam.replaceWith(i)
},
})
复制代码
能不能再短点!
traverse(ast, {
FunctionDeclaration(path) {
const firstParam = path.get(‘params.0’)
if (firstParam == null) {
return
}
let i = path.scope.generateUid(‘_’) // 也可以使用generateUid
path.scope.rename(firstParam.node.name, i)
},
})
复制代码
查看generateUid的实现代码
generateUid(name: string = “temp”) {
name = t
.toIdentifier(name)
.replace(/^_+/, “”)
.replace(/[0-9]+$/g, “”);
let uid;
let i = 0;
do {
uid = this._generateUid(name, i);
i++;
} while (
this.hasLabel(uid) ||
this.hasBinding(uid) ||
this.hasGlobal(uid) ||
this.hasReference(uid)
);
const program = this.getProgramParent();
program.references[uid] = true;
program.uids[uid] = true;
return uid;
}
复制代码
非常简洁哈?作用域操作最典型的场景是代码压缩,代码压缩会对变量名、函数名等进行压缩… 然而实际上很少的插件场景需要跟作用域进行复杂的交互,所以关于作用域这一块就先讲到这里。
搞一个插件呗
等等别走,还没完呢,这才到2/3。学了上面得了知识,总得写一个玩具插件试试水吧?
如今打算模拟babel-plugin-import, 写一个极简版插件,来实现模块的按需导入. 在这个插件中,我们会将雷同这样的导入语句:
import {A, B, C as D} from ‘foo’
复制代码
转换为:
import A from ‘foo/A’
import ‘foo/A/style.css’
import B from ‘foo/B’
import ‘foo/B/style.css’
import D from ‘foo/C’
import ‘foo/C/style.css’
复制代码
首先通过 AST Explorer 看一下导入语句的 AST 节点结构:
通过上面展示的效果,我们需要处置处罚 ImportDeclaration 节点类型,将它的specifiers拿出来遍历处置处罚一下。另外如果用户使用了默认导入语句,我们将抛堕落误,提醒用户不能使用默认导入.
基本实现如下:
// 要识别的模块
const MODULE = ‘foo’
traverse(ast, {
// 访问导入语句
ImportDeclaration(path) {
if (path.node.source.value !== MODULE) {
return
}
// 如果是空导入则直接删撤除
const specs = path.node.specifiers
if (specs.length === 0) {
path.remove()
return
}
// 判定是否包含了默认导入和命名空间导入
if (specs.some(i => t.isImportDefaultSpecifier(i) || t.isImportNamespaceSpecifier(i))) {
// 抛堕落误,Babel会展示堕落的代码帧
throw path.buildCodeFrameError(“不能使用默认导入或命名空间导入”)
}
// 转换命名导入
const imports = []
for (const spec of specs) {
const named = MODULE + ‘/’ + spec.imported.name
const local = spec.local
imports.push(t.importDeclaration([t.importDefaultSpecifier(local)], t.stringLiteral(named)))
imports.push(t.importDeclaration([], t.stringLiteral(`${named}/style.css`)))
}
// 更换原有的导入语句
path.replaceWithMultiple(imports)
}
})
复制代码
逻辑还算简朴,babel-plugin-import可比这复杂得多。
接下来,我们将它封装成标准的 Babel 插件。 按照规范,我们需要创建一个babel-plugin-*前缀的包名:
mkdir babel-plugin-toy-import
cd babel-plugin-toy-import
yarn init -y
touch index.js
复制代码
你也可以通过 generator-babel-plugin 来生成项目模板.
在 index.js 文件中填入我们的代码。index.js默认导出一个函数,函数结构如下:
// 接受一个 babel-core 对象
export default function(babel) {
const {types: t} = babel
return {
pre(state) {
// 前置操作,可选,可以用于准备一些资源
},
visitor: {
// 我们的访问者代码将放在这里
ImportDeclaration(path, state) {
// …
}
},
post(state) {
// 后置操作,可选
}
}
}
复制代码
我们可以从访问器方法的第二个参数state中获取用户传入的参数。假设用户设置为:
{
plugins: [[‘toy-plugin’, {name: ‘foo’}]]
}
复制代码
我们可以这样获取用户传入的参数:
export default function(babel) {
const {types: t} = babel
return {
visitor: {
ImportDeclaration(path, state) {
const mod = state.opts && state.opts.name
if (mod == null) {
return
}
// …
}
},
常用的JavaScript计划模式
函数
- 函数的界说
- 局部变量和全局变量
- 返回值
- 匿名函数
- 自运行函数
- 闭包
开源分享:【大厂前端口试题解析+焦点总结学习笔记+真实项目实战+最新解说视频】
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |