深入探究Prototype与原型链

Prototype和_proto_的区别

  • Prototype属性(原型对象)。是函数(Function)独有的属性,非函数类对象没有这个属性。作用是,作为一个“公共仓库”,用来放置希望所有实例共享的属性和方法。
  • _proto_属性(隐式原型指针)。JavaScript 中的每一个对象(包含函数、数组、实例等)都有。作用是,指向创建该对象的构造函数的 prototype。它就是对象用来“向上查找”的连接线。

一个函数对象拆解后的实际样子

要彻底搞懂一个函数对象在内存和浏览器控制台里的完整拆解结构,我们可以将它划分为 4 大核心板块。

在 Chrome 控制台中运行 console.dir(Foo) 并展开后,你能看到它的全部内部组成:

// 1. 打印函数对象(查看它的 prototype 和 [[Prototype]])
function Foo() {}
Foo.prototype.sayHi = function() {};

console.dir(Foo);

函数对象的 4 大结构板块

第一块:函数自身的基础元数据(自身属性)

  • length: 0
    • 含义: 函数的形参个数。因为 Foo() 没有写入参,所以是 0。
  • name: "Foo"
    • 含义: 函数的名字字符串。

第二块:prototype(给 future 实例准备的共享仓库)

展开 ▼ prototype: 这一段,就是将来使用 new Foo() 生产出的实例对象能共享的所有内容:

  • sayHi: ƒ ()
    • 含义: 挂载在 Foo.prototype 上的方法。
  • constructor: ƒ Foo()
    • 含义: 原型对象上的“反向指针”,表明这个原型仓库是属于 Foo 函数的。
  • ▼ [[Prototype]]: Object
    • 含义: Foo.prototype 本身也是一个普通的 JavaScript 对象,所以它的原型指向了最高顶层的 Object.prototype。

第三块:已废弃的旧语法属性与引擎标记

  • arguments: null / caller: null
    • 含义: 传统 JS 中用来获取“传入参数”和“调用当前函数的父级函数”的旧属性。在现代 ES6 严格模式下已被禁用,所以显示为 null。
  • [[FunctionLocation]]: prototype.html:10
    • 含义: 浏览器引擎的标记,提示这个函数是在代码文件 prototype.html 的第 10 行被定义的。

第四块:▼ [[Prototype]]: ƒ ()(Foo 函数自己继承的能力)

这是最关键的隐式原型指针(即 Foo.__proto__)。因为函数本身也是一个对象,所以它也需要继承“作为函数”的能力,这里指向的是 Function.prototype:

  • apply: ƒ apply() / bind: ƒ bind() / call: ƒ call()
    • 含义: 函数独有的能力。你平时能写 Foo.call() 或 Foo.apply(),就是因为在这个原型层级里找到了这些内置方法。
  • constructor: ƒ Function()
    • 含义: 说明函数 Foo 是由 JavaScript 内置的 Function 构造函数创建出来的。
  • toString: ƒ toString()
    • 含义: 将函数源码转为字符串的方法(如 Foo.toString())。
  • ▼ [[Prototype]]: Object
    • 含义: Function.prototype 本身也是个对象,它的原型同样指向顶级 Object.prototype。
  • ▼ [[Scopes]]: Scopes[0]
    • 含义: Function.prototype 的作用域链。

第五块:▼ [[Scopes]]: Scopes[1](Foo 函数的作用域链)

  • 0: Global {window: ...}
    • 含义: 引擎记录的当前函数生效的上下文环境。这里显示它能访问全局的 window 对象。如果是闭包函数,这里还会看到 Closure 作用域。

在控制台里看函数,记住两行关键展开项即可:

  1. 寻找 prototype, 看实例能调用的属性/方法(包含 sayHi)。
  2. 寻找 [[Prototype]], 看函数自己能调用的属性/方法(包含 call/apply/bind)。

prototype、[[Prototype]]与_proto_的关系

用最简单的话描述它们关系:[[Prototype]] 是真正的内存槽,__proto__ 是访问它的后门通道,而 prototype 只是函数手里拿着的预留仓库。

1、 [[Prototype]] 的正式定义

[[Prototype]] 的正式名称是“对象的内部槽”(Internal Slot),更具体的称呼是“内部原型”。在 ECMAScript 官方规范中:

  • 特征: 双方括号 [[...]] 表示这是 规范内部的私有属性,在传统的 JS 代码中是无法直接像 obj.[[Prototype]] 这样书写和访问的。
  • 正式名称: [[Prototype]] 内部槽(Internal Slot)。
  • 含义: 它是存在于每个 JavaScript 对象内部、由引擎维护的一个私有槽位,用来保存该对象的原型引用(即指向上一级原型对象或 null)。

2、 __proto__ 与 [[Prototype]] 的关系

简而言之:__proto__ 只是访问和修改内部槽 [[Prototype]] 的一种“历史遗留通道/访问器”。

映射关系:

  • [[Prototype]] 是真正的原型存储槽(存在于内存深处)。
  • __proto__ 是挂在 Object.prototype 上的一个 getter / setter 访问器属性(属性拦截器)。当读取 obj.__proto__ 时,底层实际上是在执行 Object.getPrototypeOf(obj),去获取真正的 [[Prototype]]。

历史与规范地位:

  • 历史: __proto__ 最初只是浏览器厂商(主要是 Firefox/Chrome)私自添加的非标准扩展功能。
  • 现实: 到了 ES6,为了兼容网络上大量已经使用了 __proto__ 的老旧代码,官方规范不得不把它收录进标准,但明确将它放在了 附录 B(Annex B – 仅为了 Web 兼容性保留的特性) 中。

由于 __proto__ 性能较差(修改它会破坏引擎优化)且属于废弃边缘的附录规范,现代 JS 严格禁止在生产环境中使用 __proto__。

规范推荐使用以下 官方标准 API 来操作 [[Prototype]]:

操作需求不推荐(历史遗留)官方标准 API(推荐 )
获取 [[Prototype]]obj.__proto__Object.getPrototypeOf(obj)
修改 [[Prototype]]obj.__proto__ = newProtoObject.setPrototypeOf(obj, newProto)
创建时指定 [[Prototype]]–Object.create(proto)

3.三者核心属性速查表

假设我们定义一个构造函数,并用它创建一个实例:

function Person() {}
const p = new Person();

它们三者在内存中的关系可以通过这个联动公式串联起来:

实例 p 的内部槽           实例 p 的访问后门           构造函数的预留仓库
p.[[Prototype]]   ===   p.__proto__   ===   Person.prototype

完整的逻辑接力过程:

  1. Person.prototype 预备资源: 函数 Person 诞生时,引擎会自动给它配一个 prototype 对象。你可以在上面挂载方法(如 Person.prototype.sayHi = ...)。
  2. new 操作做连接: 当你执行 new Person() 时,引擎创建了实例 p,并把 p 内部的 [[Prototype]] 槽位直接指向了 Person.prototype。
  3. __proto__ 负责读取: 由于 [[Prototype]] 是私有槽位,在代码里写 p.[[Prototype]] 会报错,所以你可以通过 p.__proto__ 这个后门去读取或修改底层指向的 Person.prototype。
属性名称谁拥有它?它是什么?它的真实作用
[[Prototype]]所有对象(普通对象、函数、数组等)内部私有槽位(存储在引擎深处)真正的原型指针。原型链查找时,引擎就是沿着它一路向上找的。
__proto__所有对象(通过继承 Object.prototype 获得)访问器属性(getter/setter 后门)读取或修改 [[Prototype]] 的历史通道。只是一个指向 [[Prototype]] 的快捷接口。
prototype仅函数对象(普通对象没有)普通属性(挂在函数上的对象)给未来实例准备的模板仓库。当你 new Foo() 时,新对象的 [[Prototype]] 就会连到这里。

函数的双重身份与两条原型链

二、 两种查找路径的具体对比

为了彻底理清,我们看看这两条链在查找时到底有什么区别:

1. 当函数扮演“普通对象/方法本身”时(寻找 Foo.xxx)

此时,Foo 和一个 {} 没有任何本质区别,引擎直接从 Foo 自身的 [[Prototype]] 开始向上找:

  • 查找路线: Foo $\rightarrow$ Foo.[[Prototype]](指向 Bar 或 Function.prototype) $\rightarrow$ Object.prototype $\rightarrow$ null
  • 触发场景: Foo.call()、Foo.bind()、Foo.staticMethod()

2. 当函数扮演“构造工厂”时(给实例 f1 找 f1.xxx)

此时,Foo 只是一个“借用身份的工具人”,真正参与原型链查找的是它手里拿着的那个 Foo.prototype(一个纯普通对象):

  • 查找路线: f1 $\rightarrow$ Foo.prototype $\rightarrow$ Foo.prototype.[[Prototype]](指向 Bar.prototype) $\rightarrow$ Object.prototype $\rightarrow$ null
  • 触发场景: const f1 = new Foo(); f1.sayHi()

三、 总结:底层原理相同,只是“发起点”不同

所以,说“函数的原型链查找方式和普通对象根本不同”:

  • 底层机制上: 不同(完全相同)。 无论是函数还是普通对象,引擎在找属性时,永远只认“当前对象”的 [[Prototype]] 这一根线,没有任何特权或特殊的查找算法。
  • 应用逻辑上: 对(确实要区分)。 普通对象只有一条单向链;而函数因为既有 [[Prototype]] 又有 prototype 属性,它同时身处两条互相平行的原型链中。

彻底理解JavaScript中的对象

JS对象迷惑性:理解本质

对于C++、Java、C#面向对象的语言而言,对象这个词都是很明确的:“对象”必须以“类(Class)”为模板实例化而来。

而JS中的对象概念,则宽泛得多。例如,以下代码中的staff 、p都是对象,甚至函数Person也是对象:

//普通对象
const staff = {
    name: "Tom",
    age: 18
};

//Promise对象
const p = new Promise(...);

//函数也是一个对象
function Person(name) {
  this.name = name;
}

这不禁让人困惑,JavaScript的对象到底指的是什么?

因为中文把 Object 翻译成“对象”,听起来特别抽象,实际上:

JS 里的“对象”并不是指某一种长相,而是指一种“可以被当成一个整体来管理,并且可以通过属性名找到内部东西”的值。

“对象”本质上只是一个包含键值对(Key-Value)的动态哈希表。

很粗浅的理解,对象就是一个可以装很多“属性”的东西。

认识了这个本质,我们很快就能理解了,staff,p,Person 长相完全不同,但它们在 JavaScript 的底层分类上都属于 Object。以下都是JS中的对象,只是创建它们的“模板”不同而已:

JavaScript Object
│
├── 普通对象
├── Promise对象
├── Array对象
├── Date对象
├── RegExp对象
├── Map对象
└── …

创建对象有很多种方式

例如,普通对象:

 const a = {};

其它类型对象:

 //Promise 对象
 const b = new Promise(...);
 //Array 对象。
 const c = [];
 //Date 对象
 const d = new Date();
 //Map 对象
 const e = new Map();
 //RegExp 对象
 const f = /abc/;
  //甚至得到的是函数对象
 const g = function() {};

不要再把对象单一理解成:

 { name: “Tom”, age: 18 }

而应该理解成:

对象 = 一个可以拥有属性和方法、可以被独立操作的实体。

于是:

                  Object
                    │
        ┌───────────┼────────────┐
        ↓           ↓           ↓
      普通对象     Array       Promise
        │           │           │
    name/age     push/length   then/catch

它们虽然功能完全不同,但都属于“对象”这个大类别。

HTTP方法的深度探源与RESTful API的设计思想

HTTP认知溯源

HTTP方法本质上就是一个普通的 ASCII 编码字符串,我们可以自定义HTTP方法。从这一点看每个http方法没有什么区别。但是,在应用层,考虑到缓存与网络效能、容错与重试机制、安全隔离与攻击防御等,浏览器、路由器、防火墙、CDN节点、NGINX等应用对各方法做了区别对待。

HTTP方法是在安全和幂等的理念下设计的,语义化的命名有助于互联网生态的协作,在缓存、爬虫、搜索、CDN分发、前后端数据交互等方面能更低成本协作。而REST则是一种设计API的理念,即使用 URI 来标识资源,使用标准的 HTTP 方法 来表示操作。RESTful则是符合这种理念的API。

即使写程序很多年的老鸟,能真正说清楚道明白HTTP方法的也不多,多数都只会使用而不知道所以然。每个人都知道使用GET、POST方法,使用也很熟练,但是底层设计思想并不一定了解。这里,来认真梳理一下HTTP方法背后设计的逻辑。

一. HTTP方法常见10种

HTTP 标准里常见的10 个:

方法主要作用
GET获取资源
POST提交数据 / 创建资源
PUT完整替换资源
PATCH部分修改资源
DELETE删除资源
HEAD获取响应头,不返回正文
OPTIONS查询服务器支持哪些方法/能力
CONNECT建立隧道
TRACE请求回显,用于诊断
QUERY用于查询搜索(2026年RFC10008新增)

(实际上HTTP方法数量多达几十种,这里只是列举常见的方法,详见文末)

二.HTTP方法两个核心设计理念:安全(Safe) 与 幂等(Idempotent)


注意:以上的标准是2014年颁布的RFC 7231,QUETY标准出自2026颁布的RFC 10008

FastAPI + HarfBuzz + Redis实现动态字体子集化实战

字形编译引擎(Text Shaping Engine)当今的绝对王者HarfBuzz,拥有极强性能,Android、Chromium、Firefox等主流应用底层都在用它。HarfBuzz有一个hb-subset 子集化模块,是专业的字体子集化引擎:

当你给它一段文本(比如 "动态字体子集化")和原字体时,HarfBuzz 会:
自动分析这几个汉字对应的 Glyph ID。
保留这几个字所需的矢量描边数据。
剔除其余上万个未用到的字形数据。
重构并修复 .ttf 文件的 OpenType 内部索引表,确保输出的小字体符合国际标准,能在浏览器中被完美加载。

直观了解:HarfBuzz是做什么的?

官方是这样介绍的:


harfbuzz-world.cc is a single-file build of HarfBuzz that drops into your own C or C++ project without any build-system glue, plus a live playground for it right here in your browser:

harfbuzz-world.cc 提供了一个单文件版的 HarfBuzz(HarfBuzz 整个库整合成一个可以直接编译的 .cc 文件。),可以直接放进你自己的 C/C++ 项目中,无需额外处理构建系统;同时,它还在浏览器中提供了一个可以实时操作的 HarfBuzz 实验场。


shape — the shaped glyph stream as a table, alongside an SVG preview.(字形整形 / 字形塑形)
subset — produce a font subset for the current text and download it.(子集化)
raster — pixel-perfect BGRA rendering at any size, blitted to a canvas.(光栅化/位图化)
vector — the same shaped text rendered to SVG and downloadable PDF.
gpu — slug-based GPU rendering via WebGL2.(GPU 渲染)

官网地址:https://harfbuzz-world.cc/



OpenType(包括排版规则、连字、变形、字形定位表等)也是一套极其复杂的标准规范,而 HarfBuzz 就是这套标准在全世界最权威、最完美的执行引擎(“编译器/解析引擎”)。 很多人以为:计算机显示文本 = 查字典(给一个字符编码,找到对应的字体形状画出来)。 如果全世界只有英文字母(A-Z),这确实很简单。但在现实世界中,文字的显示极其复杂:

—— 复杂连字(Ligatures)与形态变化:阿拉伯文同一个字母,在词首、词中、词尾、单独存在时,形状完全不同,而且必须连笔。印地文/泰文: 辅音和元音组合时,元音符号可能会跑到辅音的上方、下方甚至左边。英文在优雅的排版中,f 和 i 连着写时,f 的横线和 i 的点会融合成一个整体(fi 连字)。

—— 字形(Glyph)与字符(Character)不是一回事。字符(Unicode):是逻辑上的编码(如 U+0061 代表 a)。字形(Glyph):是字体文件(.ttf)中实际绘制出来的矢量图形。一个字符可能对应多个字形,多个字符也可能合成一个字形。

HarfBuzz 的核心工作,这串文本在当前字体下,应该使用字体库里的哪几个字形(Glyph ID)?每个字形应该放在什么坐标位置?字形之间的间距(Kerning)是多少?

说得更直白一些,HarfBuzz就是把字体规则给完美呈现出来,就像浏览器,要把庞大复杂的HTML/CSS/Javascript这些规则给视觉实现,HarfBuzz就是字体界的浏览器。

由于项目需要,我们需要做动态字体子集化,HarfBuzz是不二之选。我们采用的是Python FastAPI + HarfBuzz + Redis+Nginx+Vue3+pdf-lib前后端技术栈来实现。以下的记录是其中一些操作要点。

字体子集化优化在线动态渲染PDF

最近做了一个在线生成PDF,适时预览并可在线打印的系统。使用了pdf-lib.js(https://pdf-lib.js.org),作为前端创建PDF的框架,虽然很方便,但遇到一个棘手的问题:如何解决中文显示。

PDF文件不同于web文件(HTML+CSS),它的字体是嵌入文件的,而由于安全性和PDF文件本身机制的问题,并不能读取本地字体文件。而国际PDF阅读器规范中的 “Standard 14 Fonts”(14 种标准字体)中并没有中文字体,这就导致了在不嵌入中文字体的情况下,出现中文就会无法渲染。

Standard 14 Fonts

内置的 14 种标准字体 这 14 种字体全部属于 拉丁字母(英文) 系列,分为四大类:
1.无衬线 (Sans Serif)字体: Helvetica, Helvetica-Bold, Helvetica-Oblique, Helvetica-BoldOblique
2.衬线 (Serif/Roman) 字体:Times-Roman, Times-Bold, Times-Italic, Times-BoldItalic
3.等宽 (Monospace)字体: Courier, Courier-Bold, Courier-Oblique, Courier-BoldOblique
4.符号 (Symbol) :Symbol, ZapfDingbats

PDF 的设计初衷是 “所见即所得” (WYSIWYG)。它的目标是:不管你把这个文件发给谁,在手机上、打印机上还是 20 年后的电脑上,看到的排版必须一模一样。

封闭性:PDF 不信任阅读者的系统。它认为:“万一对方电脑里没有微软雅黑,我的排版不就乱了吗?”

机制:为了保证绝对一致,pdf-lib 必须执行 “字体嵌入 (Embedding)”。它需要把字体文件的每一个二进制数据“缝进” PDF 文件里。

面临的难点:

这个系统需要兼容正体中文(繁体)和简体中文,而这种字体体量大得惊人。兼容性最好的Adobe全字符体——Super OTC 体积高达100M+,师出同门的Google Noto也差不多这个体量,如放到Web项目,简直是灾难,浏览器瞬间会被卡死。

于是只能放弃使用这种大字符集的字体,选择性地支持正体或简体。

但是单独的简体字体,体积也很大,思源Source Han Serif,单独一种字重的字体,也高达20M+,Noto也要10M-20M。如果要加载这么大的字体,配置一般的电脑尤其是内存小的电脑,会频繁出现假死的情况。

于是,只能想办法减少字体的体积。

通过万能的Gemini,终于有了解决办法:字体子集化。

说白了,就是从原字体中提取一部分字符,只要能覆盖自己常用的字符就行了。

方案:

1. 用集成工具做子集化。这是最常用的方式,工具例如,Fontmin(https://ecomfe.github.io/fontmin/)。

这种方式就是要准备“字体白名单”,也就是你要把那些你需要的字符一一列出来,让它把你需要的字符从庞大字库中“抠”出来。

可以到GitHub上下载常用中文字符3500字等整理好的字符集,导入Fontmin中。也可以自己整理用到的文字再加进去。

2.用 Python 做字体子集化。

#安装fonttools库
pip install fonttools brotli

#命令执行子集化
pyftsubset original.ttf --text-file=chars.txt --output-file=sub.woff2 --flavor=woff2
'''
其中
original.ttf:原始字体,如思源宋体
--text-file:字符白名单文件
--output-file输出字体
'''

#不指定字符白名单,只按照某个标准来子集化(如变成GB2312标准字符集)
pyftsubset NotoSerifSC-Medium.ttf --unicodes="U+0020-007E,U+00A0-00FF,U+2000-206F,U+3000-303F,U+4E00-9FA5" --output-file=label_gb2312.ttf --layout-features='*'

3.在线动态字体子集化

① Node.js 生态方案:Fontmin,基于 fonteditor-core,支持 TTF、EOT、WOFF、WOFF2 之间的转换。它支持插件机制,可以方便地动态剪裁。示例代码:

const Fontmin = require('fontmin');

const fontmin = new Fontmin()
    .src('fonts/SourceHanSansCN-Regular.ttf') // 原始 15MB 字体
    .use(Fontmin.glyph({
        text: '张三的PDF发票金额: ¥100.00' // 前端传过来的动态文本
    }))
    .use(Fontmin.ttf2woff2()) // 转换为更小的 woff2 格式
    .dest('dist/fonts');

fontmin.run((err, files) => {
    // 这里的 files[0].contents 就是剪裁后只有几 KB 的字体二进制流
});

Firebase JWT库Token验证的使用

项目中使用Firebase JWT库做Token验证,确实很好用,减少了很多繁琐的判断,不过要使用它自带的验证方法,命名就必须符合它的标准。

如,起始时间,过期时间等都需要按照它的标准命名:

$tokenData = [
'iat' => $issuedAt, //Token核发时间,标准写法,名称固定为 iat
'exp' => $expirationTime, //Token过期时间 ,标准写法,名称固定为 exp
];

验证非常方便,只需要调用相应的异常结果就可以了,不必自己拆解出来做复杂的判断。

 

try {
                $decoded = JWT::decode($token, new Key($this->secretKey, 'HS256'));
                $decodeToken = (array)$decoded;
            } catch (ExpiredException $e) {//过期异常
                $message = 'Token has expired';
                $outData = ['code' => 401, 'message' => $message];
                return json($outData);
            } catch (SignatureInvalidException $e) { //签名不正确,被篡改
                 $message = 'Invalid token signature';
                $outData = ['code' => 401, 'message' => $message];
                return json($outData);
            } catch (Exception $e) { //格式不符或者被篡改
                $message = 'Token validation failed';
                $message = 'Verify Fail: permission denied';
                $outData = ['code' => 401, 'message' => $message];
                return json($outData);
            }


 

 

 

使用Fabric的一些心得

最近项目中使用Fabric.js做图像动态标记,被有些小问题缠住,花了很多时间去琢磨,有些甚至弄了一两天才理明白,特此记录。

1.canvas的width、height与style.width、style.height须一致,否则画出图形会变形或只显示图像的一部分。

canvasRef.value.width = img.naturalWidth;
canvasRef.value.height = img.naturalHeight;
canvasRef.value.style.width = img.naturalWidth + 'px';
canvasRef.value.style.height = img.naturalHeight + 'px';

看资料,fabric的画布实际上有三层(下图来自:jb51)

2.可以创建离屏/静态画布,不必在页面中显示,整体绘制完毕后再通过图像对象输出。

 

const canvas = new fabric.StaticCanvas(null, {
width: originWidth,
height: originHeight
});

离屏画布的节点引用设置为null,一样可以设置宽高。

3. 如果有背景图片,所有创建的元素都需要在创建背景图片的函数体内完成,不能在函数体外创建,否则导出的图片将背景不可见。就是这个问题,我困扰了两天,开始以为是跨域问题,背景图片格式的问题等等,后来发现原因在此。

nextTick(
      () => {
        canvasRef.value.width = img.naturalWidth;
        canvasRef.value.height = img.naturalHeight;
        canvasRef.value.style.width = img.naturalWidth + 'px';
        canvasRef.value.style.height = img.naturalHeight + 'px';
        let currentCanvas = new fabric.Canvas(canvasRef.value, {
          selectionColor: '#4fb8d3',
          selectionLineWidth: 0
        });

        fabric.Image.fromURL(imgUrl, (img) => {  //必须将绘制的文本和图形放到这个背景图函数体内,否则无法显示背景图
          currentCanvas.setBackgroundImage(
            img,
            currentCanvas.renderAll.bind(currentCanvas)
          );
          currentCanvas.backgroundImage.scaleToWidth(currentCanvas.width);
          currentCanvas.backgroundImage.scaleToHeight(currentCanvas.height);
          for (let key in LocationObj) {
            let locationNum = LocationObj[key][0].bbox;
            let arr = locationNum.split(',');
            let numberArray = arr.map(Number);
            let rect = new fabric.Rect({
              left: numberArray[0],
              top: numberArray[1],
              width: numberArray[2] - numberArray[0],
              height: numberArray[3] - numberArray[1],
              fill: 'transparent',
              stroke: 'rgb(235,64,64,0.9)',
              strokeWidth: 3,
              strokeDashArray: [5, 5]
            });
            let text = new fabric.Text(
              ' ' +
                messageMap.value[key] +
                ' - ' +
                Number(LocationObj[key][0].conf) * 100 +
                '%' +
                ' ',
              {
                left: numberArray[0], // 文本的X坐标  
                top: numberArray[1] - 22,
                fontSize: 22, // 字体大小
                textBackgroundColor: '#eb4040', // 文字背景颜色
                textBackgroundColor: 'rgb(235,64,64,0.8)',
                hasControls: false, // 不显示控件
                hasBorders: false, // 不显示边框
                fontWeight: 'normal',
                fill: 'white',
                padding: 20,
                lineHeight: 3,
                textAlign: 'right',
                fontFamily: 'Arial,Microsoft YaHei'
              }
            );
            currentCanvas.add(rect);
            currentCanvas.add(text);
            currentCanvas.renderAll();  
            // rect.set('selectable', false);
            // text.set('selectable', false);
          }

          downURL.value = currentCanvas.toDataURL({
            format: 'png',
            quality: 0.9
          });
          downLink.value.download = 'X_RayImg.png';
        });
      },
      { crossOrigin: 'Anonymous' }
    );