如果你有babel相关知识基础建议直接跳过 前置知识 部分,直接前往 "插件编写" 部分。
学习babel, 必备知识就是理解AST。
那什么是AST呢?
先来看下维基百科的解释:
在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构
"源代码语法结构的一种抽象表示
" 这几个字要划重点,是我们理解AST的关键,说人话就是按照某种约定好的规范,以树形的数据结构把我们的代码描述出来,让js引擎和转译器能够理解。
举个例子:就好比现在框架会利用虚拟dom
这种方式把真实dom
结构描述出来再进行操作一样,而对于更底层的代码来说,AST就是用来描述代码的好工具。
当然AST不是JS特有的,每个语言的代码都能转换成对应的AST, 并且AST结构的规范也有很多, js里所使用的规范大部分是 estree[1] ,当然这个只做简单了解即可。
了解了AST的基本概念, 那AST到底长啥样呢?
astexplorer.net[2]这个网站可以在线生成AST, 我们可以在里面进行尝试生成AST,用来学习一下结构
问:把冰箱塞进大象有几个阶段?
打开冰箱 -> 塞进大象 -> 关上冰箱
babel也是如此,babel利用AST的方式对代码进行编译,首先自然是需要将代码变为AST,再对AST进行处理,处理完以后呢再将AST 转换回来
也就是如下的流程
code转换为AST -> 处理AST -> AST转换为code
然后我们再给它们一个专业一点的名字
解析 -> 转换 -> 生成
通过 parser 把源码转成抽象语法树(AST)
这个阶段的主要任务就是将code转为AST, 其中会经过两个阶段,分别是词法分析和语法分析。当parse阶段开始时,首先会进行文档扫描,并在此期间进行词法分析。那怎么理解此法分析呢 如果把我们所写的一段code比喻成句子,词法分析所做的事情就是在拆分这个句子。如同 “我正在吃饭”
这句话,可以被拆解为“我”
、“正在”
、“吃饭”
一样, code也是如此。比如: const a = '1' 会被拆解为一个个最细粒度的单词(tokon): 'const', 'a', '=', '1' 这就是词法分析阶段所做的事情。
词法分析结束后,将分析所得到的 tokens 交给语法分析, 语法分析阶段的任务就是根据 tokens 生成 AST。它会对 tokens 进行遍历,最终按照特定的结构生成一个 tree 这个 tree 就是 AST。
如下图, 可以看到上面语句的到的结构,我们找到了几个重要信息, 最外层是一个VariableDeclaration
意思是变量声明,所使用的类型是 const
, 字段declarations
内还有一个 VariableDeclarator
[变量声明符] 对象,找到了 a
, 1
两个关键字。
除了这些关键字以为,还可以找到例如行号等等的重要信息,这里就不一一展开阐述。总之,这就是我们最终得到的 AST 模样。
那问题来了,babel里该如何将code 转为 AST 呢?在这个阶段我们会用到 babel 提供的解析器 @babel/parser,之前叫 Babylon,它并非由babel团队自己开发的,而是基于fork的 acorn 项目。
它为我们提供了将code转换为AST的方法,基本用法如下:
file
更多信息可以访问官方文档查看@babel/parser[3]
在 parse 阶段后,我们已经成功得到了AST。babel接收到 AST后,会使用 @babel/traverse 对其进行深度优先遍历,插件会在这个阶段被触发,以vistor 函数的形式访问每种不同类型的AST节点。以上面代码为例, 我们可以编写 VariableDeclaration
函数对 VariableDeclaration
节点进行访问,每当遇到该类型节点时都会触发该方法。如下:
file
该方法接受两个参数,
path为当前访问的路径, 并且包含了节点的信息、父节点信息以及对节点操作许多方法。可以利用这些方法对 ATS 进行添加、更新、移动和删除等等。
state包含了当前plugin的信息和参数信息等等,并且也可以用来自定义在节点之间传递数据。
generate:把转换后的 AST 打印成目标代码,并生成 sourcemap
这个阶段就比较简单了, 在 transform 阶段处理 AST 结束后,该阶段的任务就是将 AST 转换回 code, 在此期间会对 AST 进行深度优先遍历,根据节点所包含的信息生成对应的代码,并且会生成对应的sourcemap。
俗话说,最好的学习就是动手,我们来一起尝试一个简单的经典案例:将上面案例中的 es6 的 const
转变为 es5 的 var
使用 @babel/parser
生成AST
比较简单,跟上面的案例是一样的, 此时我们ast变量中就是转换后的 AST
const parser = require('@babel/parser');
const ast = parser.parse('const a = 1');
复制代码
使用 @babel/traverse
处理 AST
在这个阶段我们通过分析所生成的 AST 结构,确定了在 VariableDeclaration
中由 kind
字段控制 const
,所以我们是不是可以尝试着把 kind
改写成我们想要的 var
?既然如此,我们来尝试一下
file
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default
const ast = parser.parse('const a = 1');
traverse(ast, {
VariableDeclaration(path, state) {
// 通过 path.node 访问实际的 AST 节点
path.node.kind = 'var'
}
});
复制代码
好,此时我们凭借着猜想修改了 kind
,将其改写为了 var
, 但是我们还不能知道实际是否有效,所以我们需要将其再转换回 code 看看效果。
使用 @babel/generator
处理 AST
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default
const generate = require('@babel/generator').default
const ast = parser.parse('const a = 1');
traverse(ast, {
VariableDeclaration(path, state) {
path.node.kind = 'var'
}
});
// 将处理好的 AST 放入 generate
const transformedCode = generate(ast).code
console.log(transformedCode)
复制代码
我们再来看看效果:
file
执行完成,成功了,是我们想要的效果~
通过上面这个经典案例, 大概了解了 babel 的使用,但我们平时的插件该如何去写呢?
实际上插件的开发和上面的基本思路是一样的, 只是作为插件我们只需要关注这其中的 转换
阶段
我们的插件需要导出一个函数/对象, 如果是函数则需要返回一个对象, 我们只需要在改对象的 visitor 内做同样的事情即可,并且函数会接受几个参数, api继承了babel提供的一系列方法, options 是我们使用插件时所传递的参数,dirname 为处理时期的文件路径。
以上面的案例改造为如下:
module.exports = {
visitor: {
VariableDeclaration(path, state) {
path.node.kind = 'var'
}
}
}
// 或是函数形式
module.exports = (api, options, dirname) => {
return {
visitor: {
VariableDeclaration(path, state) {
path.node.kind = 'var'
}
}
}
}
复制代码
在有前置知识的基础上,我们来一步步的讲解开发一个 babel 插件。首先我们明确接下来要开发的插件的核心需求:
基本效果展示如下:
处理前
// log 声明需要被插入并被调用的方法
// @inject:log
function fn() {
console.log(1)
// 用 @inject:code指定插入行
// @inject:code
console.log(2)
}
复制代码
处理后
// 导入包 xxx 之后要在插件参数内提供配置
import log from 'xxx'
function fn() {
console.log(1)
log()
console.log(2)
}
复制代码
了解了大概的需求,先不着急动手,我们要先想想要怎么开始做,已经设想一下过程中需要处理的问题。
为了提升插件的灵活度,我们需要设计一个较为合适的参数规则。插件参数接受一个对象。
key 作为插入函数的函数名。
kind 表示导入形式。有三种导入方式 named 、 default、 namespaced, 此设计参考 babel-helper-module-imports[4]
named 对应 import { a } from "b"
形式
default 对应 import a from "b"
形式
namespaced 对应 import * as a from "b"
形式
require 为依赖的包名
比如,我需要插入 log
方法,它需要从 log4js
这个包里导入,并且是以 named
形式, 参数便为如下形式。
// babel.config.js
module.exports = {
plugins: [
// 填写我们的plugin的js 文件地址
['./babel-plugin-myplugin.js', {
log: {
// 导入方式为 named
kind: 'named',
require: 'log4js'
}
}]
]
}
复制代码
好,知道了具体要做什么事情并且设计好了参数的规则, 我们就可以开始动手了。
首先我们进入 astexplorer.net/[5] 将待处理的 code 生成 AST 方便我们梳理结构, 然后我们在进行具体编码
首先是函数声明语句,我们分析一下其 AST 结构以及该如何处理, 来看一下demo
// @inject:log
function fn() {
console.log('fn')
}
复制代码
其生成的 AST 结构如下,可以看到有比较关键的两个属性:
@inject:log
console.log('fn')
此时就在里面,我们等会代码的插入操作就是需要操作它file
好,知道了可以通过 leadingComments
来获知函数是否需要被插入, 对 body
操作可以实现我们的代码插入需求。。
首先我们得先找到 FunctionDeclaration
这一层,因为只有这一层才有 leadingComments
属性, 然后我们需要遍历它,匹配出需要插入的函数。再将匹配到的函数插入至 body 只中, 但我们这里需要注意可插入的body 所在层级, FunctionDeclaration
内的body 他不是一个数组而是 BlockStatement
,这表示函数的函数体,并且它也有body , 所以我们实际操作位置就在这个BlockStatement
的 body 内
file
代码如下:
module.exports = (api, options, dirname) => {
return {
visitor: {
// 匹配函数声明节点
FunctionDeclaration(path, state) {
// path.get('body') 相当于 path.node.body
const pathBody = path.get('body')
if(path.node.leadingComments) {
// 过滤出所有匹配 @inject:xxx 字符 的注释
const leadingComments = path.node.leadingComments.filter(comment => /\@inject:(\w+)/.test(comment.value) )
leadingComments.forEach(comment => {
const injectTypeMatchRes = comment.value.match(/\@inject:(\w+)/)
// 匹配成功
if( injectTypeMatchRes ) {
// 匹配结果的第一个为 @inject:xxx 中的 xxx , 我们将它取出来
const injectType = injectTypeMatchRes[1]
// 获取插件参数的 key, 看xxx 是否在插件的参数中声明过
const sourceModuleList = Object.keys(options)
if( sourceModuleList.includes(injectType) ) {
// 搜索body 内部是否有 @code:xxx 注释
// 因为无法直接访问到 comment,所以需要访问 body内每个 AST 节点的 leadingComments 属性
const codeIndex = pathBody.node.body.findIndex(block => block.leadingComments && block.leadingComments.some(comment => new RegExp(`@code:\s?${injectType}`).test(comment.value) ))
// 未声明则默认插入位置为第一行
if( codeIndex === -1 ) {
// 操作`BlockStatement` 的 body
pathBody.node.body.unshift(api.template.statement(`${state.options[injectType].identifierName}()`)());
}else {
pathBody.node.body.splice(codeIndex, 0, api.template.statement(`${state.options[injectType].identifierName}()`)());
}
}
}
})
}
}
}
})
复制代码
编写完后我们看看结果, log
成功被插入了, 因为我们没有使用 @code:log
所以就默认插入在了第一行
然后我们试试使用 @code:log
标识符, 我们将 demo 的代码改为如下
// @inject:log
function fn() {
console.log('fn')
// @code:log
}
复制代码
再次运行代码查看结果, 确实是在 @code:log
位置成功插入了
处理完了我们第一个案例函数声明,这时候可能有人会问了, 那箭头函数这种没有函数体的你怎么办, 比如:
// @inject:log
() => true
复制代码
这有问题吗?没有问题!
file
没有函数体我们给它一个函数体就是了,怎么做呢?
首先我们还是先学会来分析一下 AST 结构, 首先看到最外层其实是一个ExpressionStatement
表达式声明,然后其内部才是 ArrowFunctionExpression
箭头函数表达式, 可见跟我们之前的函数声明生成的结构是大有不同, 其实我们不用被这么多层结构迷了眼睛,我们只需要找对我们有用的信息就可以了,一句话:哪一层有 leadingComments 我们就找哪一层。这里的 leadingComments
在 ExpressionStatement
上,所以我们找它就行
file
分析完了结构,那怎么判断是否有函数体呢?还记得上面处理函数声明时,我们在 body 中看到的 BlockStatement
吗,而你看到我们箭头函数的 body 却是 BooleanLiteral
。所以,我们可以判断其 body 类型来得知是否有函数体 具体方法可以使用babel 提供的类型判断方法 path.isBlockStatement()
来区分是否有函数体。
module.exports = (api, options, dirname) => {
return {
visitor: {
ExpressionStatement(path, state) {
// 访问到 ArrowFunctionExpression
const expression = path.get('expression')
const pathBody = expression.get('body')
if(path.node.leadingComments) {
// 正则匹配 comment 是否有 @inject:xxx 字符
const leadingComments = path.node.leadingComments.filter(comment => /\@inject:(\w+)/.test(comment.value) )
leadingComments.forEach(comment => {
const injectTypeMatchRes = comment.value.match(/\@inject:(\w+)/)
// 匹配成功
if( injectTypeMatchRes ) {
// 匹配结果的第一个为 @inject:xxx 中的 xxx , 我们将它取出来
const injectType = injectTypeMatchRes[1]
// 获取插件参数的 key, 看xxx 是否在插件的参数中声明过
const sourceModuleList = Object.keys(options)
if( sourceModuleList.includes(injectType) ) {
// 判断是否有函数体
if (pathBody.isBlockStatement()) {
// 搜索body 内部是否有 @code:xxx 注释
// 因为无法直接访问到 comment,所以需要访问 body内每个 AST 节点的 leadingComments 属性
const codeIndex = pathBody.node.body.findIndex(block => block.leadingComments && block.leadingComments.some(comment => new RegExp(`@code:\s?${injectType}`).test(comment.value) ))
// 未声明则默认插入位置为第一行
if( codeIndex === -1 ) {
pathBody.node.body.unshift(api.template.statement(`${injectType}()`)());
}else {
pathBody.node.body.splice(codeIndex, 0, api.template.statement(`${injectType}()`)());
}
}else {
// 无函数体情况
// 使用 ast 提供的 `@babel/template` api , 用代码段生成 ast
const ast = api.template.statement(`{${injectType}();return BODY;}`)({BODY: pathBody.node});
// 替换原本的body
pathBody.replaceWith(ast);
}
}
}
})
}
}
}
}
}
复制代码
可以看到除了新增的函数体判断,生成函数体插入代码再用新的 AST 替换原本的节点,除掉这些之外,大体上的逻辑跟之前的函数声明的处理过程没有区别。
生成 AST 所使用的
@babel/template
的 API 相关用法可以查看文档 @babel/template[6]
针对不同情况的下的函数大体上相同,总结就是:
分析 AST 找到 leadingComments
所在节点 -> 找到可插入的 body 所在节点 -> 编写插入逻辑
实际处理的情况还有很多,如:对象属性、iife、函数表达式等很多, 处理思路都是一样的,这里就不过重复阐述。我会将插件完整代码发在文章底部。
第一条完成了,那需求的第二条,我们使用的包如何自动引入呢, 如上面案例使用的 log4js
, 那么我们处理后的代码就应该自动加上:
import { log } from 'log4js'
复制代码
此时,我们可以思考一下,我们需要处理以下两种情况
针对 问题1 我们需要先检索一下是否有导入过 log4js
,并且以 named
的形式导入了 log
针对 问题2 我们需要给 log
一个唯一的别名, 并且要保证在后续的代码插入中也使用这个别名。所以这就要求了我们要在文件的一开始就处理完成自动引入的逻辑。
有了大概的思路,但是我们如何提前完成自动引入逻辑呢。抱着疑问,我们再来看看 AST 的结构。可以看到 AST 最外层是 File
节点, 他有一个 comments
属性,它包含了当前文件里所有的注释,有了这个我们就可以解析出文件里需要插入的函数,并提前进行引入。我们再往下看, 内部是一个 Program
, 我们将首先访问它, 因为它会在其他类型的节点之前被调用,所以我们要在此阶段实现自动引入逻辑。
小知识:babel 提供了 path.traverse 方法,可以用来同步访问处理当前节点下的子节点。
如图:
代码如下:
const importModule = require('@babel/helper-module-imports');
// ......
{
visitor: {
Program(path, state) {
// 拷贝一份options 挂在 state 上, 原本的 options 不能操作
state.options = JSON.parse(JSON.stringify(options))
path.traverse({
// 首先访问原有的 import 节点, 检测 log 是否已经被导入过
ImportDeclaration (curPath) {
const requirePath = curPath.get('source').node.value;
// 遍历options
Object.keys(state.options).forEach(key => {
const option = state.options[key]
// 判断包相同
if( option.require === requirePath ) {
const specifiers = curPath.get('specifiers')
specifiers.forEach(specifier => {
// 如果是默认type导入
if( option.kind === 'default' ) {
// 判断导入类型
if( specifier.isImportDefaultSpecifier() ) {
// 找到已有 default 类型的引入
if( specifier.node.imported.name === key ) {
// 挂到 identifierName 以供后续调用获取
option.identifierName = specifier.get('local').toString()
}
}
}
// 如果是 named 形式的导入
if( option.kind === 'named' ) {
//
if( specifier.isImportSpecifier() ) {
// 找到已有 default 类型的引入
if( specifier.node.imported.name === key ) {
option.identifierName = specifier.get('local').toString()
}
}
}
})
}
})
}
});
// 处理未被引入的包
Object.keys(state.options).forEach(key => {
const option = state.options[key]
// 需要require 并且未找到 identifierName 字段
if( option.require && !option.identifierName ) {
// default形式
if( option.kind === 'default' ) {
// 增加 default 导入
// 生成一个随机变量名, 大致上是这样 _log2
option.identifierName = importModule.addDefault(path, option.require, {
nameHint: path.scope.generateUid(key)
}).name;
}
// named形式
if( option.kind === 'named' ) {
option.identifierName = importModule.addNamed(path, key, option.require, {
nameHint: path.scope.generateUid(key)
}).name
}
}
// 如果没有传递 require 会认为是全局方法,不做导入处理
if( !option.require ) {
option.identifierName = key
}
})
}
}
}
复制代码
Program
节点内我们先将接收到的插件配置 options
拷贝了一份,挂到了 state
上, 之前有说过 state
可以用作 AST 节点之间的数据传递,然后我们首先访问 Program
下的 ImportDeclaration
也就是 import
语句, 看看 log4js
是否有被导入过, 若引入过便会记录到 identifierName
字段上,完成对 import
语句的访问后,我们就可根据 identifierName
字段判断是否已被引入,若未引入则使用 @babel/helper-module-imports[7] 创建 import
,并用 babel 提供的 generateUid
方法创建唯一的变量名。
这样在之前的代码我们也需要略微调整, 不能直接使用从注释 @inject:xxx
提取出的方法名, 而是应该使用 identifierName
, 关键部分代码修改如下:
if( sourceModuleList.includes(injectType) ) {
// 判断是否有函数体
if (pathBody.isBlockStatement()) {
// 搜索body 内部是否有 @code:xxx 注释
// 因为无法直接访问到 comment,所以需要访问 body内每个 AST 节点的 leadingComments 属性
const codeIndex = pathBody.node.body.findIndex(block => block.leadingComments && block.leadingComments.some(comment => new RegExp(`@code:\s?${injectType}`).test(comment.value) ))
// 未声明则默认插入位置为第一行
if( codeIndex === -1 ) {
// 使用 identifierName
pathBody.node.body.unshift(api.template.statement(`${state.options[injectType].identifierName}()`)());
}else {
// 使用 identifierName
pathBody.node.body.splice(codeIndex, 0, api.template.statement(`${state.options[injectType].identifierName}()`)());
}
}else {
// 无函数体情况
// 使用 ast 提供的 `@babel/template` api , 用代码段生成 ast
// 使用 identifierName
const ast = api.template.statement(`{${state.options[injectType].identifierName}();return BODY;}`)({BODY: pathBody.node});
// 替换原本的body
pathBody.replaceWith(ast);
}
}
复制代码
最终效果如下:
file
我们实现了函数自动插入并自动引入依赖包。
本篇文章是对自己学习 “Babel 插件通关秘籍” 小册子后的一个记录总结,我开始和大部分想写babel插件却无从下手的同学一样,所以这篇文章主要也是按自己写插件时摸索的思路去写。希望也是能给大家提供一个思路。
完整版已支持 自定义代码片段
的插入,完整代码已上传至 github:https://github.com/nxl3477/babel-plugin-code-inject,同时也发布至了 npm:https://www.npmjs.com/package/babel-plugin-code-inject。 欢迎大家 star 和 issue。
给 star 是人情,不给是事故,哈哈。
file
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/AR6ioK_A8Z5g3YYMtlEmuQ
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。