implement the merge functionality for 2048 with JavaScript
Review
Some review points on your code

Always delimit code blocks with {}. Eg  if (target !== 0) startIndex++; is safer as         if (target !== 0) { startIndex++; }

Avoid using continue (Its a goto in disguise). Rather the line if (array[endIndex] === 0) continue; is better as if (array[endIndex]) { /*... loop code ...*/ }. See rewrite.

Try to reduce array indexing by storing values in a variable. See rewrite

0 is falsey so if (target !== 0) startIndex++; can be if (target) { startIndex++; } and if (target === 0 ||  can be if (!target || 

Variables that do not change should be constants. eg  let target = array[start]; can be const target = array[start];

Try to keep names short. Long names make code harder to read


Rewrite
rewriting your function using the points above.

function mergeLeft(row) {
    var start = 0;
    for (let end = 1; end &lt; row.length; end++) {
        const tile = row[end];
        if (tile) {
            const target = row[start];
            if (!target || target === tile ) {
                row[start] += tile ;
                row[end] = 0;
            } else if (start + 1 &lt; end) {
                end--;
            }
            if (target) { start++; }
        }
    }
    return row;
}

Coordinate transform
Use a single dimension (1D) array to store the game board. The position on the board can be calculated from a coordinate pair. x = 2, y = 1 is 3rd column 2nd row. The index in a 1D array is index = x + y * size where size is the number of columns (4 in this case)
Your merge function moves from left to right, then down (I assume) to the next row and does the same. You can create an inner (columns) and outer (rows) loop, and calculate the board index from the coordinate pairs x,y

for (y = 0; y &lt; 4; y ++) {
    for (x = 0; x &lt; 4; x ++) {
        index = x + y * 4;
        // merge row
    }
}

The above steps over the arrays in the same direction as your mergeLeft function
To have it handle different directions you can define some functions that convert (transform) the x, y position to the correct index for the 4 directions. Up, Right, Left, and Down
For Example
const size = 4;                             // width and height of board
const tileCount = size * size;
const board = new Array(tileCount).fill(0); // Create the array of tiles

// named directions. The name of the key press
const moves = {
    up: 0,
    right: 1,
    down: 2,
    left: 3,
};

// functions that convert x,y position into board index
const coordTransform = [
    (x, y) =&gt; y + x * size,                // up from top down
    (x, y) =&gt; (size - 1) - x + y * size,   // right for right to left
    (x, y) =&gt; y + ((size - 1) - x) * size, // down from bottom up
    (x, y) =&gt; x + y * size,                // left from left to right
];

Before merging the rows we get the direction function const move = coordTransform[moves.up];
Then in the loop we use that function to transform the coordinates so that we step over items in the correct direction for the move. const idx = move(x, y); gets the transformed position.
Example using the above code to merge in any direction.
// The comments and naming is in terms of rows from left to right. move(moves.left)
// Call the function with the direction index eg move(moves.up);
function move(dir) {  
    // Tiles will stack to the left. 
    // left is the idx of the next left free pos
    // prev holds the prev left pos and is used to merge values if same        
    var x, y = 0, left, prev;

    // get the function to convert coord to board index
    const move = coordTransform[dir];  //  transform function for direction
    while (y &lt; size) {
        left = x = 0;
        prev = -1;
        while (x &lt; size) {
            const idx = move(x, y);  // Get the tile index using transform function
            const tile = board[idx]; // Get the tile value
            if (tile) {              // Does tile have a value
                board[idx] = 0;                  // Remove the tile from the board
                const idxLeft = move(left, y);   // Get next stack pos
                board[idxLeft] = tile;           // Set the tile value
                if (prev &gt; -1) {                 // Is there a tile to left
                    if (tile === board[prev]) {  // Is tile to left the same
                        board[prev] += tile;     // Merge the tile values
                        board[idxLeft] = 0;      // Remove second tile, 
                        left--                   // Move index back 
                        prev = -1;               // Ensure only two merge at a time
                    } else { prev = idxLeft }    // If tile did not merge remember 
                } else { prev = idxLeft  }       //    position of tile
                left ++;                         // Move stack pos to next tile
            }
            x++;     // step to next column
        } 
        y++;         // step to next row.
    }
}

UPDATE Implementing the game.
The snippet is an implementation of the game without an in depth study of the original game.
It uses a partial OO approach

&quot;If we are going to build this game using a traditional object-oriented way, what are the class we need to design here?&quot;

Objects used

Note that JS uses Objects. You should not use the term class due to the significant differences between classes and objects in computer science.

I do not use the class syntax due to its very ill though out implementation (incomplete and a syntactical hack)


There is a factory function Game that encapsulates the game, with support code to add the UI for the particular environment (Code review snippet) placed outside Game.
The Game object uses closure to create private functions and properties. It also reduces the need to add the noisy this. and the even hackier this.#
Within Game is the Object Tile that uses the traditional OO JS prototype object style to implement the display and animation of tiles.
There are also various simple objects within Game
. states  Holds enumerated game states.
. moves Holds enumerated game actions (a move is a keyboard (swipe, or mouse, if implemented)) direction to move tiles
. moveTransform is the transform used to convert 2D tile positions to 1D tile array positions depending on the move direction. (See details in first part of answer)
Outside the Game object
The keyboard interface uses a simple static object keys via an immediately invoked factory function (using closure to encapsulate private function) that ensures the will only be one instance of the keyboard interface.
Question 2

&quot;I figured that we can use a 2-dimensional array to represent the board, or as one of the answers presented, we can use a 1-dimensional array to hold x,y coordinates, I wonder what are some of the pros and cons with either approach?&quot;

The main reason is to reduce complexity. Rather than have to deal with coordinate pairs only a single value is needed to locate a tile. This reduces the complexity .
This game only has a small number of tiles 16 (though example allows up to 64 (it will handle many more but has be limited in the example to prevent poor performance on low end devices).
Many games use 2D layouts with 1000s of tiles, using 1D rather than 2D has many advantages

1D arrays reduce the overhead associated with indexing arrays items

1D arrays use less memory

1D arrays can easily be converted to typed arrays without the need for complex instantiation code. On low end devices a typed array (as a shared array) allows logic in worker threads to share the data.


Question 3

&quot;I was looking at it from an OOD approach and I was also aware that we can use functional programming instead. Here is my attempt&quot;

Functional programming (FP) as a style in JS adds way too much processing and memory overheads to make it a practical for realtime games.
FP in JS has been adopted as a defense against poor encapsulation that is so common in JS.
Using a robust encapsulation style there is (in my view) no need to write in FP style in JS
The example below is written to fit the code review snippet and as such can not use modules.
Modules add an additional layer of encapsulation to JS code preventing side effects by creating privates scopes per module further reducing the need to use the FP style.


const eCurve = (v, p = 2) =&gt;  v &lt;= 0 ? 0 : v &gt;= 1 ? 1 : v ** p / (v ** p + (1 - v) ** p);
const bellCurve = (v, p = 2) =&gt;  v &lt;= 0 ? 0 : v &gt;= 1 ? 0 : (v *= 2, (v = v &gt; 1 ? 2 - v : v),  v ** p / (v ** p + (1 - v) ** p));
const bCurve = (v, a, b) =&gt; v &lt;= 0 ? 0 : v &gt;= 1 ? 1 : 3*v*(1-v)*(a*(1-v)+b*v)+v*v*v; // cubic bezier curve
Math.PI90 = Math.PI / 2;
Math.TAU = Math.PI * 2;
Math.PI270 = Math.PI * (3 / 2);
const randUI = (min, max) =&gt; (max !== undefined ? Math.random() * (max - min) + min : Math.random() * min) | 0;
const randPick = arr =&gt; arr.splice(randUI(arr.length), 1)[0];
const randItem = arr =&gt; arr[randUI(arr.length)];
const tag = (tag, props = {}) =&gt; Object.assign(document.createElement(tag), props);

canvas.width = innerWidth;
canvas.height = innerHeight;
const ctx = canvas.getContext("2d");
const keys = (()=&gt;{
    const keys = {
        ArrowLeft: false, 
        ArrowRight: false, 
        ArrowUp: false, 
        ArrowDown: false
    };
    document.addEventListener("keydown", keyEvent);
    document.addEventListener("keyup", keyEvent);
    function keyEvent(e) {
       if (keys[e.code] !== undefined) { keys[e.code] = e.type === "keydown"; e.preventDefault() }
    }
    return keys;
})();
var pageResized = false, inGame = false, instructionsKnown = 0, gameType = 1;
addEventListener("resize", () =&gt; { if (inGame) { pageResized = true } });
sel.addEventListener("click", (e) =&gt; {
  if (!inGame &amp;&amp; e.target.dataset.game !== undefined) {
      gameType = e.target.dataset.game;
      sel.style.display = "none";
      playGame(gameType);
  }

});

const pathRoundBox = (ctx, x, y, w, h, r) =&gt; {
    const w2 = w / 2, h2 = h / 2;
    ctx.arc(x - w2 + r, y - h2 + r, r, Math.PI, Math.PI270);
    ctx.arc(x + w2 - r, y - h2 + r, r, Math.PI270, Math.TAU);
    ctx.arc(x + w2 - r, y + h2 - r, r, 0, Math.PI90);
    ctx.arc(x - w2 + r, y + h2 - r, r, Math.PI90, Math.PI);
}
function Game(size, winTile, speed = 20, tilesPerMove = 2) {
    var state;
    var moveCount = 0, displayDirty = true;
    const animRate = 1 / speed;
    const tileCount = size * size;
    const board = new Array(tileCount).fill(0);
    const mergingTiles = [];
    const freeTiles = [];
    const newTileVal = [];
    const newTileSet = [2,4,2,4,2,4,2,4,2,4,2,4];
    const states = {
        over: 1,
        inPlay: 2,
        animating: 3,
        win: 4,
    };
    state = states.inPlay;
    const moves = {
        up: 0,
        right: 1,
        down: 2,
        left: 3,
    };
    const moveTransform = [
        (x, y) =&gt; y + x * size,
        (x, y) =&gt; (size - 1) - x + y * size,
        (x, y) =&gt; y + ((size - 1) - x) * size,
        (x, y) =&gt; x + y * size,
    ];
    function Tile(val, idx, growDelay = 0) {
        this.nx = this.x = idx % size;
        this.ny = this.y = idx / size | 0;
        this.val = val;
        this.anim = 0;
        this.alpha = 1;
        this.animate = true;
        this.merging = false;
        this.grow = true;
        this.size = -growDelay;
        this.flash = false;
        this.flashAlpha = 0;
    }
    Tile.prototype = {
        newPos(idx) {
            this.nx = idx % size;
            this.ny = idx / size | 0;
            this.ax = this.x;
            this.ay = this.y;
            this.anim = 1;
            this.animate = true;
        },
        merge(idx) {
            this.newPos(idx);
            this.merging = true;
            return this;
        },
        mergeFlash(idx) {
            this.animate = true;
            this.flash = true;
            this.flashAlpha = 1;
            this.val *= 2;
            return this;
        },
        update() {
            if (this.merging) {
                this.alpha -= animRate;
                if (this.alpha &lt;= 0) { 
                    this.alpha = 0;
                    this.merging = false;
                }   
            }
            if (this.anim &gt; 0) {
                this.anim -= animRate;
                if (this.anim &lt;= 0) { this.anim = 0 }
            }
            if (this.grow) {
                this.size += animRate;
                if (this.size &gt; 1) {
                    this.size = 1;
                    this.grow = false;
                }
            }
            if (this.flash) {
                this.flashAlpha -= animRate * 2;
                if (this.flashAlpha &lt; 0) {
                    this.flashAlpha = 0;
                    this.flash = false;
                }
            }
            this.animate =  this.flash || this.grow || this.merging || this.anim &gt; 0;
            displayDirty = this.animate ? true : displayDirty;
            if (this.anim &gt; 0) { 
                const p = eCurve(1 - this.anim);
                this.x = (this.nx - this.ax) * p + this.ax;
                this.y = (this.ny - this.ay) * p + this.ay;
            } else {
                this.x = this.nx;
                this.y = this.ny;
            }
        },
        render(target) {
            const {ctx, scale, radius, cols, textCol, pad} = target;
            ctx.save()
            const hS = scale / 2;
            var x = this.x * scale;
            var y = this.y * scale;
            const inScale = this.flash ?  1 + bellCurve(this.flashAlpha) * 0.1 : bCurve(this.size, 0, 1.6);
            ctx.transform(inScale, 0, 0 , inScale, x + hS, y + hS);
            ctx.globalAlpha = eCurve(this.alpha);
            ctx.fillStyle = cols[this.val];
            ctx.beginPath();
            pathRoundBox(ctx, 0, 0, scale - pad * 2, scale - pad * 2, radius);
            ctx.fill();
            if (this.flash) {
                ctx.globalCompositeOperation = "lighter";
                ctx.globalAlpha = eCurve(this.flashAlpha);
                ctx.fillStyle = "#FFF";
                ctx.fill();
                ctx.globalCompositeOperation = "source-over";
                ctx.globalAlpha = 1;
            }
            ctx.fillStyle = textCol;
            ctx.fillText(this.val, 0, 0 + scale * 0.05);
            ctx.restore()
        }
    }
    function addTile(growDelay) {
        const pos = randPick(freeTiles);
        if (newTileVal.length === 0) { newTileVal.push(...newTileSet) }
        board[pos] = new Tile(randPick(newTileVal), pos, growDelay);
    }
    function checkBoard() {
        var i = tileCount, s = state;
        freeTiles.length = 0;
        while (i--) {
            const t = board[i];
            if (t?.val === winTile) {
                s = states.win;
            } else if (t === 0) {
                freeTiles.push(i);
            }
        }
        if (freeTiles.length &lt; tilesPerMove &amp;&amp; s !== states.win) { s = states.over }
        API.state = s;
    }
    function move(dir) {
        moveCount ++;
        var x, y = 0, f, prev;
        const move = moveTransform[dir];
        while (y &lt; size) {
            f = x = 0;
            prev = -1;
            while (x &lt; size) {
                const idx = move(x, y);
                const tile = board[idx];
                board[idx] = 0;
                if (tile) {
                    const idxf = move(f, y);
                    board[idxf] = tile;
                    idxf !== idx &amp;&amp; tile.newPos(idxf);
                    if (prev &gt; -1) {
                        if (tile.val === board[prev].val) {
                            board[prev].mergeFlash();
                            mergingTiles.push(tile.merge(prev));
                            board[idxf] = 0;
                            f--;
                            prev = -1;
                        } else { prev = idxf }
                    } else { prev = idxf }
                    f++;
                }
                x++;
            }
            y++;
        }
    }
    const API = {
        states: Object.freeze(states),
        moves: Object.freeze(moves),
        get displayDirty() {
            if (state === states.animating) { displayDirty = true }
            const res = displayDirty;
            displayDirty = false;
            return res;
        },
        get moveCount() { return moveCount },
        get state() { return state },
        set state(s) {
            if (s !== state) {
                if (state === states.animating &amp;&amp; s === states.inPlay) {
                    state = s;
                    checkBoard();
                    if (state === states.inPlay) {
                        let i = 0
                        while (i &lt; tilesPerMove) { addTile(i++ / 2) }
                        displayDirty = true;
                    }
                } else {
                    state = s;
                    displayDirty = true;
                }
            }
        },
        set move(dir) {
            if (state === states.inPlay) {
                if (dir &gt;= 0 &amp;&amp; dir &lt; 4) {
                    move(dir);
                    API.state = states.animating;
                }
            }
        },
        reset() {
            board.fill(0);
            API.state = states.animating;
            moveCount = 0;
            checkBoard();
        },
        render(target) {
            var i = 0, tail = 0, animating = false;
            board.forEach(tile =&gt; {
                if (tile !== 0) {
                    tile.animate &amp;&amp; (animating = true, tile.update());
                    tile.render(target);
                }
            });
            while (i &lt; mergingTiles.length) {
                const tile = mergingTiles[i];
                tile.update();
                tile.render(target);
                if (tile.animate) {
                    animating = true
                    mergingTiles[tail++] = tile
                }
                i++;
            }
            mergingTiles.length = tail;
            !animating &amp;&amp; state === states.animating &amp;&amp; (API.state = states.inPlay);
        },
    }
    return Object.freeze(API);
}


function createBGImage(img, tiles, pad, radius) {
    const ctx = img.getContext("2d");
    ctx.fillStyle = "#222";
    ctx.beginPath();
    pathRoundBox(ctx, img.width / 2, img.height / 2, img.width, img.height, radius + pad * 2);
    ctx.fill();
    var i = 0;
    const size = img.width / tiles;
    ctx.fillStyle = "#666";
    while (i &lt; tiles * tiles) {
        const x = (i % tiles) + 0.5;
        const y = (i / tiles | 0)  + 0.5;
        ctx.beginPath();
        pathRoundBox(ctx, x * size, y * size, size - pad * 2, size - pad * 2, radius);
        ctx.fill();
        i++;
    }
    return img;
}   


function playGame(game = gameType) {
    inGame = true;
    const GAMES = [
       [4, 512,  20, 2],
       [5, 2048, 20, 2],
       [6, 2048, 20, 2],
       [7, 1024, 20, 3],
       [8, 2048, 16, 4],
    ];
    const SIZE = GAMES[game][0];
    const g = Game(...GAMES[game]);
    g.reset();
    ctx.canvas.width = innerWidth;
    ctx.canvas.height = innerHeight;
    requestAnimationFrame(animLoop);
    const minRes =  Math.min(ctx.canvas.width, ctx.canvas.height);
    const unit =  minRes / 64;
    const RADIUS = minRes / SIZE / 8, PAD = minRes / SIZE / 16;
    const bgImage = createBGImage(tag("canvas", {width: minRes, height: minRes}), SIZE, PAD / 2, RADIUS + PAD / 2);
    const renderTarget = {
        ctx,
        scale:  minRes / SIZE,
        radius: RADIUS,
        pad:    PAD,
        textCol: "#000",
        cols: {
            [2]:    "#F40",
            [4]:    "#F80",
            [8]:    "#FA0",
            [16]:   "#FC0",
            [32]:   "#FF0",
            [64]:   "#CF0",
            [128]:  "#8F0",
            [256]:  "#0F0",
            [512]:  "#0F8",
            [1024]: "#0FF",
            [2048]: "#FFF",
        }    
    }
    ctx.font = (renderTarget.scale * 0.7 | 0) + "px Arial";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    function animLoop() {
        var m = -1;

        // Rather than dynamic resize this just restarts on resize
        if (pageResized) {
            pageResized = false;
            setTimeout(playGame, 0);
            inGame = false;
            return;
        }
        if (keys.ArrowUp) { keys.ArrowUp = false; m = g.moves.up }
        if (keys.ArrowRight) { keys.ArrowRight = false; m = g.moves.right }
        if (keys.ArrowDown) { keys.ArrowDown = false; m = g.moves.down }
        if (keys.ArrowLeft) { keys.ArrowLeft = false; m = g.moves.left }
        
        if (m &gt; -1) { 
            g.move = m; 
            instructionsKnown++;
        }
        const state = g.state;
        if (g.displayDirty) {
            ctx.textAlign = "center";
            ctx.textBaseline = "middle";
            ctx.setTransform(1,0,0,1,0,0);
            ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
            ctx.font = (renderTarget.scale * 0.45 | 0) + "px Arial";
            ctx.setTransform(1,0,0,1,(canvas.width - SIZE * renderTarget.scale) / 2, 0);
            ctx.drawImage(bgImage, 0, 0);
            g.render(renderTarget);
            if (!(state === g.states.inPlay || state === g.states.animating)) {
                ctx.fillStyle = "#0008";
                ctx.setTransform(1,0,0,1,0,0);
                ctx.fillRect(0, 0, ctx.canvas.width,  ctx.canvas.height);
                ctx.font = (renderTarget.scale * 0.7 | 0) + "px Arial";
                ctx.textAlign = "center";
                ctx.textBaseline = "middle";        
                ctx.fillStyle = "#FFF";
                ctx.strokeStyle = "#000";
                ctx.lineWidth = unit * 2;
                ctx.lineJoin = "round";
                const cx =  ctx.canvas.width * 0.5;
                const cy =  ctx.canvas.height * 0.3
                if (state === g.states.win) {
                    ctx.strokeText("WINNER!", cx + unit / 2, cy + unit / 2);
                    ctx.fillText("WINNER!",cx, cy)
                    ctx.strokeStyle = "#FED";
                    ctx.lineWidth = unit / 2;
                    ctx.lineJoin = "round";
                    ctx.strokeText("WINNER!", cx, cy)
                    ctx.font = (renderTarget.scale * 0.4 | 0) + "px Arial";
                    ctx.fillStyle = "#FFF";
                    ctx.strokeStyle = "#000";
                    ctx.lineWidth = unit;
                    const mC = g.moveCount;
                    ctx.strokeText("Moves: " + mC, cx + unit / 4, cy + unit * SIZE * 3 + unit / 4);
                    ctx.fillText("Moves: " + mC, cx, cy + unit * SIZE * 3);
                    
                } else {
                    ctx.strokeText("GAME OVER", cx + unit / 2, cy + unit / 2);
                    ctx.fillText("GAME OVER", cx, cy)
                    ctx.strokeStyle = "#DEF";
                    ctx.lineWidth = unit  / 2;
                    ctx.lineJoin = "round";
                    ctx.strokeText("GAME OVER", cx, cy);
                }

                inGame = false;
                return;
            } 
            if (instructionsKnown &lt; 2) {
                ctx.setTransform(1,0,0,1,0,0);
                ctx.fillStyle = "#FFF";
                ctx.font = "16px Arial";
                ctx.fillText("Use arrow keys to play", ctx.canvas.width / 2, 20);
            }            
        }
        requestAnimationFrame(animLoop);
    }
}
canvas {
   position: absolute;
   top: 0px;
   left: 0px;
   pointer-events: none;
}
#sel {
  font-family: arial;
  display: flex;
}
.bt {
   flex-wrap: row;
   cursor: pointer;
   width: 50px;
   text-align: center;
   border: 1px solid black;
   margin: 4px;
   padding-top: 3px;
}
.bt:hover {
   background: #D92;
}
.txt {
  margin-top: 8px;
}
&lt;canvas id="canvas"&gt;&lt;/canvas&gt;
&lt;div id="sel"&gt;
   &lt;div class="txt"&gt;Select grid size:&lt;/div&gt;
   &lt;div class="bt" data-game="0"&gt;4 by 4&lt;/div&gt;
   &lt;div class="bt" data-game="1"&gt;5 by 5&lt;/div&gt;
   &lt;div class="bt" data-game="2"&gt;6 by 6&lt;/div&gt;
   &lt;div class="bt" data-game="3"&gt;7 by 7&lt;/div&gt;
   &lt;div class="bt" data-game="4"&gt;8 by 8&lt;/div&gt;
&lt;/div&gt;



Note Code review pages use a lot of JS and as such impact the performance of snippets. To get a true feel of how the game plays copy the code to a stand alone page..
Note as there is a limit to code review answer size the above was coded to fit and as such lost a lot of code and reduced naming length to fit.