前言
MDN 有部分中文内容明显翻译得有问题,且到了无法理解的程度,所以对照着原文机翻整理一下。
Object.freeze()
例子
冻结数组
let a = [0];
Object.freeze(a); // 现在数组不能被修改了。
a[0]=1; // fails silently
a.push(2); // fails silently
// In strict mode such attempts will throw TypeErrors
function fail() {
"use strict"
a[0] = 1;
a.push(2);
}
fail();
被冻结的对象是不可变的。但也不总是这样。下例展示了冻结对象不是常量对象(浅冻结)。
obj1 = {
internal: {}
};
Object.freeze(obj1);
obj1.internal.a = 'aValue';
obj1.internal.a // 'aValue'
对于一个常量对象,整个引用图(对其他对象的直接和间接引用)只能引用不可变的冻结对象。冻结的对象被认为是不可变的,因为整个对象中的全部对象状态(值和对其他对象的引用)是固定的。注意,字符串,数字和布尔总是不可变的,而函数和数组是对象。
什么是“浅冻结”?
调用Object.freeze(object)
的结果仅适用于object
自身的直接属性,并且将仅在object
上阻止未来的属性添加、删除或值重新分配操作。如果这些属性的值是对象本身,则这些对象不会被冻结,并且可能是属性添加、删除或值重新分配操作的目标。
const employee = {
name: "Mayank",
designation: "Developer",
address: {
street: "Rohini",
city: "Delhi"
}
};
Object.freeze(employee);
employee.name = "Dummy"; // fails silently in non-strict mode
employee.address.city = "Noida"; // attributes of child object can be modified
console.log(employee.address.city) // Output: "Noida"
要使对象不可变,需要递归冻结每个类型为对象的属性(深冻结)。当你知道对象在引用图中不包含任何环 (循环引用) 时,根据你的设计对逐个对象使用该模式,否则将触发无限循环。一个增强功能deepFreeze()
是拥有一个接收路径(例如数组)参数的内部函数,这样您就可以在对象变成不可变的过程中抑制递归调用deepFreeze()
。你仍然有冻结不应冻结的对象的风险,例如 [window]。
// 深冻结函数。
function deepFreeze(obj) {
// 取回定义在 obj 上的属性名
var propNames = Object.getOwnPropertyNames(obj);
// 在冻结自身之前冻结属性
propNames.forEach(function(name) {
var prop = obj[name];
// 如果 prop 是个对象,冻结它
if (typeof prop == 'object' && prop !== null)
deepFreeze(prop);
});
// 冻结自身 (no-op if already frozen)
return Object.freeze(obj);
}
obj2 = {
internal: {}
};
deepFreeze(obj2);
obj2.internal.a = 'anotherValue';
obj2.internal.a; // undefined
this
例子
箭头函数
在箭头函数中,this
保留了封闭词法上下文的 this
值。在全局代码中,它将被设置为全局对象:
const globalObject = this;
const foo = (() => this);
console.log(foo() === globalObject); // true
注意:如果在调用箭头函数时将
this
参数传递给call
、bind
或apply
,它将被忽略。您仍然可以在调用前添加参数,但第一个参数 (thisArg
) 应设置为null
。
// Call as a method of an object
const obj = { func: foo };
console.log(obj.func() === globalObject); // true
// 尝试使用call设置this
console.log(foo.call(obj) === globalObject); // true
// 尝试使用bind设置this
const boundFoo = foo.bind(obj);
console.log(boundFoo() === globalObject); // true
无论如何,foo
的 this
都被设置为它被创建时的状态(在上面的示例中,全局对象)。这同样适用于在其他函数内部创建的箭头函数:它们的 this
仍然是封闭的词汇上下文。
// 创建带有方法bar的obj,方法bar返回一个
// 返回this的函数。返回的函数被创建为
// 一个箭头函数,因此它的this被永久绑定到
// 它的外围函数的this上。bar的值可以
// 在调用时设置,而调用又设置返回函数的值。
// 注意:在这个上下文中`bar()`语法等价于`bar: function ()`
const obj = {
bar() {
const x = (() => this);
return x;
}
};
// 作为obj的一个方法调用bar,设置它的this为obj
// 将返回函数的引用赋给fn
const fn = obj.bar();
// 在没有设置this的情况下调用fn,通常默认
// 为全局对象或在严格模式下为undefined
console.log(fn() === obj); // true
// 但是如果你引用了obj的方法而没有调用它,请注意
const fn2 = obj.bar;
// 在bar方法中调用箭头函数的this将返回window,因为它遵循了fn2中的this。
console.log(fn2()() === window); // true
在上面,分配给obj.bar
的函数(称为匿名函数 A) 返回另一个作为箭头函数创建的函数(称为匿名函数 B)。结果,函数 B 的 this
在调用时被永久设置为 obj.bar
(函数 A)的 this
。当调用返回的函数(函数 B)时,它的 this
将始终是它最初设置的值。在上面的代码示例中,函数 B 的 this
被设置为函数 A 的 this
,它是 obj
,因此即使以通常将其 this
设置为 undefined
或全局对象的方式(或任何其他方法,如上例中的全局执行上下文)调用它,它仍保持设置为 obj
。
传输层安全协议
历史
HTTPS 被引入时,它是基于安全套接字层 (SSL) 2.0的,这是 Netscape 引入的一种技术。不久之后它更新到 SSL 3.0,随着其使用范围的扩大,很明显需要指定一种通用的标准加密技术,以确保所有 Web 浏览器和服务器之间的互通性。互联网工程专案组(IETF)于 1999 年 1 月在 RFC 2246 中指定了 TLS 1.0。TLS 的当前版本是 1.3(RFC 8446)。
尽管网络现在使用 TLS 进行加密,但出于习惯,许多人仍然将其称为“SSL”。
尽管 TLS 可以在任何低层传输协议之上使用,但该协议的最初目标是加密 HTTP 流量。使用 TLS 加密的 HTTP 通常称为HTTPS。按照惯例,TLS 加密的 Web 流量默认在端口 443 上进行交换,而未加密的 HTTP 默认使用端口 80。HTTPS 仍然是 TLS 的重要用例。
基于 TLS 的 HTTP
TLS 提供三项主要服务,有助于确保用它交换的数据的安全性:
-
验证
身份验证允许通信的每一方验证对方是他们声称的身份。
-
加密
数据在用户代理和服务器之间传输时被加密,以防止未经授权的各方读取和解释数据。
-
完整性
TLS 确保在对数据进行加密、传输和解密之间没有信息被丢失、损坏、篡改或伪造。
TLS 连接从握手阶段开始,其中客户端和服务器就共享密钥和重要参数(如密码套件)达成一致。一旦参数和数据交换模式,应用程序数据,如 HTTP,被交换。
密码套件
TLS 握手协商的主要参数是密码套件。
在 TLS 1.2 和更早版本中,协商密码套件包括一组加密算法,它们共同提供共享密钥的协商、服务器验证的方式以及将用于加密数据的方法。
TLS 1.3 中的密码套件主要管理数据的加密,单独的协商方法用于密钥协商和验证。
不同的软件可能对相同的密码套件使用不同的名称。例如,OpenSSL 和 GnuTLS 中使用的名称与 TLS 标准中的名称不同。Mozilla OpSec 团队关于 TLS 配置的文章中的密码名称对应表列出了这些名称以及有关兼容性和安全级别的信息。
配置您的服务器
正确配置服务器至关重要。通常,您应该尝试将密码支持限制为可能与您希望能够连接到您的站点的浏览器兼容的最新密码。Mozilla OpSec TLS 配置指南提供了有关推荐配置的更多信息。
为了帮助您配置站点,Mozilla 提供了一个有用的 TLS 配置生成器,它将为以下 Web 服务器生成配置文件:
- Apache
- Nginx
- Lighttpd
- HAProxy
- Amazon Web Services CloudFormation 弹性负载均衡器
推荐使用配置器来创建配置以满足您的需求;然后将其复制并粘贴到服务器上的相应文件中,然后重新启动服务器以获取更改。配置文件可能需要一些调整以包含自定义设置,因此请务必在使用前查看生成的配置;在不确保对域名等的任何引用正确的情况下安装配置文件将导致服务器无法正常工作。
TLS 1.3
RFC 8446:TLS 1.3 是一次对 TLS 的重大修订。TLS 1.3 包括许多提高安全性和性能的更改。TLS 1.3 的目标是:
- 删除 TLS 1.2 的未使用和不安全的功能。
- 在设计中包含强大的安全分析。
- 通过加密更多协议来改善隐私。
- 减少完成握手所需的时间。
TLS 1.3 改变了协议的许多基础,但几乎保留了以前的 TLS 版本的所有基本功能。对于 Web,可以启用 TLS 1.3 而不会影响兼容性,除了一些罕见的例外(见下文)。
TLS 1.3 的主要变化是:
- 在大多数情况下,TLS 1.3 握手在一次往返中完成,从而减少了握手延迟。
- 服务器可以启用 0-RTT(零往返时间)握手。重新连接到服务器的客户端可以立即发送请求,完全消除 TLS 握手的延迟。尽管 0-RTT 带来的性能提升可能非常显着,但它们也存在重放攻击的风险,因此在启用此功能之前需要小心谨慎。
- TLS 1.3 仅支持前向安全模式,除非连接恢复或使用预共享密钥。
- TLS 1.3 定义了一组新的 TLS 1.3 独有的密码套件。这些密码套件都使用现代的带有关联数据的认证加密(AEAD)算法。
- TLS 1.3 握手是加密的,但建立共享密钥所需的消息除外。特别是,这意味着服务器和客户端证书是加密的。但是请注意,客户端发送到服务器的服务器身份(server_name 或 SNI 扩展)未加密。
- 许多机制已被禁用:重新协商、通用数据压缩、数字签名算法(DSA)证书、静态 RSA 密钥交换以及与自定义 Diffie-Hellman (DH) 组的密钥交换。
TLS 1.3 草案版本的实现可用。某些浏览器启用了 TLS 1.3,包括 0-RTT 模式。启用 TLS 1.3 的 Web 服务器可能需要调整配置以允许 TLS 1.3 成功运行。
TLS 1.3 只增加了一个重要的新用例。0-RTT 握手可以为延迟敏感的应用程序(如 Web)提供显著的性能提升。启用 0-RTT 需要额外的步骤,以确保成功部署和管理重放攻击的风险。
在 TLS 1.3 中删除重新协商可能会影响一些依赖于使用证书进行客户端身份验证的 Web 服务器。一些 Web 服务器使用重新协商来确保客户端证书被加密,或者仅在请求某些资源时才请求客户端证书。为了客户端证书的隐私,TLS 1.3 握手的加密确保客户端证书被加密;但是,这可能需要一些软件更改。TLS 1.3 支持使用证书的反应式客户端身份验证,但未广泛实施。替代机制正在开发中,也将支持 HTTP/2。
淘汰旧的 TLS 版本
为了帮助打造更现代、更安全的网络,所有主要浏览器都在 2020 年初开始取消对 TLS 1.0 和 1.1 的支持。您需要确保您的网络服务器支持 TLS 1.2 或 1.3。
从版本 74 开始,Firefox 将在使用旧 TLS 版本连接到服务器时返回安全连接失败错误(错误 1606734)。
TLS 握手超时值
如果 TLS 握手由于某些原因开始变慢或无响应,则用户体验可能会受到很大影响。为了缓解这个问题,现代浏览器已经实现了握手超时:
- 从版本 58 开始,Firefox 实现了 TLS 握手超时,默认值为 30 秒。可以通过在 about:config 中编辑 network.http.tls-handshake-timeout 首选项来改变超时值。
也可以看看
- Mozilla SSL 配置生成器和Cipherlist.eu可以帮助您为服务器生成配置文件以保护您的站点。
- Mozilla 运营安全 (OpSec) 团队维护一个带有参考 TLS 配置的 wiki 页面。
- Mozilla Observatory、SSL Labs和Cipherscan可以帮助您测试站点以了解其 TLS 配置的安全性。
- 安全上下文
- Strict-Transport-Security HTTP 标头
HTTP缓存
概述
缓存类型
共享缓存
代理缓存
另一方面,如果TLS桥接代理通过在 PC 上安装来自组织管理的CA(证书颁发机构)的证书,以中间人方式解密所有通信,并执行访问控制等——可以查看响应的内容并将其缓存。但是,由于CT(证书透明度)近年来已经普及,并且一些浏览器只允许带有 SCT(签名证书时间戳)颁发的证书,因此这种方法需要企业政策的应用。在这样的受控环境中,无需担心代理缓存“已过时且未更新”。
托管缓存
例如,HTTP缓存规范本质上没有定义显式删除缓存的方法——但是使用托管缓存,可以通过仪表板操作、API 调用、重新启动等随时删除存储的响应。这允许更主动的缓存策略。
请注意,某些 CDN 提供自己的标头,这些标头仅对该 CDN 有效(例如,Surrogate-Control
)。目前,定义一个CDN-Cache-Control
标头来标准化这些标头的工作正在进行中。
启发式缓存
例如,采取以下响应。此响应最后一次更新是在 1 年前。
基于age的新鲜和陈旧
收到该响应的客户端会发现它在剩余的 518400 秒内是新鲜的,即响应的 max-age
和 Age
之间的差。
Expires 或 max-age
但是时间格式难以解析,很多实现bug被发现,并且有可能通过故意偏移系统时钟来诱发问题;因此,max-age
——用于指定经过的时间——在 HTTP/1.1 中被用于 Cache-Control
。
Vary
例如,对于带有Accept-Language: en
标头返回并缓存的英文内容,接着为具有 Accept-Language: ja
请求标头的请求重用该缓存响应是不可取的。在这种情况下,您可以通过将“Accept-Language
”添加到Vary
标头的值来根据语言分别缓存响应。
此外,如果您正在基于 user agent 提供内容优化(例如,响应式设计),您可能会想在 Vary
标头的值中包含“User-Agent
”。但是,User-Agent
请求头通常有非常多的变体,这大大降低了缓存被重用的机会。因此,如果可能,请考虑一种基于特征检测而不是基于User-Agent
请求标头来改变行为的方式。
验证
If-Modified-Since
以下响应在 22:22:22 生成,max-age
为 1 小时,因此您知道它在 23:22:22 之前是新鲜的。
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
Cache-Control: max-age=3600
<!doctype html>
…
在 23:22:22,响应变得陈旧,缓存不能被重用。因此,下面的请求显示了客户端发送带有If-Modified-Since
请求标头的请求,以询问服务器自指定时间以来是否有任何改变。
GET /index.html HTTP/1.1
Host: example.com
Accept: text/html
If-Modified-Since: Tue, 22 Feb 2022 22:00:00 GMT
如果内容自指定时间以来没有更改,服务器将响应304 Not Modified
。
ETag/If-None-Match
但是,如果服务器确定请求的资源现在应该具有不同的 ETag
值,则服务器将改为以 200 OK
和资源的最新版本进行响应。
重新加载和强制重新加载
重新加载
该行为也在Fetch规范中定义,并且可以通过在缓存模式设置为no-cache
的情况下调用fetch()
来在 JavaScript 中重现(请注意,对于这种情况,reload
不是正确的模式):
强制重新加载
出于向后兼容的原因,浏览器在重新加载期间使用 max-age=0
——因为 HTTP/1.1 之前的许多过时实现不理解 no-cache
。但是在这个用例中no-cache
现在很好,强制重新加载是绕过缓存响应的另一种方法。
该行为在Fetch规范中也有定义,并且可以通过在将缓存模式设置为 reload
的情况下调用 fetch()
来在 JavaScript 中重现(注意缓存模式不是 force-reload
):
避免重新验证
请注意,Chrome 没有实现该指令,而是更改了其实现,以便在重新加载子资源期间不执行重新验证。
删除存储的响应
一旦响应在服务器上过期,您可能希望覆盖该响应,但是一旦响应被储存,服务器就无法执行任何操作——由于缓存,不再有请求到达服务器。
因此,应该假设任何存储的响应都将在其max-age
内保留,除非用户手动执行重新加载、强制重新加载或清除历史记录操作。
常见的缓存模式
缓存破坏
所以上面的 HTML 使得使用 max-age
缓存 bundle.js
和 build.css
变得很困难。
因此,您可以使用包含一个根据版本号或哈希值变化的部分的 URL 来提供 JavaScript 和 CSS。一些做到这一点的方法如下所示。
如果您选择其中一个编号的选项,则可以在通过 HTTP3 传输时将值压缩为 1 个字节。
因为缓存会在保存新条目时删除旧条目,所以存储的响应一周后仍然存在的可能性并不高——即使max-age
设置为 1 周。因此,在实践中,您选择哪一种并没有太大的区别。
请注意,41
号具有最长的 max-age
(1 年),但具有 public
。
即使Authorization
标头存在,public
值也具有使响应可存储的效果。
注意:只有在设置了
Authorization
标头时需要存储响应,才应使用public
指令。否则不需要,因为只要给定max-age
,响应就会存储在共享缓存中。
验证
主资源
如果存储了以下 HTML 本身,即使内容在服务器端更新了,也无法显示最新版本。
有关托管缓存的更多信息
缓存主资源很困难,因为仅使用 HTTP 缓存规范中的标准指令,服务器更新内容时无法主动删除缓存内容。
例如,允许通过 API 或仪表板操作清除缓存的 CDN 将通过存储主资源并仅在服务器上发生更新时显式清除相关缓存来实现更积极的缓存策略。
dns-prefetch
最佳实践
一些资源(如字体)以匿名模式加载。在这种情况下,应使用预连接提示设置 crossorigin 属性。如果您省略它,则浏览器将仅执行 DNS 查找。
Element.getClientRects()
语法
返回值
即使对于具有空 border-box 的 CSS 盒子,也会返回矩形。left
、top
、right
和 bottom
坐标仍然有意义。
Element.getBoundingClientRect()
语法
返回值
空 border-box 被完全忽略。如果所有元素的 border-box 都是空的,则返回一个 width
和 height
为零的矩形,其中 top
和 left
是元素的第一个 CSS 盒子(按内容顺序)的 border-box 的左上角。
在计算边界矩形时,会考虑视口区域(或任何其他可滚动元素)的滚动量。这意味着每次滚动位置更改时 ,矩形的边界边缘(top
、right
、bottom
、left
)都会更改它们的值(因为它们的值是相对于 viewport 的而不是绝对的)。
如果您需要相对于 document 左上角的边界矩形,只需将当前滚动位置加到 top
和 left
属性(可以使用 window.scrollY
和 window.scrollX
获得)以获得与当前滚动位置无关的边界矩形。
Object.defineProperty()
描述
描述符可拥有的键值
请记住,这些属性不一定是描述符自己的属性。继承的属性也将被考虑。为了确保保留这些默认值,您可以预先冻结描述符对象原型链中的现有对象,明确指定所有选项,或使用 Object.create(null)
指向 null
。
Object.freeze()
Object.freeze()
方法冻结一个对象。冻结对象可防止扩展并使现有属性不可写和不可配置。无法再更改冻结的对象:无法添加新属性,无法删除现有属性,无法更改它们的可枚举性、可配置性、可写性或值,并且无法重新分配对象的原型。freeze()
返回传入的同一个对象。
深入:微任务与 Javascript 运行时环境
JavaScript 运行时
为了运行 JavaScript 代码,运行时引擎维护一组用于执行 JavaScript 代码的代理。每个代理由一组执行上下文、执行上下文栈、一个主线程、一组用于处理 worker 的任何附加线程、一个任务队列和一个微任务队列组成。除了一些浏览器在多个代理之间共享的主线程之外,一个代理的每个组件对于该代理都是唯一的。
事件循环(Event loops)
每个代理都由一个事件循环驱动,事件循环收集任何用户和其他事件,将任务排入队列以处理每个回调。然后它运行任何挂起的 JavaScript 任务,然后是任何挂起的微任务,然后执行任何需要的渲染和绘制,然后再次循环以检查挂起的任务。
您的网站或应用程序的代码在同一个线程中运行,共享同一个事件循环,如同 Web 浏览器本身的用户界面。这是主线程,除了运行站点的主要代码体之外,它还处理接收和调度用户和其他事件、渲染和绘制 Web 内容等。
- 窗口事件循环
- 窗口事件循环是驱动所有共享相似来源的窗口的事件循环(尽管对此有进一步的限制,如下所述)。
//End of Article