接着上篇文章: 《深入浅出 Babel 上篇:架构和原理 + 实战 ????》 欢迎转载,让更多人看到我的文章,转载请注明出处

这篇文章干货不少于上篇文章,这篇我们深入讨论一下宏这个玩意 —— 我想我们对宏并不默认,因为很多程序员第一门语言就是 C/C++; 一些 Lisp 方言也支持宏(如 ClojureScheme), 听说它们的宏写起来很优雅;一些现代的编程语言对宏也有一定的支持,如 RustNimJuliaElixir,它们是如何解决技术问题, 实现类Lisp的宏系统的?宏在这些语言中扮演这什么角色...

如果没读过上篇文章,请先阅读一下,避免影响对本篇文章内容的理解。

文章大纲

  • 关于宏

    • 文本替换式

    • 语法扩展式

    • Sweet.js

    • 小结

  • 既生 Plugin 何生 Macro

  • 如何写一个 Babel Macro

    • 实战

  • 扩展资料

关于宏

Wiki 上面对‘宏’的定义是:宏(Macro), 是一种批处理的称谓,它根据一系列的预定义规则转换一定的文本模式。解释器编译器在遇到宏时会自动进行这一模式转换,这个转换过程被称为“宏展开(Macro Expansion)”。对于编译语言,宏展开在编译时发生,进行宏展开的工具常被称为宏展开器。

你可以认为,宏就是用来生成代码的代码,它有能力进行一些句法解析和代码转换。宏大致可以分为两种: 文本替换和语法扩展

文本替换式

大家或多或少有接触过宏,很多程序员第一门语言是C/C++(包括C的衍生语言Objective-C), 在C中就有宏的概念。使用#define指令定义一个宏:

#define MIN(X, Y) ((X) < (Y) ? (X) : (Y))

如果我们的程序使用了这个宏,就会在编译阶段被展开,例如:

MIN(a + b, c + d)

会被展开为:

((a + b) < (c + d) ? (a + b) : (c + d)

除了函数宏C 中还有对象宏, 我们通常使用它来声明'常量':

#define PI 3.1214

如上图,宏本质上不是C语言的一部分, 它由C预处理器提供,预处理器在编译之前对源代码进行文本替换,生成‘真正’的 C 代码,再传递给编译器。

当然 C 预处理器不仅仅会处理宏,它还包含了头文件引入、条件编译、行控制等操作

除此之外,GNU m4是一个更专业/更强大/更通用的预处理器(宏展开器)。这是一个通用的宏展开器,不仅可以用于 C,也可以用于其他语言和文本文件的处理(参考这篇有趣的文章:《使用 GNU m4 为 Markdown 添加目录支持》), 关于m4可以看让这世界再多一份 GNU m4 教程 系列文章.

文本替换式宏很容易理解、实现也简单,因为它们只是纯文本替换, 换句话说它就像‘文本编辑器’。所以相对而言,这种形式的宏能力有限,比如它不会检验语法是否合法, 使用它经常会出现问题。

所以随着现代编程语言表达能力越来越强,很多语言都不再推荐使用宏/不提供宏,而是使用语言本身的机制(例如函数)来解决问题,这样更安全、更容易理解和调试。没用宏机制,现代语言可以通过提供强大的反射机制或者动态编程特性(如Javascript的Proxy、Python的装饰器)来弥补缺失宏导致的元编程短板。所以反过来推导,之所以C语言需要宏,正是因为C语言的表达能力太弱了。

语法扩展式

‘真正’的宏起源于Lisp. 这个得益于Lisp语言本身的一些特性:

  • 它的语法非常简单。只有S-表达式(s-expression)(特征为括号化的前缀表示法, 可以认为S-表达式就是近似的 Lisp 的抽象语法树(AST))

  • 数据即代码。S-表达式本身就是树形数据结构。另外 Lisp 支持数据和代码之间的转换

由于 Lisp 这种简单的语法结构,使得数据和程序之间只有一线之隔(quote修饰就是数据, 没有quote就是程序), 换句话说就是程序和数据之间可以灵活地转换。这种数据即程序、程序即数据的概念,使得Lisp可以轻松地自定义宏. 不妨来看一下Lisp定义宏的示例:

(defmacro nonsense (function-name)  `(defun ,(intern (concat "nonsense-" function-name)) (input)     (print (concat ,function-name input))))

如果你不理解上面程序的含义,这里有一个Javascript的实现

应用宏展开:

(nonsense "apple")(nonsense-apple " is good")

对于Lisp而言,宏有点像一个函数, 只不过这个函数必须返回一个quoted数据; 当调用这个宏时,Lisp会使用unquote函数将宏返回的quoted数据转换为程序

通过上面的示例,你会感叹Lisp的宏实现竟然如此清奇,如此简单。搞得我想跟着题叶学一波Clojure,但是后来我学了Elixir ????.

Lisp宏的灵活性得益于简单的语法(S-表达式可以等价于它的AST),对于复杂语法的语言(例如Javascript),要实现类似Lisp的宏就难得多. 因此很少有现代语言提供宏机制可能也是这个原因。

尽管如此,现在很多技术难点慢慢被解决,很多现代语言也引入'类' Lisp的宏机制,如Rust、Julia, 还有Javascript的 Sweet.js

Sweet.js

Sweet.js 和 Rust 师出同门,所以两个的宏语法和非常接近(初期)。不过需要注意的是: 官方认为 Sweet.js 目前仍处于实验阶段,而且Github最后提交时间停留在2年前,社区上也未见大规模的使用。所以不要在生产环境中使用它,但是不妨碍我们去学习一个现代编程语言的宏机制。

我们先使用 Sweet.js 来实现上面我们通过 Lisp 实现的nosense宏, 对比起来更容易理解:

import { unwrap, fromIdentifier, fromStringLiteral } from '@sweet-js/helpers' for syntax;

syntax nosense = function (ctx) {  let name = ctx.next().value;  let funcName = 'nonsense' + unwrap(name).value

  return #`function ${fromIdentifier(name, funcName)} () {    console.log(${fromStringLiteral(name, unwrap(name).value)} + input)  }`;};

nosense ApplenosenseApple(" is Good") // Apple is Good

首先,Sweet.js使用syntax关键字来定义一个宏,其语法类似于const或者let

本质上一个宏就是一个函数, 只不过在编译阶段被执行. 这个函数接收一个 TransformerContext 对象,你也通过这个对象获取宏应用传入的语法对象(Syntax Object)数组,最终这个宏也要返回语法对象数组。

什么是语法对象?语法对象是 Sweet.js 关于语法的内部表示, 你可以类比上文Lisp的 quoted 数据。在复杂语法的语言中,没办法使用 quoted 这么简单的序列来表示语法,而使用 AST 则更复杂,开发者更难以驾驭。所以大部分宏实现会参考 Lisp 的S-表达式,取折中方案,将传入的程序转换为Tokens,再组装成类似quoted的数据结构。

举个例子,Sweet.js 会将 foo,bar('baz', 1)转换成这样的数据结构:

从上图可知,Sweet.js 会将传入的程序解析成嵌套的Token序列,这个结构和Lisp的S-表达式非常相似。也就是, 说对于闭合的词法单元会被嵌套存储,例如上例的('baz', 1).

Elixir 也采用了类似的quote/unquote机制,可以结合着一起理解

TransformerContext实现了迭代器方法,所以我们通过调用它的next()来遍历获取语法对象。最后宏必须返回一个语法对象数组,Sweet.js 使用了类似字符串模板的语法(称为语法模板)来简化开发,这个模板最终转换为语法对象数组。

需要注意的是语法模板的内嵌值只能是语法对象、语法对象序列或者TransformerContext.

旧版本使用了模式匹配,和Rust语法类似,我个人更喜欢这个,不知为何废弃了

说了这么多,类似Sweet.js 语法对象 的设计是现代编程语言为了贴近 Lisp 宏的一个关键技术点。我发现ElixirRust等语言也使用了类似的设计。除了数据结构的设计,现代编程语言的宏机制还包含以下特性:

1️⃣ 卫生宏(Hygiene)

卫生宏指的是在宏内生成的变量不会污染外部作用域,也就是说,在宏展开时,Sweet.js 会避免宏内定义的变量和外部冲突.

举个例子,我们创建一个swap宏,交换变量的值:

syntax swap = (ctx) => { const a = ctx.next().value ctx.next() // 吃掉',' const b = ctx.next().value return #` let temp = ${a} ${a} = ${b} ${b} = temp `;}

swap foo,bar

展开会输出为

let temp_10 = foo;foo = bar;bar = temp_10;

如果你想引用外部的变量,也可以。不过不建议这么做,宏不应该假定其被展开的上下文:

syntax swap = (ctx) => {  // ...  return #`  temp = ${a} // 不使用 let 声明  ${a} = ${b}  ${b} = temp  `;}

2️⃣ 模块化

Sweet.js 的宏是模块化的:

'lang sweet.js';

export syntax class = function (ctx) {

};

导入:

import { class } from './es2015-macros';

class Droid {  constructor(name, color) {    this.name = name;    this.color = color;  }

  rollWithIt(it) {    return this.name + " is rolling with " + it;  }}

相对Babel(编译器)来说,Sweet.js的宏是模块化/显式的。Babel你需要在配置文件中配置各种插件和选项,尤其是团队项目构建有统一规范和环境时,项目构建脚本修改可能有限制。而模块化的宏是源代码的一部分,而不是构建脚本的一部分,这使得它们可以被灵活地使用、重构以及废弃。

下文介绍的 babel-plugin-macros 最大的优势就在这里, 通常我们希望构建环境是统一的、稳定的、开发人员应该专注于代码的开发,而不是如何去构建程序,正是因为代码多变性,才催生出了这些方案。

需要注意的是宏是在编译阶段展开的,所以无法运行用户代码,例如:

let log = msg => console.log(msg);

syntax m = ctx => {

  log('doing some Sweet things');

};

Sweet.js 和其他语言的宏一样,有了它你可以:

  • 新增语法糖(和Sweet.js 一样甜), 实现复合自己口味的语法或者某些实验性的语言特性

  • 自定义操作符, 很强大

  • 消灭重复的代码,提升语言的表达能力。

  • ...

  • 别炫技

????很遗憾!Sweet.js 基本死了。所以现在当个玩具玩玩尚可,切勿用于生产环境。即使没有死,Sweet.js 这种非标准的语法, 和现有的Javascript工具链生态格格不入,开发和调试都会比较麻烦(比如Typescript).

归根到底,Sweet.js 的失败,是社区抛弃了它。Javascript语言表达能力越来越强,版本迭代快速,加上有了Babel和Typescript这些解决方案,实在拿不出什么理由来使用 Sweet.js

Sweet.js 相关论文可以看这里

小结

这一节扯得有点多,将宏的历史和分类讲了个遍。最后的总结是Elixir官方教程里面的一句话:显式好于隐式,清晰的代码优于简洁的代码(Clear code is better than concise code)

能力越大、责任越大。宏强大,比正常程序要更难以驾驭,你可能需要一定的成本去学习和理解它, 所以能不用宏就不用宏,宏是应该最后的法宝.

既生 Plugin 何生 Macro

????还没完, 一下子扯了好远,掰回正题。既然 Babel 有了 Plugin 为什么又冒出了个 babel-plugin-macros?

如果你尚不了解Babel Macro,可以先读一下官方文档, 另外Creact-React-APP 已经内置

这个得从 Create-React-App(CRA) 说起,CRA 将所有的项目构建逻辑都封装在react-scripts 服务中。这样的好处是,开发者不需要再关心构建的细节, 另外构建工具的升级也变得非常方便, 直接升级 react-scripts即可。

如果自己维护构建脚本的话,升一次级你需要升级一大堆的依赖,如果你要维护跨项目的构建脚本,那就更蛋疼了。

我在《为什么要用vue-cli3?》 里阐述了 CRA 以及 Vue-cli这类的工具对团队项目维护的重要性。

CRA 是强约定的,它是按照React社区的最佳实践给你准备的,为了保护封装带来的红利,它不推荐你去手动配置Webpack、Babel... 所以才催生除了 babel-plugin-macros, 大家可以看这个 Issue: RFC - babel-macros

所以为 Babel 寻求一个'零配置'的机制是 babel-plugin-macros 诞生的主要动机。

这篇文章正好证实了这个动机:《Zero-config code transformation with babel-plugin-macros》, 这篇文章引述了一个重要的观点:"Compilers are the New Frameworks"

的确,Babel 在现代的前端开发中扮演着一个很重要的角色,越来越多的框架或库会创建自己的 Babel 插件,它们会在编译阶段做一些优化,来提高用户体验、开发体验以及运行时的性能。比如:

  • babel-plugin-lodash 将lodash导入转换为按需导入

  • babel-plugin-import 上篇文章提过的这个插件,也是实现按需导入

  • babel-react-optimize 静态分析React代码,利用一定的措施优化运行效率。比如将静态的props或组件抽离为常量

  • root-import 将基于根目录的导入路径重写为相对路径

  • styled-components 典型的CSS-in-js方案,利用Babel 插件来支持服务端渲染、预编译模板、样式压缩、清除死代码、提升调试体验。

  • preval 在编译时预执行代码

  • babel-plugin-graphql-tag 预编译GraphQL查询

  • ...

上面列举的插件场景中,并不是所有插件都是通用的,它们要么是跟某一特定的框架绑定、要么用于处理特定的文件类型或数据。这些非通用的插件是最适合使用macro取代的。

用 preval 举个例子. 使用插件形式, 你首先要配置插件:

{  "plugins": ["preval"]}

代码:

const greeting = preval`  const fs = require('fs')  module.exports = fs.readFileSync(require.resolve('./greeting.txt'), 'utf8')`

使用Macro方式:

import preval from 'preval.macro'

const greeting = preval`  const fs = require('fs')  module.exports = fs.readFileSync(require.resolve('./greeting.txt'), 'utf8')`

这两者达到的效果是一样的,但意义却不太一样。有哪些区别?

  • 1️⃣ 很显然,Macro不需要配置.babelrc(当然babel-plugin-macros这个基座需要装好). 这个对于CRA这种不推荐配置构建脚本的工具来说很有帮助

  • 2️⃣ 由隐式转换为了显式。上一节就说了“显式好于隐式”。你必须在源代码中通过导入语句声明你使用了 Macro;而基于插件的方式,你可能不知道preval这个标识符哪里来的? 如何被应用?何时被应用?而且通常你还需要和其他工具链的配合,例如ESlint、Typescript声明等等。

    Macro 由代码显式地应用,我们更明确它被应用的目的和时机,对源代码的侵入性最小。因为中间多了 babel-plugin-macro 这一层,我们降低了对构建环境的耦合,让我们的代码更方便被迁移。

  • 3️⃣ Macro相比Plugin 更容易被实现。因为它专注于具体的 AST 节点,见下文

  • 4️⃣ 另外,当配置出错时,Macro可以得到更好的错误提示

有利有弊,Babel Macro 肯定也有些缺陷,例如相对于插件来说只能显式转换,这样代码可能会比较啰嗦,不过个人觉得在某些场景利大于弊, 能显式的就显式。

那么Babel Macro也是宏?相对于 Sweet.js 这些'正统'的宏机制有哪些不足?

  • 首先 Babel Macro 必须是合法的 Javascript 语法。不支持自定义语法,也要分两面讨论,合法的Javascript语法不至于打破现有的工具协作链,如果允许用户毫无限制地创建新的语法,将来指不定会和标准的语法发生歧义。反过来不能自定义语法的‘宏’,是否显得不太地道,不够'强大'?

  • 因为必须是合法的Javascript语法,Babel Macro 实现DSL(Domain-specific languages)能力就弱化了

  • 再者,Babel Macro 和 Babel Plugin没有本质的区别,相比Sweet.js提供了显式定义和应用宏的语法,Babel Macro直接操作 AST 则要复杂得多,你还是需要了解一些编译原理,这把一般的开发者挡在了门外。

Babel 可以实现自定义语法,只不过你需要Fork @babel/parser, 对它进行改造(可以看这篇文章《精读《用 Babel 创造自定义 JS 语法》》)。这个有点折腾,不太推荐

总之,Babel Macro 本质上和Babel Plugin没有什么区别,它只是在Plugin 之上封装了一层(分层架构模式的强大),创建了一个新的平台,让开发者可以在源代码层面显式地应用代码转换。所以,任何适合显式去转换的场景都适合用Babel Macro来做:

  • 特定框架、库的代码转换。如 styled-components

  • 动态生成代码。preval

  • 特定文件、语言的处理。例如graphql-tag.macroyaml.macrosvgr.macro

  • ... (查看awesome-babel-macros)

如何写一个 Babel Macro

所以,Babel Macro是如何运作的呢? babel-plugin-macros 要求开发者必须显式地导入 Macro,它会遍历匹配所有导入语句,如果导入源匹配/[./]macro(\.js)?$/正则,就会认为你在启用Macro。例如下面这些导入语句都匹配正则:

import foo from 'my.macro'import { bar } from './bar/macro'import { baz as _baz} from 'baz/macro.js'

Ok, 当匹配到导入语句后,babel-plugin-macros就会去导入你指定的 macro 模块或者npm包(Macro 即可以是本地文件,也可以是公开的 npm 包, 或者是npm包中的子路径)。

那么 macro 文件里面要包含什么内容呢?如下:

const { createMacro } = require('babel-plugin-macros')

module.exports = createMacro(({references, state, babel}) => {

})

macro 文件必须默认导出一个由 ceateMacro 创建的实例, 在其回调中可以获取到一些关键对象:

  • babel 和普通的Babel插件一样,Macro 可以获取到一个 babel-core 对象

  • state 这个我们也比较熟悉,Babel 插件的 visitor 方法的第二个参数就是它, 我们可以通过它获取一些配置信息以及保存一些自定义状态

  • references 获取 Macro 导出标识符的所有引用。上一篇文章介绍了作用域,你应该还没忘记绑定和引用的概念。如下

假设用户这样子使用你的 Macro:

import foo, {bar, baz as Baz} from './my.macro' 

foo(1)foo(2)

bar`by tagged Template`;<Baz>by JSX</Baz>

那么你将拿到references结构是这样的:

{  default: [NodePath, NodePath],  bar: [NodePath],  baz: [NodePath],}

查看详细开发指南
AST Explorer 也支持 babel-plugin-macros,可以玩一下. 下面的实战实例,也建议在这里探索一下

接下来你就可以遍历references, 对这些节点进行转换,实现你想要的宏功能。开始实战!

实战

这一次我们模范preval 创建一个eval.macro Macro, 利用它在编译阶段执行(eval)一些代码。例如:

import evalm from 'eval.macro'const x = evalm`function fib(n) {  const SQRT_FIVE = Math.sqrt(5);  return Math.round(1/SQRT_FIVE * (Math.pow(0.5 + SQRT_FIVE/2, n) - Math.pow(0.5 - SQRT_FIVE/2, n)));}

fib(20)`const x = 6765

创建 Macro 文件. 按照上一节的介绍,① 我们使用createMacro来创建一个 Macro实例, ② 并从references 中拿出所有导出标识符的引用路径, ③接着就是对这些引用路径进行AST转换:

const { createMacro, MacroError } = require('babel-plugin-macros')

function myMacro({ references, state, babel }) {

  const { default: defaultImport = [] } = references;

  defaultImport.forEach(referencePath => {    if (referencePath.parentPath.type === "TaggedTemplateExpression") {      const val = referencePath.parentPath.get("quasi").evaluate().value      const res = eval(val)      const ast = objToAst(res)      referencePath.parentPath.replaceWith(ast)    } else {

      throw new MacroError('只支持标签模板字符串, 例如:evalm`1`')    }  });}

module.exports = createMacro(myMacro);

为了行文简洁,本案例中只支持标签模板字符串 形式调用,但是标签模板字符串中可能包含内插的字符串,例如:

hello`hello world ${foo} + ${bar + baz}`

其 AST 结构如下:

我们需要将 TaggedTemplateExpression 节点转换为字符串。手动去拼接会很麻烦,好在每个 AST 节点的 Path 对象都有一个evaluate 方法,这个方法可以对节点进行‘静态求值’:

t.evaluate(parse("5 + 5"))t.evaluate(parse("!true"))

t.evaluate(parse("foo + foo"))

因此这样子的标签模板字符串是无法求值的:

evalm`1 + ${foo}` evalm`1 + ${bar(1)}`

这个和 Typescript 的 enum, 还有一些编译语言的常量是一样的,它们在编译阶段被求值,只有一些原始值以及一些原始值的表达式才支持在编译阶段被求值.

So,上面的代码还不够健壮,我们再优化一下,在求值失败时给用户更好的提示:

  defaultImport.forEach(referencePath => {    if (referencePath.parentPath.type === "TaggedTemplateExpression") {      const evaluated = referencePath.parentPath.get("quasi").evaluate();

      if (!evaluated.confident) {        throw new MacroError("标签模板字符串内插值只支持原始值和原始值表达式");      }

      try {        const res = eval(evaluated.value);        const ast = objToAst(res);

        referencePath.parentPath.replaceWith(ast);      } catch (err) {        throw new MacroError(`求值失败: ${err.message}`);      }    } else {      throw new MacroError("只支持标签模板字符串, 例如:evalm`1 + 1`");    }  });

接下来将执行后的值转换为 AST,然后替换掉TaggedTemplateExpression:

  function objToAst(res) {    let str = JSON.stringify(res);    if (str == null) {      str = "undefined";    }    const variableDeclarationNode = babel.template(`var x = ${str}`, {})();

    return variableDeclarationNode.declarations[0].init;  }

这里@babel/template 就派上用场了,它可以将字符串代码解析成 AST,当然直接使用parse方法解析也是可以的。

Ok, 文章到这里基本结束了。本文对‘宏’进行了深入的讨论,从 C 语言的文本替换宏到濒死的Sweet.js, 最后介绍了babel-plugin-macros.

Babel Macro 本质上还是Babel 插件,只不过它是模块化的,你要使用它必须显式地导入。和‘正统’宏相比, Babel Macro 直接操作 AST,需要你掌握编译原理, ‘正统’宏可以实现的东西, Babel Macro也可以实现(例如卫生宏). 虽然相比Babel插件略有简化,还是比较啰嗦。另外Babel Macro 不能创建新的语法,这使得它可以和现有的工具生态保持兼容。

最后!打开脑洞 ????,Babel Macro 可以做很多有意思的东西,查看《Awesome babel macros》。不过要谨记:‘显式好于隐式,清晰的代码优于简洁的代码’

文章转自:https://juejin.im/post/5da12397e51d4578364f6ffa

 好文 推 荐 

☞ 深入浅出 Babel 上篇:架构和原理 + 实战

☞ 大规模应用 TypeScript

☞ 揭开 JavaScript 引擎的面纱

好文章,我 在看 

深入浅出 Babel 下篇:既生 Plugin 何生 Macros相关推荐

  1. 小学生计算机舞蹈,最近“泼水成画”很火?舞蹈生VS体育生,看到计算机:你是来添乱的?...

    最近泼水拍照非常的流行,不知道大家在私底下有没有关注过这个视频,而且在这个视频中,这些花放在水里确实也特别的好看,接下来就一起来看一下,不同的学生拍出来的泼水照片都是什么样的. 首先大家看到的就是舞蹈 ...

  2. 既生瑜何生亮 access_token VS refresh_token

    中国有句老话, 既生瑜何生亮, 既然有我周瑜在世, 为什么老天还要一个诸葛亮啊? 同样的, 众所周知, 在 OAuth 2.0 授权协议中, 也有两个令牌 token , 分别是 access_tok ...

  3. excel如何找到高频词_拟录取后:应届生和往届生档案哪里找;重灾院校区;高频词背诵表...

    今日消息1.应届生和往届生档案哪里找?2.重灾院校区3.考研云督学班高频词背诵表汇总1.应届生和往届生档案哪里找? 往年这个时候论文答辩.复试已经结束,已经进入毕业季!现在你们毕业答辩结束了吗?你们都 ...

  4. python 数学基础_Python3数学基础 - 随笔分类 - 既生喻何生亮 - 博客园

    本系列主要集中于数学知识点,利用python编程描述以往学过的数学知识. 摘要:Kronecker delta 克罗内克函数 Wiki "维基百科" Kronecker delta ...

  5. 转贴:既生瑜何生亮:FreeBSD与Linux再比较

    原贴:http://www.phpchina.com/8051/viewspace_8240.html 传说中FreeBSD比linux稳定,大型网站几乎都建立在FreeBSD系统上,我一直疑惑难道l ...

  6. 计算机系男生生的都是女儿吗,IT男只能生女孩,生男孩几率很小吗?

    "IT男"即指男性网络编辑员.计算机维修工.数据库系统管理员.游戏程序开发师等,"IT"系信息技术.互联网技术.信息论等的缩写.据业内人士传辐射会降低精子活力, ...

  7. 应届生和往届生,报名条件区别汇总!

    即将预报名,今天给大家整理了一下应届生和往届生报名需要注意的点.填写信息和报名需要的材料,大家一定要认真对待. 1 关于应届生和往届生身份确认 应届生是指2022年的毕业生,含普通高校.成人高校.普通 ...

  8. 应届生和往届生,谁更容易考研成功?

    据教育部统计,2017年共201万人报考,其中,应届考生113万人,往届考生88万人:2018年共238万人报考,其中,应届考生131万人,往届考生107万人.从数据可知,2017年,往届生考研人数占 ...

  9. 计算机往届生考研失败找工作,终于发现应届生和往届生考研复试会被歧视吗-考研复习...

    对于20考研的学生来说,或许这一段时间比较的焦虑,一方面是初试成绩公布在即,另一方面,复试准备还在摸索中,有很多问题困扰着我们的考研大学生.而在诸多的复试问题中,其中关于应届生和往届生二者之间,是否会 ...

最新文章

  1. 如何安装并使用Windows 8 Client Hyper-V
  2. 网页的手机版本是否值得去做?,互联网营销
  3. emacs 安装auto-complete
  4. java下载本地目录excel_java写简单Excel 首行是目录 然后前台下载
  5. 【Linux】一步一步学Linux——dmesg命令(74)
  6. SPF难以解决邮件伪造的现状以及方案
  7. CRM Fiori launchpad请求响应结果的字段分析
  8. set和multiset集合容器
  9. P5704 【深基2.例6】字母转换(python实现)
  10. Qt文档阅读笔记-写一个简单的单元测试
  11. 计算机基础【面试遇到】
  12. Spark on Yarn查看删除日志
  13. JSTL核心标签库详解
  14. 零压力入门算法的顶流畅销书《漫画算法》施展了哪些“魔法”?
  15. 十二月份找工作好找吗_人民大学在职研究生将来好找工作吗?
  16. Atitit.计算机图形图像图片处理原理与概论attilax总结
  17. 使用R语言进行时间序列分析
  18. java 高级笔试题_JAVA高级工程师笔试题及答案
  19. 软件工程专业的论文答辩_软件工程毕业论文答辩PPT模板
  20. lumion自动保存_lumion 保存在哪里? 我想在家里做 白天带到公司做 怎么操作 保存文件可以带走的吗?...

热门文章

  1. Vue项目报错npm ERR code 1
  2. 高等代数-三阶特征根、特征向量求解详细过程
  3. 苦涩又难懂的io<3>
  4. 这一篇TCP总结,请务必收下!
  5. 数据结构课程设计 ——考试报名系统
  6. 常用的计算机病毒检测方法都有哪些?
  7. 视频人像抠图论文阅读
  8. c语言n阶方阵,如何用C语言编出一个N阶螺旋方阵?
  9. 富途网络科技测试笔试题
  10. 多智能体强化学习(MARL)训练环境总结