Internal method for comparing the equivalence of items.

@param x The first item to compare.
@param y The second item to compare.
@param rc The running counter for comparing circular references.
@returns {boolean} An indication as to whether or not x and y were equal.
function _equals(x, y, rc) {
    if (rc.count > rc.maxStackDepth)
        throw new Error("Stack depth exceeded: " + rc.maxStackDepth + "!");
    // check for reference and primitive equality
    if (x === y)
        return true;
    // check for type equality
    let xType = getType(x);
    let yType = getType(y);
    if (xType !== yType)
        return false;
    // check for circular references
    let xIndex = rc.xStack.lastIndexOf(x);
    let yIndex = rc.yStack.lastIndexOf(y);
    if (xIndex !== -1) {
        if (yIndex === xIndex)
            return true;
    }
    // check for inequalities
    switch(xType) {
        case types.number:
            if (x !== y) {
                if (isNaN(x) && isNaN(y))
                    return true;
                return false;
            }
            break;
        case types.date:
            if (x.getTime() !== y.getTime())
                return false;
            // check for extra properties stored on the Date object
            if (!_compareObject(x, y, rc))
                return false;
            break;
        case types.array:
            if (x.length !== y.length)
                return false;
            rc.push(x, y);
            for (let i = 0; i < x.length; i++) {
                if (!_equals(x[i], y[i], rc))
                    return false;
            }
            rc.pop();
            break;
        case types.map:
            if (x.size !== y.size)
                return false;
            let xMapArr = [...x]; // these need to be sorted by key before comparison
            let yMapArr = [...y]; // order shouldn't matter, as long as they are the same
            xMapArr.sort((a, b) => {
                return a[0] > b[0] ? 1 : a[0] < b[0] ? -1 : 0;
            });
            yMapArr.sort((a, b) => {
                return a[0] > b[0] ? 1 : a[0] < b[0] ? -1 : 0;
            });
            if (!_equals(xMapArr, yMapArr, rc))
                return false;
            break;
        case types.set:
            if (x.size !== y.size)
                return false;
            let xArr = [...x]; // consider doing a comparison without converting to array?
            let yArr = [...y]; // converting to array may still be the fastest option.
            if (!_equals(xArr, yArr, rc))
                return false;
            break;
        case types.function:
            if (!_compareObject(x, y, rc)) // check for properties on function
                return false;
            if (x !== y)
                return false; // other than that, just use reference equality for now
            break;
        case types.object:
            if (Buffer.isBuffer(x)) {
                if (!Buffer.isBuffer(y))
                    return false;
                if (x.length !== y.length)
                    return false;
                if (!x.equals(y))
                    return false;
            }
            else {
                if (!_compareObject(x, y, rc))
                    return false;
            }
            break;
        case types.regexp:
            if (!_equals(x.toString(), y.toString(), rc))
                return false;
            if (!_compareObject(x, y, rc))
                return false;
            break;
        case types.boolean:
        case types.string:
        case types.symbol:
            if (x !== y)
                return false;
            break;
        default:
            if (!_compareObject(x, y, rc))
                return false;
            break;
    }
    return true;
}