8.2 Avoiding sharing by copying data

Copying data is one way of avoiding sharing it.

8.2 Avoiding sharing by copying data - 图1 Background

For background on copying data in JavaScript, please refer to the following two chapters in this book:

8.2.1 How does copying help with shared mutable state?

As long as we only read from shared state, we don’t have any problems. Before modifying it, we need to “un-share” it, by copying it (as deeply as necessary).

Defensive copying is a technique to always copy when issues might arise. Its objective is to keep the current entity (function, class, etc.) safe:

  • Input: Copying (potentially) shared data passed to us, lets us use that data without being disturbed by an external entity.
  • Output: Copying internal data before exposing it to an outside party, means that that party can’t disrupt our internal activity.

Note that these measures protect us from other parties, but they also protect other parties from us.

The next sections illustrate both kinds of defensive copying.

8.2.1.1 Copying shared input

Remember that in the motivating example at the beginning of this chapter, we got into trouble because logElements() modified its parameter arr:

  1. function logElements(arr) {
  2. while (arr.length > 0) {
  3. console.log(arr.shift());
  4. }
  5. }

Let’s add defensive copying to this function:

  1. function logElements(arr) {
  2. arr = [...arr]; // defensive copy
  3. while (arr.length > 0) {
  4. console.log(arr.shift());
  5. }
  6. }

Now logElements() doesn’t cause problems anymore, if it is called inside main():

  1. function main() {
  2. const arr = ['banana', 'orange', 'apple'];
  3. console.log('Before sorting:');
  4. logElements(arr);
  5. arr.sort(); // changes arr
  6. console.log('After sorting:');
  7. logElements(arr); // (A)
  8. }
  9. main();
  10. // Output:
  11. // 'Before sorting:'
  12. // 'banana'
  13. // 'orange'
  14. // 'apple'
  15. // 'After sorting:'
  16. // 'apple'
  17. // 'banana'
  18. // 'orange'
8.2.1.2 Copying exposed internal data

Let’s start with a class StringBuilder that doesn’t copy internal data it exposes (line A):

  1. class StringBuilder {
  2. _data = [];
  3. add(str) {
  4. this._data.push(str);
  5. }
  6. getParts() {
  7. // We expose internals without copying them:
  8. return this._data; // (A)
  9. }
  10. toString() {
  11. return this._data.join('');
  12. }
  13. }

As long as .getParts() isn’t used, everything works well:

  1. const sb1 = new StringBuilder();
  2. sb1.add('Hello');
  3. sb1.add(' world!');
  4. assert.equal(sb1.toString(), 'Hello world!');

If, however, the result of .getParts() is changed (line A), then the StringBuilder ceases to work correctly:

  1. const sb2 = new StringBuilder();
  2. sb2.add('Hello');
  3. sb2.add(' world!');
  4. sb2.getParts().length = 0; // (A)
  5. assert.equal(sb2.toString(), ''); // not OK

The solution is to copy the internal ._data defensively before it is exposed (line A):

  1. class StringBuilder {
  2. this._data = [];
  3. add(str) {
  4. this._data.push(str);
  5. }
  6. getParts() {
  7. // Copy defensively
  8. return [...this._data]; // (A)
  9. }
  10. toString() {
  11. return this._data.join('');
  12. }
  13. }

Now changing the result of .getParts() doesn’t interfere with the operation of sb anymore:

  1. const sb = new StringBuilder();
  2. sb.add('Hello');
  3. sb.add(' world!');
  4. sb.getParts().length = 0;
  5. assert.equal(sb.toString(), 'Hello world!'); // OK