让我们回到基础:“在JavaScript中,变量或常量是不可变的吗?” (Let us go back to basics: “In JavaScript, are variables or constants immutable?”)
The answer is neither, and if you even have a little hesitation on your answer, read on. Every programming language have different flavors and characteristics, and in JavaScript, this is one of the most important things to be aware of, especially while we are picking up a few languages like Python, Java, etc.
答案都不是 ,如果您甚至对答案有些犹豫,请继续阅读。 每种编程语言都有不同的风格和特征,在JavaScript中,这是要意识到的最重要的事情之一,尤其是在我们选择Python,Java等几种语言时。
You may not necessarily change how you code in JavaScript immediately, but in knowing this early, it will prevent you from getting into nasty situations that are difficult to debug later on. I will include some ways you can adopt to prevent getting into such problems as well — different ways to do shallow & deep copy.
您可能不必立即更改使用JavaScript编码的方式,但是尽早知道这一点,它将防止您陷入麻烦的情况,这些情况以后很难调试。 我还将介绍一些可以用来防止出现此类问题的方法-进行浅层和深层复制的不同方法。
Just a quick summary before we begin:Variables (initialised with let
) — Re-assignable & MutableConstants (initialised with const
) — Non re-assignable & Mutable
在开始之前,请先简要介绍一下: 变量 (用let
初始化)—可重分配和可变的常量 (用const
初始化)—不可重分配和可变的
Before we explain mutability in JavaScript, let us quickly go through some basics… You may skip this part.
在解释JavaScript的可变性之前,让我们快速了解一些基础知识……您可以跳过此部分。
There are broadly a few groups of data types in JavaScript:
JavaScript中大致有几组数据类型:
Primitive (primary)— Boolean, Number, String
原始(原始) —布尔值,数字,字符串
Non-primitive (reference) or Objects — Object, Array, Function
非原始(引用)或对象 -对象,数组,函数
Special— Null, Undefined
特殊 —空,未定义
Quick tip, you can use console.log(typeof unknownVar)
to figure out the data type of the variable you are dealing with
快速提示,您可以使用 console.log(typeof unknownVar)
找出要处理的变量的数据类型
默认情况下,原始数据类型是不可变的 (Primitive data types are Immutable by default)
For primitive data types (like boolean, number and strings), they are immutable if they are declared as constants because for these data types, you cannot add any additional properties or mutate certain properties.
对于原始数据类型(如布尔值,数字和字符串),如果将它们声明为常量,则它们是不可变的 ,因为对于这些数据类型,您无法添加任何其他属性或使某些属性发生突变。
To ‘change/alter’ primitives, it simply means you have to reassign them, which is only possible if they are declared as variables.
要“更改/更改”原语,仅意味着您必须重新分配它们,这只有在将它们声明为变量的情况下才有可能。
let var1 = 'apple' //'apple' is stored in memory location A
var1 = 'orange' //'orange' is stored in memory location Bconst var2 = 'apple'
var2 = 'orange' // ERROR: Re-assignment not allowed for constants
In the example above, if we edit the string for var1, JavaScript will simply create another string at another memory location and var1 will point to this new memory location, and this is called Re-assignment. This applies for all primitive data types regardless if they are declared as variables or constants.
在上面的示例中,如果我们编辑var1的字符串,JavaScript会在另一个内存位置简单地创建另一个字符串,而var1会指向这个新的内存位置,这称为Re-assignment 。 这适用于所有原始数据类型,无论它们被声明为变量还是常量。
And all constants cannot be re-assigned.
并且所有常量都不能重新分配。
在JavaScript中,对象通过引用传递 (In JavaScript, Objects are Passed By Reference)
Problems start to occur when we are dealing with objects…
当我们处理对象时,问题开始出现……
对象不是不可变的 (Objects are not Immutable)
Objects generally refer to the non-primitive data types (Object, Array and Function), and they are mutable even if they are declared as constants with const
对象通常引用非原始数据类型(对象,数组和函数),并且即使使用const
将其声明为常量,它们也是可变的
(For the rest of this article, I will give examples for the Object data type as problems arise the most here. The concepts will be the same for Arrays and Functions)
(对于本文的其余部分,我将给出对象数据类型的示例,因为这里出现的问题最多。关于数组和函数的概念相同)
So what does this mean?
那么这是什么意思?
const profile1 = {'username':'peter'}profile1.username = 'tom'
console.log(profile1) //{'username':'tom'}
In this case, profile1 is pointing to the object located at the same memory location and what we have done is to mutate the properties of this object at the same memory location.
在这种情况下,profile1指向位于相同存储位置的对象,而我们要做的是在同一存储位置更改该对象的属性。
Okay this looks simple enough, why would this be problematic?
好吧,这看起来很简单,为什么会有问题呢?
当对象突变成为问题时... (When Mutation in Objects become a PROBLEM…)
const sampleprofile = {'username':'name', 'pw': '123'}
const profile1 = sampleprofileprofile1.username = 'harry'console.log(profile1) // {'username':'harry', 'pw': '123'}
console.log(sampleprofile) // {'username':'harry', 'pw': '123'}
Looks like a simple piece of code that you may potentially & innocently write right? Guess what, there’s already an issue here!
看起来像一段简单的代码,您可能会无辜地写正确的代码? 猜猜是什么,这里已经有问题了!
This is because Objects are passed by reference in JavaScript.
这是因为对象是通过 JavaScript中的引用传递的 。
What is meant by ‘passing by reference’ in this case is, we are passing the reference of the constant sampleprofile to profile1. In other words, the constants of both profile1 and sampleprofile are pointed to the same object located at the same memory location.
在这种情况下,“ 通过引用传递 ”的意思是,我们正在将常量sampleprofile的引用传递给profile1。 换句话说,profile1和sampleprofile的常量都指向位于 相同存储位置的同一对象。
Hence, when we change the property of the object of the constant profile1, it also affects sampleprofile because both of them are pointed to the same object.
因此,当我们更改常量profile1的对象的属性时,它也会影响sampleprofile,因为它们都指向同一对象。
console.log(sampleprofile===profile1)//true
This is just a simple example of how passing by reference (and hence mutation) can potentially be problematic. But we can imagine how this can get really gnarly when our code gets more complex and large, and if we are not too aware of this fact, it will be hard for us to solve certain bugs.
这只是一个简单的例子,说明通过引用传递(因此发生变异)可能会引起问题。 但是我们可以想象,当我们的代码变得更加复杂和庞大时,这种情况会变得多么棘手,如果我们对这一事实不太了解,那么我们将很难解决某些错误。
So, how do we prevent or try to avoid from potentially facing such issues?
那么,我们如何防止或试图避免潜在地面临此类问题?
There are two concepts that we should be aware of in order to effectively face potential issues related to Mutation in Objects:
为了有效面对与对象变异有关的潜在问题,我们应该意识到两个概念:
Preventing Mutation by Freezing Objects
通过冻结对象防止变异
Using Shallow & Deep copy
使用浅复制和深复制
I will show you some examples of implementation in JavaScript, using vanilla JavaScript methods, as well as some useful libraries we could use.
我将向您展示一些使用普通JavaScript方法实现JavaScript的示例,以及一些我们可以使用的有用库。
防止对象突变 (Preventing Mutation in Objects)
1.使用Object.freeze()方法 (1. Using Object.freeze() Method)
If you want to prevent an object from changing properties, you can use Object.freeze()
. What this does is it will not allow the existing properties of the object to alter. Any attempts to do so will cause it to ‘silently fail’, meaning it will not be successful but and there will not be any warnings as well.
如果要防止对象更改属性,可以使用Object.freeze()
。 这是因为它将不允许更改对象的现有属性。 任何尝试这样做都会导致它“静默失败”,这意味着它不会成功,但是也不会有任何警告。
const sampleprofile = {'username':'name', 'pw': '123'}Object.freeze(sampleprofile)sampleprofile.username = 'another name' // no effectconsole.log(sampleprofile) // {'username':'name', 'pw': '123'}
HOWEVER, this is a form of shallow freeze and this will not work with deeply nested objects:
但是,这是一种浅冻结形式,不适用于深度嵌套的对象:
const sampleprofile = {
'username':'name',
'pw': '123',
'particulars':{'firstname':'name', 'lastname':'name'}
}Object.freeze(sampleprofile)sampleprofile.username = 'another name' // no effect
console.log(sampleprofile)/*
{
'username':'name',
'pw': '123',
'particulars':{'firstname':'name', 'lastname':'name'}
}
*/sampleprofile.particulars.firstname = 'changedName' // changes
console.log(sampleprofile)/*
{
'username':'name',
'pw': '123',
'particulars':{'firstname':'changedName', 'lastname':'name'}
}
*/
In the above example, the properties of the nested object is still able to change.
在上面的示例中,嵌套对象的属性仍然可以更改。
You can potentially create a simple function to recursively freeze the nested objects (you can try this on your own and comment your answers in this article? 😊), but if you are lazy here are some libraries you could use:
您可能会创建一个简单的函数来递归冻结嵌套对象(您可以自己尝试并在本文中评论您的答案?😊),但是如果您很懒,这里可以使用一些库:
2.使用深度冻结 (2. Using deep-freeze)
But seriously, if you look at the source code of deep-freeze, it is essentially just a simple recursion function, but anyway this is how you can use it easily..
但认真的说,如果您查看deep-freeze的源代码 ,它本质上只是一个简单的递归函数,但是无论如何,这就是您可以轻松使用它的方式。
var deepFreeze = require('deep-freeze');const sampleprofile = {
'username':'name',
'pw': '123',
'particulars':{'firstname':'name', 'lastname':'name'}
}deepFreeze(sampleprofile)
Another alternative to deep-freeze is ImmutableJS which some of you may prefer because it will help to throw an error whenever you try to mutate an object that you have created with the library.
深度冻结的另一种替代方法是ImmutableJS ,有些人可能更喜欢它,因为当您尝试对使用该库创建的对象进行变异时,它会引发错误。
避免与通过引用有关的问题 (Avoiding problems related to Passing By Reference)
The key is in understanding shallow and deep copying/cloning/merging in JavaScript.
关键在于理解JavaScript中的浅层和深层复制/克隆/合并 。
Depending on your individual implementation of the objects in your program, you may want to use shallow or deep copying. There may also be other considerations pertaining to memory and performance, which will affect your choice of shallow or deep copy and even libraries to use. But we shall leave this to another day when we get there 😉
根据您对程序中对象的单独实现,您可能需要使用浅复制或深复制。 可能还存在与内存和性能有关的其他注意事项,这将影响您对浅拷贝或深拷贝甚至是要使用的库的选择。 但是我们要等到那一天再去😉
Let’s start off with shallow copying, followed by deep copying.
让我们从浅层复制开始,然后再进行深层复制。
浅复制 (Shallow Copying)
1.使用传播算子(…) (1. Using spread operator (…))
The spread operator introduced with ES6 provides us with a cleaner way to combine arrays and objects.
ES6引入的散布运算符为我们提供了一种更干净的方式来组合数组和对象。
const firstSet = [1, 2, 3];
const secondSet= [4, 5, 6];
const firstSetCopy = [...firstset]
const resultSet = [...firstSet, ...secondSet];console.log(firstSetCopy)
console.log(resultSet) // [1,2,3,4,5,6]
ES2018 also extended spread properties to object literals, so we can also do the same for objects. The properties of all the objects will be merged but for conflicting properties, the subsequent objects will take precedence.
ES2018还将扩展属性扩展到对象文字,因此我们也可以对对象执行相同的操作。 所有对象的属性将被合并,但是对于冲突的属性,后续对象将优先。
const profile1 = {'username':'name', 'pw': '123', 'age': 16}
const profile2 = {'username':'tom', 'pw': '1234'}
const profile1Copy = {...profile1}
const resultProfile = {...profile1, ...profile2}console.log(profile1Copy)
console.log(
2.使用Object.assign()方法 (2. Using Object.assign() Method)
This is similar to using the spread operators above, which can be used for both arrays and objects.
这类似于使用上面的扩展运算符,该运算符可用于数组和对象。
const profile1 = {'username':'name', 'pw': '123', 'age': 16}
const profile2 = {'username':'tom', 'pw': '1234'}
const profile1Copy = Object.assign({}, profile1)
const resultProfile = Object.assign({},...profile1, ...profile2)console.log(profile1Copy)
console.log(
Note that I have used an empty object {}
as the first input because this method updates the first input from the result of the shallow merge.
请注意,我已使用空对象{}
作为第一个输入,因为此方法从浅合并的结果中更新了第一个输入。
3.使用.slice() (3. Using .slice())
This is just a convenient method just for shallow cloning arrays!
这只是用于浅克隆阵列的便捷方法!
const firstSet = [1, 2, 3];
const firstSetCopy = firstSet.slice()console.log(firstSetCopy) //note that they are not the same objects
console.log(firstSet===firstSetCopy) // false
4.使用lodash.clone() (4. Using lodash.clone())
Also note there is a method in lodash to do shallow cloning as well. I think it is a little overkill to use this (unless you already have lodash included) but I’ll just leave an example here.
还要注意,lodash中也有一种方法可以进行浅层克隆。 我认为使用此功能有点过大(除非您已经包括lodash了),但我在这里仅举一个示例。
const clone = require('lodash/clone')const profile1 = {'username':'name', 'pw': '123', 'age': 16}
const profile1Copy = clone(profile1)
...
浅克隆问题: (Problem of Shallow Cloning:)
For all of these examples of Shallow Cloning, issues start to come if we have deeper nesting of objects, like this example below.
对于所有这些浅层克隆示例,如果我们具有更深层的对象嵌套,就会出现问题,如以下示例所示。
const sampleprofile = {
'username':'name',
'pw': '123',
'particulars':{'firstname':'name', 'lastname':'name'}
}const profile1 = {...sampleprofile}profile1.username='tom'
profile1.particulars.firstname='Wong'console.log(sampleprofile)
/*
{
'username':'name',
'pw': '123',
'particulars':{'firstname':'Wong', 'lastname':'name'}
}
*/console.log(profile1)
/*
{
'username':'tom',
'pw': '123',
'particulars':{'firstname':'Wong', 'lastname':'name'}
}
*/console.log(sampleprofile.particulars===profile1.particulars) //true
Note how mutating the nested property (‘firstname’) of profile1
, also affects sampleprofile
.
请注意,如何sampleprofile
profile1
的嵌套属性(“名字”)也会影响sampleprofile
。
For Shallow Cloning, the nested object’s references are copied. So the objects of ‘particulars’ for both sampleprofile
and profile1
refer to the same object located at the same memory location.
对于“浅克隆”,将复制嵌套对象的引用。 因此, sampleprofile
和profile1
的“特殊对象”是指位于同一存储位置的同一对象。
To prevent such a thing from happening and if you want a 100% true copy with no external references, we need to use Deep Copy.
为了防止这种情况发生,如果您想要一个没有外部引用的100%真实副本,我们需要使用Deep Copy 。
深度复制 (Deep Copying)
1.使用JSON.stringify()和JSON.parse() (1. Using JSON.stringify() & JSON.parse())
This was not possible previously but for ES6, JSON.stringify() method is able to do deep copying of nested objects as well. However, note that this method only works great for Number, String and Boolean data types. Here’s an example in JSFiddle, try playing around to see what is copied and what’s not.
以前这是不可能的,但是对于ES6,JSON.stringify()方法也可以对嵌套对象进行深层复制。 但是,请注意,此方法仅适用于Number,String和Boolean数据类型。 这是JSFiddle中的一个示例,尝试四处查看复制的内容和未复制的内容。
Generally if you are only working with primitive data types and a simple object, this is a short & simple one-liner code to do the job!
通常,如果仅使用原始数据类型和简单对象,则这是一个简短的单行代码即可完成工作!
2.使用lodash.deepclone() (2. Using lodash.deepclone())
const cloneDeep = require('lodash/clonedeep')
const sampleprofile = {
'username':'name',
'pw': '123',
'particulars':{'firstname':'name', 'lastname':'name'}
}const profile1 = cloneDeep(sampleprofile)profile1.username='tom'
profile1.particulars.firstname='Wong'console.log(sampleprofile)
/*
{
'username':'name',
'pw': '123',
'particulars':{'firstname':'name', 'lastname':'name'}
}
*/console.log(profile1)
/*
{
'username':'tom',
'pw': '123',
'particulars':{'firstname':'Wong', 'lastname':'name'}
}
*/
FYI, lodash is included in react apps created with create-react-app
仅供参考,lodash包含在使用create-react-app创建的react应用中
3.自定义递归功能 (3. Custom Recursion Function)
If you don’t want to download a library just to do a deep copy, feel free to create a simple recursion function too!
如果您不想仅下载库来进行深拷贝,也可以创建一个简单的递归函数!
The code below (though doesn't cover all cases) gives a rough idea of how you can create this yourself.
下面的代码(尽管不能涵盖所有情况)大致介绍了如何自己创建此代码。
function clone(obj) {
if (obj === null || typeof (obj) !== 'object' || 'isActiveClone' in obj)
return obj;
if (obj instanceof Date)
var temp = new obj.constructor(); //or new Date(obj);
else
var temp = obj.constructor();
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
obj['isActiveClone'] = null;
temp[key] = clone(obj[key]);
delete obj['isActiveClone'];
}
}
return temp;
}
// taken from https://stackoverflow.com/questions/122102/what-is-the-most-efficient-way-to-deep-clone-an-object-in-javascript
Perhaps it is simpler to just download a library to implement deep cloning? There are other micro-libraries like rfdc, clone, deepmerge that does the job, in a smaller package size than lodash. You don’t have to download lodash just to use one function.
也许下载一个库来实现深度克隆会更简单吗? 还有其他微库(如rfdc , clone , deepmerge)可以完成此任务,并且封装的大小比lodash小。 您不必只下载lodash就可以使用一个功能。
Hope this gives you a perspective of the Object-Oriented nature of JavaScript, and how to handle bugs related to mutation in objects! This is a popular JavaScript interview question too. Thanks for reading! :)
希望这使您对JavaScript的面向对象的本质有一个了解,以及如何处理与对象变异有关的错误! 这也是一个受欢迎JavaScript面试问题。 谢谢阅读! :)
普通英语JavaScript (JavaScript In Plain English)
Enjoyed this article? If so, get more similar content by subscribing to Decoded, our YouTube channel!
喜欢这篇文章吗? 如果是这样,请订阅我们的YouTube频道解码,以获得更多类似的内容!