包阅导读总结
1. `JavaScript`、`对象不可变性`、`Object.freeze`、`Object.seal`、`性能考虑`
2. 本文介绍了 JavaScript 中对象不可变性的概念,重点阐述了 `Object.freeze()` 和 `Object.seal()` 方法,包括其工作原理、使用场景及性能问题,并对相关属性标志进行了说明。
3.
– 前言
– 介绍了 JavaScript 中对象不可变性,比较了 `Object.freeze()` 和 `Object.seal()` 方法,提及使用场景和性能考虑。
– 了解 JavaScript 中的不可变性
– 解释使对象不可变的含义及 `const` 关键字用于基本数据类型和对象的差异。
– 指出对象通过引用复制存在风险,可通过不同方法限制访问。
– `writable` 、 `configurable` 属性标志
– 介绍这些属性标志在对象属性中的作用和设置规则。
– 使用 `Object.freeze` 与 `Object.seal` 实现对象不可变性
– 详细介绍 `Object.freeze()` 方法,包括其浅层冻结和深层冻结,以及如何实现深层冻结。
– 说明 `Object.seal()` 方法的特点和限制。
– 提及 `Object.preventExtensions()` 方法。
– 和 `Object.freeze` 以及 `Object.seal` 使用案例和性能问题
– 阐述这些方法的使用场景和可能带来的问题。
思维导图:
文章地址:https://mp.weixin.qq.com/s/y3pOvU72p8W6MlHxKBPNLQ
文章来源:mp.weixin.qq.com
作者:飘飘
发布时间:2024/7/25 0:02
语言:中文
总字数:4622字
预计阅读时间:19分钟
评分:85分
标签:JavaScript,对象不可变性,Object.freeze,Object.seal,Object.preventExtensions
以下为原文内容
本内容来源于用户推荐转载,旨在分享知识与观点,如有侵权请联系删除 联系邮箱 media@ilingban.com
前言
介绍了 JavaScript 中对象不可变性的概念,并通过比较Object.freeze()
和Object.seal()
方法,阐述了如何限制对对象属性的修改,以及这些方法的使用场景和性能考虑。今日前端早读课文章由 @飘飘翻译分享。
正文从这开始~~
在 JavaScript 处理值和对象时,有时可能需要限制对它们的操作,以防止对全局配置对象、状态对象或全局常量进行更改,从而保护应用程序的整体配置。
【第3203期】ECMAScript 2024(ES15)将带来的新特性
具有此类数据访问权限的函数可能会在不应修改数据的情况下直接修改数据(这可能也是由开发人员无意间犯下的错误引起的)。此外,在同一代码库(或使用您的代码)的其他开发人员可能会意外地进行此类更改。
值得庆幸的是,JavaScript 提供了一些构造来处理这类情况。
在本教程中,我们将讨论不变性的概念以及 JavaScript 中的freeze()
和seal()
对象方法。我们将通过示例代码了解它们的工作原理,并讨论可能存在的性能限制。现在,让我们开始吧!
了解 JavaScript 中的不可变性
简而言之,使对象不可变意味着对它的进一步更改将不会生效。从本质上讲,它的状态变成了只读状态。在某种程度上,这就是 const 关键字的作用:
const jarOfWine = "full";
// throws error "Uncaught TypeError: Assignment to constant variable."
jarOfWine = "empty";
但当然,我们不能将const
用于对象和数组等实体,因为 const 声明的工作方式 — 它只是创建一个值的引用。 为了解释这一点,让我们回顾一下 JavaScript 的数据类型。
基本数据类型与对象
第一类数据类型是仅包含一个项目的值。这些值包括不可变的简单数据类型,如字符串或数字。
let nextGame = "Word Duel";
// change to "Word Dual"? Doesn't stick.
nextGame[7] = “a”;
nextGame; // still "Word Duel"
// Of course, if we'd declared nextGame with `const`, then we couldn't reassign it.
nextGame = "Word Dual";
nextGame; // now "Word Dual"
当我们复制这些基本类型时,我们是在复制值:
const jarOfWine = "full";
const emptyJar = jarOfWine; // both jars are now 'full'
两个变量jarOfWine
和emptyJar
现在都包含两个独立的字符串,你可以独立地更改其中任何一个,而不会影响另一个。但是,对象的行为有所不同。
在声明对象时,如下面的代码, user 变量并不包含对象本身,而是对象的引用:
const user = {
name: "Jane",
surname: "Traveller",
stayDuration: "3 weeks",
roomAssigned: 1022,
}
这就好比写下通往藏有你金堆的洞穴的地址。地址并不是洞穴本身。因此,当我们试图使用与复制字符串时相同的赋值方法来复制一个对象时,我们最终只会复制引用或地址,而没有两个独立的对象:
const guest = user;
修改user
也会影响guest
:
guest.name = "John";
// now both user and guest look like this:
{
name: "John",
surname: "Traveller",
stayDuration: "3 weeks",
roomAssigned: 1022,
}
通常可以使用Object.is()
方法或严格相等运算符来进行测试:
Object.is(user, guest) // returns true
user === guest // returns true
与const
关键字相似的是,它也创建了一个对值的引用,这意味着虽然绑定无法更改(即你无法重新分配变量),但所引用的值可以更改。
这是在我们之前成功修改了 name 属性之后发生的,尽管 guest 是用 const 声明的。
guest.name = "John";
换句话说, const 给我们提供的是赋值不变性,而不是值不变性。
限制对象属性和整个对象的更改
由于 JavaScript 中的对象是通过引用复制的,因此总是存在复制的引用会改变原始对象的风险。 根据您的使用情况,这种行为可能并不可取。 在这种情况下,从本质上 “锁定” 对象可能是有意义的。
理想情况下,你应该复制你的对象并对其进行修改,而不是修改原始对象。虽然大多数复制或克隆机制都是浅层的,但如果你处理的是嵌套较深的对象,那么你需要进行深度克隆。
JavaScript 提供了三种方法,可对对象执行不同程度的访问限制。 这些方法包括Object.freeze()
、Object.seal()
和Object.preventExtensions()
。 虽然我们会稍微涉及后者,但主要关注前两者。
writable 、 configurable 属性标志
不过,在继续讨论之前,我们先来了解一下限制访问属性的机制背后的一些基本概念。具体来说,我们对 writable 和 configurable 等属性标志很感兴趣。
在使用Object.getOwnPropertyDescriptor
或Object.getOwnPropertyDescriptors
方法时,通常可以查看这些标志的值:
const hunanProvince = {
typeOfWine: "Emperor's Smile",
};
Object.getOwnPropertyDescriptors(hunanProvince);
// returns
{
typeOfWine: {
value: "Emperor's Smile",
writable: true,
enumerable: true,
configurable: true
},
}
虽然我们在使用 JavaScript 对象时通常更关心属性的实际值,但除了持有属性值的 value 属性外,属性还有其他属性。
这些属性包括前面提到的 value 、 writable 和 configurable 属性,以及上面提到的 enumerable 属性。
writable 和 configurable 标志对我们来说是最重要的。 当属性的 writable 设置为 true 时,其值可以更改。 否则,它就是只读的。
还有 configurable ,在属性上设置为 true 时,可以更改上述标志或删除属性。
如果 configurable 被设置为 false ,所有内容基本上都变成只读,但有一个例外:如果 writable 被设置为 true ,而 configurable 是 false ,属性值仍然可以更改:
Object.defineProperty(hunanProvince, "capital", {
value: "Caiyi Town",
writable: true,
});
hunanProvince.capital = "Possibly Gusu";
Object.getOwnPropertyDescriptors(hunanProvince);
// now returns
{
typeOfWine: {
value: "Emperor's Smile",
writable: true,
enumerable: true,
configurable: true
},
capital: {
value: "Possibly Gusu",
writable: true,
enumerable :false,
configurable: false
},
}
需要注意的是, enumerable 和 configurable 都是 false 类型的,因为它们是使用Object.defineProperty()
创建的。正如之前所提到的,使用这种方式创建的属性的所有标志都被设置为 false 。但是 writable 是不同的,因为我们明确地为其设置了值。
我们还可以将 writable 从 true 改为 false ,但仅此而已。 你不能将它从 false 更改为 true 。事实上,一旦某个属性的 configurable 和 writable 都设置为 false ,就无法再对其进行任何更改了:
Object.defineProperty(hunanProvince, "capital", {
writable: false,
// everything else also `false`
});
// no effect
hunanProvince.capital = "Caiyi Town";
虽然这些标志是在属性级别上使用的,但Object.freeze()
和Object.seal()
等方法是在对象级别上使用的。现在我们继续讨论这个问题。
本文假定您已大致了解不变性概念为何有用。
使用 Object.freeze 与 Object.seal 实现对象不可变性
现在,让我们来看看 freeze 和 seal 方法。
使用 Object.freeze
当我们使用Object.freeze
将一个对象冻结后,它将无法再被修改。换句话说,无法再为它添加新的属性,也无法删除已有的属性。你可以猜到,这是通过将所有属性的所有标志都设置为 false 来实现的。
让我们来看一个例子。以下是我们将要使用的两个对象:
let obj1 = {
"one": 1,
"two": 2,
};
let obj2 = {
"three": 3,
"four": 4,
};
现在,让我们修改第一个对象的某个属性: obj1
obj1.one = "one"; // returns "one"
因此,原始对象现在看起来是这样的:
obj1;
{
one: "one",
two: 2,
};
当然,这是预期的行为。 对象的默认行为是可以被更改的。 现在,让我们尝试冻结一个对象。 我们将使用 obj2 ,因为它尚未被篡改:
// freeze() returns the same object passed to it
Object.freeze(obj2); // returns {three: 3, four: 2}
// test
obj2 === Object.freeze(obj2); // returns true
为了测试一个对象是否被冻结,JavaScript 提供了Object.isFrozen()
方法:
Object.isFrozen(obj2); // returns true
现在,即使我们尝试像下面这样修改,也不会有任何效果。
obj2.three = "three"; // no effect
不过,我们很快就会看到,当我们开始使用嵌套对象时,就会遇到麻烦。就像对象克隆一样,冻结也可以是浅层的或深层的。
让我们用 obj1 和 obj2 创建一个新对象,并在其中嵌套一个数组:
// nesting
let obj3 = Object.assign({}, obj1, obj2, {"otherNumbers": {
"even": [6, 8, 10],
"odd": [5, 7, 9],
}});
obj3;
// {
// one: "one",
// two: 2,
// three: 3,
// four: 4,
// "otherNumbers": {
// "even": [6, 8, 10],
// "odd": [5, 7, 9],
// }
// }
你会注意到,即使我们将它冻结了,我们仍然可以对嵌套对象中的数组进行修改:
Object.freeze(obj3);
obj3.otherNumbers.even[0] = 12;
obj3;
// {
// one: "one",
// two: 2,
// three: 3,
// four: 4,
// "otherNumbers": {
// "even": [12, 8, 10],
// "odd": [5, 7, 9],
// }
// }
现在这个偶数数组的首元素从6
修改为12
. 因为数组也是对象,在这里也出现了类似的行为:
let testArr = [0, 1, 2, 3, [4, 5, [6, 7]]];
Object.freeze(testArr);
testArr[0] = "zero"; // unable to modify top-level elements...
// ...however, nested elements can be changed
testArr[4][0] = "four"; // now looks like this: [0, 1, 2, 3, ["four", 5, [6, 7]]]
如果你一直在浏览器控制台中测试代码,那么它很可能在悄无声息的情况下失败,并且不会抛出任何错误。如果你希望错误信息更加明确,可以尝试将代码封装在一个立即调用函数表达式(IIFE)中,并开启 “严格模式”:
(function() {
"use strict";
let obj = {"one": 1, "two": 2};
Object.freeze(obj);
obj.one = "one";
})();
上面的代码现在应该会在控制台中抛出一个TypeError
。
Uncaught TypeError: Cannot assign to read only property 'one' of object '#<Object>'
那么,我们如何让整个对象(包括顶层(直接属性引用)和嵌套属性)冻结呢?
我们已经指出,冻结只应用于对象的顶级属性,因此我们需要一个能够递归地冻结每个属性的函数:
const deepFreeze = (obj) => {
// fetch property keys
const propKeys = Object.getOwnPropertyNames(obj);
// recursively freeze all properties
propKeys.forEach((key) => {
const propValue = obj[key];
if (propValue && typeof(propValue) === "object") deepFreeze(propValue);
});
return Object.freeze(obj);
}
现在,尝试更改嵌套属性都不成功。
请注意,虽然冻结基本上保护对象的变化,但它确实允许变量重新分配。
使用 Object.seal ()
使用Object.freeze()
方法,新的更改不会对冻结对象产生任何影响。但是,seal()
方法允许修改现有属性。也就是说,虽然不能添加新属性或删除现有属性,但可以进行更改。
seal()
方法基本上是将我们之前讨论过的 configurable 标志设置为 false ,并将每个属性的 writable 设置为 true :
const students = {
"001" : "Kylie Yaeger",
"002": "Ifeoma Kurosaki"
};
// seal object
Object.seal(students);
// test
Object.isSealed(students); // returns true
// cannot add or delete properties
students["003"] = "Amara King"; // fails
delete students["001"]; // fails
下面是另一个使用数组的例子:
const students = ["Kylie Yaeger", "Ifeoma Kurosaki"];
// seal
Object.seal(students);
// test
Object.isSealed(students); // returns true
// throws a TypeError saying object is not extensible
students.push("Amara King");
封装还可以防止使用Object.defineProperty()
或Object.defineProperties()
等标记对属性进行重新定义,无论是添加新属性还是修改现有属性。
请记住,如果将 writable 更改为 true ,您仍然可以进一步将其更改为 false ,但这种更改无法撤销。
// fails
Object.defineProperty(hunanProvince, "capital", {
value: "Unknown",
writable: true,
});
封装的另一个变化是不可能将普通数据属性转换为访问器(即 getter 和 setter):
// fails
Object.defineProperty(hunanProvince, "capital", {
get: () => "Caiyi Town",
set: (val) => hunanProvince["capital"] = val;
});
反之亦然:你不能将访问者改为数据属性。与冻结对象一样,封存对象可以防止其原型发生变化:
const languageSymbols = {
English: "ENG",
Japanese: "JP",
French: "FR",
};
const trollLanguageSymbols = {
trollEnglish: "T-ENG",
trollJapanese: "T-JP",
trollFrench: "T-FR",
};
Object.seal(trollLanguageSymbols);
// fails
Object.setPrototypeOf(trollLanguageSymbols, languageSymbols);
同样,与封装一样,这里的默认行为也是浅封装。因此,你可以像深冻食物一样选择深封装一个对象:
const deepSeal = (obj) => {
// fetch property keys
const propKeys = Object.getOwnPropertyNames(obj);
// recursively seal all properties
propKeys.forEach((key) => {
const propValue = obj[key];
if (propValue && typeof(propValue) === "object") deepSeal(propValue);
});
return Object.seal(obj);
}
我们在这里修改了 MDN 的deepFreeze()
函数,以实现封装操作:
const students = {
"001" : "Kylie Yaeger",
"002": "Ifeoma Kurosaki",
"003": {
"004": "Yumi Ren",
"005": "Plisetsky Ran",
},
};
deepSeal(students);
// fails
delete students["003"]["004"];
现在,我们的嵌套对象也被封装了。
使用 Object.preventExtensions ()
另一种可以专门防止添加新属性的 JavaScript 方法是preventExtensions()
方法:
(() => {
"use strict";
const trollToken = {
name: "Troll",
symbol: "TRL",
decimal: 6,
totalSupply: 100_000_000,
};
Object.preventExtensions(trollToken);
// fails
trollToken.transfer = (_to, amount) => {}
})();
因为我们所做的只是阻止添加新的属性,因此显然现有的属性可以被修改甚至删除:
delete trollToken.decimal;
trollToken;
// {
// name: "Troll",
// symbol: "TRL",
// totalSupply: 100_000_000,
// }
需要注意的是,[[prototype]]
属性变得不可更改:
const token = {
transfer: () => {},
transferFrom: () => {},
approve: () => {},
};
// fails with a TypeError
Object.setPrototypeOf(trollToken, token);
要测试一个对象是否可扩展,只需使用isExtensible()
方法即可:
// I've omitted `console.log` here since I'm assuming you're typing in the browser console directly
(`Is trollToken extensible? Ans: ${Object.isExtensible(trollToken)}`);
就像我们手动为一个属性设置 configurable 和 writable 标志为 false 一样,使一个对象不可扩展是一条单行道。
和 Object.freeze 以及 Object.seal 使用案例和性能问题
总之, Object.freeze () 和 Object.seal () 是 JavaScript 语言提供的构造,用于帮助维护对象的不同级别的完整性。但是,何时需要使用这些方法可能会让人感到困惑。
前面提到的一个例子是使用全局对象进行应用程序状态管理。你可能希望保持原始对象不变,并在副本上进行更改,尤其是如果你想跟踪状态更改并进行回滚的话。
冻结机制用于防止代码直接修改不应被修改的对象。
冻结或密封的对象还可以防止由于打字错误引入的新属性,例如打错的属性名称。
这些方法在调试时也很有用,因为对对象的限制可以帮助缩小可能的 bug 来源。
也就是说,对于使用您代码的人来说,这可能会成为令人头痛的问题,因为冻结对象和未冻结对象在物理上实际上没有区别。
唯一能确定一个对象是否被冻结或密封的方法是使用isFrozen()
或isSealed()
方法。这可能会让您难以预测对象的行为,因为可能并不清楚为何要设置这些限制。
标记模板是一项使用隐式Object.freeze()
的特性;styled-components 库和其他一些库依赖于它。前者使用标记模板字面量来创建其样式组件。
如果你想知道使用上述任何一种方法是否存在性能开销的问题,那么在 V8 引擎中曾经存在一些历史性的性能问题。然而,这更多是因为存在一些 bug,现在已经被修复了。
在 2013 年至 2014 年期间, Object.freeze () 和 Object.seal () 也在 V8 版本中进行了一些性能提升。
这里有一篇 Stack Overflow 上的帖子,追踪了冻结对象与非冻结对象在 2015 年至 2019 年之间的性能表现。数据显示,在 Chrome 中,这两种情况下的性能几乎相同。
不过,在某些像 Safari 这样的浏览器中,封装或冻结可能会影响对象的枚举速度。
处理不可变性的第三方库
在 JavaScript 中,有多种处理不可变性的方法。虽然上面讨论的方法在某些情况下很有用,但对于任何大型应用程序,你很可能会选择使用库来实现不可变性。
一些例子包括 Immer 和 Immutable.js。使用 Immer 时,您可以使用熟悉的 JavaScript 数据类型。然而,虽然 Immutable.js 引入了新的数据结构,但它可能是更快的选择。
结论
JavaScript 为对象提供了不同级别的访问限制方法,如Object.freeze()
和Object.seal()
等。
然而,就像克隆一样,由于对象是通过引用进行复制的,因此冻结通常是浅层的。因此,您可以实现自己的基本深层冻结或密封函数,或者根据您的使用情况利用 Immer 或 Immutable.js 等库。
关于本文
译者:@飘飘
作者:@Jemimah Omodior
原文:https://blog.logrocket.com/javascript-object-immutability-object-freeze-vs-object-seal/
这期前端早读课
对你有帮助,帮”赞“一下,
期待下一期,帮”在看” 一下 。