Hatefiend
Hatefiend

Reputation: 3596

Shallow clone of an object? - Javascript

I have one core object to my project called SudokuBoard. The only field SudokuBoard has is a 2D array. All prototype functions of SudokuBoard involve the 2D array and need visibility of it at all times.

The problem:

In an algorithm coming up, I NEED to have some way to make copies of the SudokuBoard. It's mandatory functionality. Whenever I try to make a copy, it's 2D array is simply a reference to the old one. I'm not sure why.

I read about how clone is an absolute nightmare in Javascript so I'm a bit worried. I'm a beginner so the last thing I want to do is install Jquery or use some external library to solve this problem. I provided the files below; they should run with no errors.

SudokuBoard.js

/**
 * Constructs a SudokuBoard object.
 * Initializes the board with the numbers
 * provided to the constructor.
 * @param nums array, must be BOARD_SIZE^2 length.
 * @constructor
 */

function SudokuBoard(nums)
{
    // Private Fields:

    var BOARD_SIZE = 9;
    // The Sudoku board, represented as a 2D array.
    var gameboard = [];

    if (nums.length != BOARD_SIZE * BOARD_SIZE)
    {
        document.write("InvalidSizeError");
        throw "InvalidSizeError";
    }

    var counter = 0;
    for (var i = 0; i < BOARD_SIZE; i++)
    {
        var row = [];

        // Each row has a set amount of elements.
        while (row.length < BOARD_SIZE)
        {
            row.push(nums[counter]);
            counter++;
        }

        // Add the row to the board.
        gameboard.push(row);
    }

    SudokuBoard.prototype.getBoard = function()
    {
        return gameboard;
    }
}

/**
 * Gets all values within a row of the 2D array.
 * The Y coordinate works on the typical number
 * scale, meaning indexes start from 1, not 0.
 * Y corresponds to the vertical axis. The bottom
 * left of the board is at 1,1. The bottom right
 * of the board is at 9,1.
 * @param y coordinate of the row.
 * @returns {Array}
 */
SudokuBoard.prototype.getRow = function(y)
{
    return this.getBoard()[this.getBoard().length - y];
};

/**
 * Gets all values within a column of the 2D array.
 * The X coordinate works on the typical number
 * scale, meaning indexes start from 1, not 0.
 * X corresponds to the horizontal axis. The bottom
 * left of the board is at 1,1. The bottom right
 * of the board is at 9,1.
 * @param x coordinate of the column.
 * @returns {Array}
 */
SudokuBoard.prototype.getColumn = function(x)
{
    var column = [];

    for (var i = 1; i <= this.getBoard().length; i++)
    {
        column.push(this.getSlot(x, i));
    }

    return column;
};

/**
 * Algorithm which finds the correct quadrant of a given
 * coordinate and gets all the numbers which are contained
 * inside it. This operation relies on the fact that there
 * are three quadrants and once you make it so the first
 * index of quadrant one is considered as (3,3) you can
 * divide all X and Y values by 3 and yield their quadrant #.
 * @param x coordinate.
 * @param y coordinate.
 * @returns {Array}
 */
SudokuBoard.prototype.getQuadrant = function(x, y)
{
    // Determine what quadrant this coordinate is in.
    var horizQuad = Math.floor((x + 2) / 3); // 1 2 or 3
    var vertQuad = Math.floor((y + 2) / 3); // 1 2 or 3

    var quadrant = [];

    for (var i = 1; i <= 3; i++)
    {
        for (var h = 1; h <= 3; h++)
        {
            // Add the number to the array.
            quadrant.push(this.getSlot((horizQuad - 1) * 3 + i, (vertQuad - 1) * 3 + h));
        }
    }

    return quadrant;
};

/**
 * Gets a given slot on the board.
 * The X,Y coordinates work on the typical number
 * scale, meaning indexes start from 1, not 0.
 * X corresponds to the horizontal axis while Y
 * corresponds to the vertical axis. The bottom
 * left of the board is at 1,1. The bottom right
 * of the board is at 9,1.
 * @param x coordinate.
 * @param y coordinate.
 */
SudokuBoard.prototype.getSlot = function(x, y)
{
    return this.getBoard()[this.getBoard().length - y][x - 1];
};

/**
 * Sets a given slot on the board to a value.
 * The X,Y coordinates work on the typical number
 * scale, meaning indexes start from 1, not 0.
 * X corresponds to the horizontal axis while Y
 * corresponds to the vertical axis. The bottom
 * left of the board is at 1,1. The bottom right
 * of the board is at 9,1.
 * @param x coordinate.
 * @param y coordinate.
 * @param value to be placed.
 */
SudokuBoard.prototype.setSlot = function(x, y, value)
{
    this.getBoard()[this.getBoard().length - y][x - 1] = value;
};

SudokuBoard.prototype.clone = function()
{
    var numbers = [];

    for (var i = 0; i < this.getBoard().length; i++)
    {
        for (var h = 0; h < this.getBoard()[i].length; h++)
        {
            numbers.push(this.getBoard()[i][h]);
        }
    }

    return new SudokuBoard(numbers);
};

/**
 * ToString() method for SudokuBoard.
 * @returns {string}
 */
SudokuBoard.prototype.toString = function()
{
    const border = "+-----+-----+-----+";
    const nextline = "<br>";

    var temp = border + nextline;

    for (var i = 0; i < this.getBoard().length; i++)
    {
        temp += "|";

        for (var h = 0; h < this.getBoard()[i].length; h++)
        {
            // Every third character is proceeded by a |
            //\u00A0 for empty space.
            temp += ((this.getBoard()[i][h] == "0") ? "-" : this.getBoard()[i][h]) + ((h % 3 == 2) ? "|" : " ");
        }

        // Add a new line.
        temp += nextline;
    }

    // Return and add the bottom border.
    return temp + border;
};

Tester.js

var nums = [0, 0, 0, 0, 0, 0, 1, 4, 6,
            4, 0, 8, 7, 0, 0, 0, 0, 0,
            0, 6, 0, 0, 5, 0, 8, 9, 0,
            0, 0, 0, 1, 0, 0, 0, 0, 3,
            0, 8, 0, 0, 7, 4, 0, 0, 0,
            7, 0, 0, 0, 0, 0, 9, 0, 0,
            0, 0, 1, 8, 0, 9, 2, 0, 0,
            0, 0, 0, 5, 0, 0, 0, 0, 0,
            8, 0, 3, 0, 1, 7, 0, 0, 0];

var myBoard = new SudokuBoard(nums);
println("ORIGINAL:");
println(myBoard);

var clone = myBoard.clone();
println("CLONING:");
println(clone);

myBoard.setSlot(1, 1, 3);
println("CHANGED ORIGINAL:");
println(myBoard);

println("CLONE:");
println(clone);




/**
 * Used for debugging.
 * @param line
 */
function println(line)
{
    document.write(line + "<br>");
}

Runner.html

    <!DOCTYPE html>
<!--
  Project: SudokuSolver
  Name: Kevin
  Date: 2/12/2016
 -->

<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
    <font face="monospace"><font size="12">
    <!--Load from JavaScript file. -->
    <script type="text/javascript" src="SudokuBoard.js"></script>
    <script type="text/javascript" src="Tester.js"></script>
    </font></font>
</head>
<body>

</body>
</html>

Upvotes: 2

Views: 570

Answers (3)

nnnnnn
nnnnnn

Reputation: 150010

I don't see anything wrong with your .clone() method.

The problem is in your constructor, in how it sets up the .getBoard() method. Each time your constructor is called you overwrite the prototype .getBoard() method, and that method refers to a local variable that is captured by the closure, not an instance variable. So call .getBoard() for any instance of SudokuBoard and you get the prototype method that will reference the local variable from the latest instance's closure and thus only return the most recently created board.

Change this line:

SudokuBoard.prototype.getBoard = function()

to create an instance method instead of a prototype method:

this.getBoard = function()

If every instance has its own .getBoard() they'll all reference their own closure.

Upvotes: 3

Haring10
Haring10

Reputation: 1557

Primitives are passed by value, Objects are passed by "copy of a reference".

This is a snippet I have that my mentor gave me to clone JavaScript objects:

function cloneObj(obj) {
    var result = {};
    for(var key in obj) {
        if (obj.hasOwnProperty(key)) {
            if (typeof obj[key] == 'object') {
                result[key] = cloneObj(obj[key]);
            } else {
                result[key] = obj[key];
            }
        }
    }
    return result;
};

--

Or maybe try this (from this post)?

// Shallow copy
var newObject = jQuery.extend({}, oldObject);

// Deep copy
var newObject = jQuery.extend(true, {}, oldObject);

Upvotes: 0

Vitaliy Terziev
Vitaliy Terziev

Reputation: 6671

Try with David Flanagan's extend.

/*
 * Add a nonenumerable extend() method to Object.prototype.
 * This method extends the object on which it is called by copying properties
 * from the object passed as its argument.  All property attributes are
 * copied, not just the property value.  All own properties (even non-
 * enumerable ones) of the argument object are copied unless a property
 * with the same name already exists in the target object.
 */
Object.defineProperty(Object.prototype,
    "extend",                  // Define Object.prototype.extend
    {
        writable: true,
        enumerable: false,     // Make it nonenumerable
        configurable: true,
        value: function(o) {   // Its value is this function
            // Get all own props, even nonenumerable ones
            var names = Object.getOwnPropertyNames(o);
            // Loop through them
            for(var i = 0; i < names.length; i++) {
                // Skip props already in this object
                if (names[i] in this) continue;
                // Get property description from o
                var desc = Object.getOwnPropertyDescriptor(o,names[i]);
                // Use it to create property on this
                Object.defineProperty(this, names[i], desc);
            }
        }
    });


var mainObject = {name:'test'};
var clone = {};
clone.extend(mainObject);

Upvotes: 0

Related Questions