我不知道的Set

早上朋友发来一道面试题

印象中Set的机制是会把初始时候的可迭代对象调用next再一个个add进去,自然而然的想到了-0

javascript 复制代码
console.log(...new Set([null, null, undefined, undefined, NaN, NaN, -0, 0, +0]));// null undefined NaN 0

意料之外的结果,为什么是0?

Set如何判断重复?

先回到经典问题:Set是如何判断重复的?我们可以手写一个低级Set:

手写一个Set

kotlin 复制代码
class xSet {
    constructor(iterable = []) {
        this.items = [];
        if (iterable[Symbol.iterator]) {
            for (let item of iterable) {
                this.add(item);
            }
        }
    }

    add(value) {
        if (!this.has(value)) {
            this.items.push(value);
        }
        return this;
    }

    delete(value) {
        const index = this.items.findIndex(item => item === value);
        if (index !== -1) {
            this.items.splice(index, 1);
            return true;
        }
        return false;
    }

    has(value) {
        return this.items.some(item => item === value);
    }

    clear() {
        this.items = [];
    }

   size() {
        return this.items.length;
    }

    [Symbol.iterator]() {
        let index = 0;
        const items = this.items;
        return {
            next() {
                if (index < items.length) {
                    return { value: items[index++], done: false };
                } else {
                    return { done: true };
                }
            }
        };
    }

    keys() {
        return this[Symbol.iterator]();
    }

    values() {
        return this[Symbol.iterator]();
    }

    entries() {
        let index = 0;
        const items = this.items;
        return {
            next() {
                if (index < items.length) {
                    return { value: [items[index], items[index++]], done: false };
                } else {
                    return { done: true };
                }
            }
        };
    }

    forEach(callbackFn, thisArg) {
        for (let item of this.items) {
            callbackFn.call(thisArg, item, item, this);
        }
    }
}
去重核心

可以观察到new xSet的流程其实会调用add方法,add的时候会调用has,那么核心来了,has的判断条件是 item === value

javascript 复制代码
console.log(...new xSet([null, null, undefined, undefined, NaN, NaN, -0, 0, +0]));//null undefined NaN NaN -0
修改去重条件

与预期不符合,根据上文我们知道核心其实是has里面的判断条件,所以我们封装一个判断相等的函数,这个函数逻辑里面NaN 与 NaN算相等

javascript 复制代码
const sameValueZero = (x, y) => {
    return x === y || (typeof x === 'number' && typeof y === 'number' && isNaN(x) && isNaN(y));
}

has(value) {
        return this.items.some(item => sameValueZero(item , value));
    }

很好现在NaN已经被处理了,回到问题的核心那么-0为什么会被0呢

为什么输入的-0会变成0

猜想

1. 控制台显示问题

2. 数组转换的-0

3. Set转换的-0

验证

1. 控制台显示问题

这个问题最好解决我们打印-0, 出于严谨在node环境和浏览器环境都打印。

可以排除是控制台的显示问题。

2. 数组转换的-0

以下测试浏览器和node环境结果一致

打印一个数组:

arduino 复制代码
console.log([-0,0,+0]);// [ -0, 0, 0 ]

很好又排除掉一个猜想。

3. Set转换的-0

带着拨云见日以及可能前功尽弃的心情来到第三个测试:

arduino 复制代码
const set = new Set()
set.add(-0)
console.log(set);// Set(1) { 0 }
console.log(...set.keys()); // 0

随着一个个结果的打印心里的石头也是放下了。

新猜想与解决

这时候突然萌生出一个离谱的想法: 我看见是0那么一定是0吗,有没有可能还有什么导致显示问题?

好了现在自己把自己困住了,知识体系不完整常常陷入这种困境,思考几分钟后我突然想到,那么我用代码证明他是0不就行了!

判断0 和 -0 可太简单了,请出我们的Object.is

vbnet 复制代码
console.log(Object.is(-0,0)); // false
console.log(Object.is(...set.keys(),0)); // true

这一刻终于释怀了,问题的原因终于定位到了。

调研Set的机制

激动的打开es官网搜索Set

es官方定义的Set

new的时候调用了add,根据查看es其他方法的经验大胆猜想add的时候转换了-0

add方法

CanonicalizeKeyedCollectionKey的处理

结论

到这里终于解决了为什么-0会变成0了。由于add的时候先转换了一次-0导致最后变成0。

补充

前面手写Set里面定义了sameValueZero方法,其实es官方自己有一个sameValueZero方法

sameValueZero方法

# Number::sameValueZero方法

完善自定义Set

其实只需要自定义一个CanonicalizeKeyedCollectionKey方法

自定义 CanonicalizeKeyedCollectionKey方法

ini 复制代码
const CanonicalizeKeyedCollectionKey = (value) => value === -0 ? 0 : value

修改add方法

kotlin 复制代码
 add(value) {
        const newValue = CanonicalizeKeyedCollectionKey(value)
        if (!this.has(newValue)) {
            this.items.push(newValue);
        }
        return this;
    }

测试

javascript 复制代码
console.log(...new xSet([null, null, undefined, undefined, NaN, NaN, -0, 0, +0])); // null undefined NaN 0

完整的xSet

kotlin 复制代码
const sameValueZero = (x, y) => {
    return x === y || (typeof x === 'number' && typeof y === 'number' && isNaN(x) && isNaN(y));
}
const CanonicalizeKeyedCollectionKey = (value) => value === -0 ? 0 : value

export class xSet {
    constructor(iterable = []) {
        this.items = [];
        if (iterable[Symbol.iterator]) {
            for (let item of iterable) {
                this.add(item);
            }
        }
    }

    add(value) {
        const newValue = CanonicalizeKeyedCollectionKey(value)
        if (!this.has(newValue)) {
            this.items.push(newValue);
        }
        return this;
    }

    delete(value) {
        const index = this.items.findIndex(item => item === value);
        if (index !== -1) {
            this.items.splice(index, 1);
            return true;
        }
        return false;
    }

    has(value) {
        return this.items.some(item => sameValueZero(item , value));
    }

    clear() {
        this.items = [];
    }

   size() {
        return this.items.length;
    }

    [Symbol.iterator]() {
        let index = 0;
        const items = this.items;
        return {
            next() {
                if (index < items.length) {
                    return { value: items[index++], done: false };
                } else {
                    return { done: true };
                }
            }
        };
    }

    keys() {
        return this[Symbol.iterator]();
    }

    values() {
        return this[Symbol.iterator]();
    }

    entries() {
        let index = 0;
        const items = this.items;
        return {
            next() {
                if (index < items.length) {
                    return { value: [items[index], items[index++]], done: false };
                } else {
                    return { done: true };
                }
            }
        };
    }

    forEach(callbackFn, thisArg) {
        for (let item of this.items) {
            callbackFn.call(thisArg, item, item, this);
        }
    }
}
相关推荐
古迪红尘6 小时前
el-tree 采用懒加载方式,怎么初始化就显示根节点和下级节点
前端·javascript·vue.js
Aotman_6 小时前
Vue el-table 字段自定义排序(进阶)
前端·javascript·vue.js·elementui·前端框架·ecmascript
西维6 小时前
大屏、看板必备的丝滑技巧 — 数字滚动
前端·javascript·动效
前端达人6 小时前
2026年React数据获取的第六层:从自己写缓存到用React Query——减少100行代码的秘诀
前端·javascript·react.js·缓存·前端框架
2501_948122636 小时前
React Native for OpenHarmony 实战:Steam 资讯 App 通知设置实现
javascript·react native·react.js·游戏·ecmascript·harmonyos
小酒星小杜6 小时前
在AI时代,技术人应该每天都要花两小时来构建一个反内耗构建系统 - Ship 篇
前端·javascript·vue.js
—Qeyser6 小时前
Flutter 生命周期完全指南:从出生到死亡的全过程
前端·javascript·flutter
2501_948122636 小时前
React Native for OpenHarmony 实战:Steam 资讯 App 帮助中心实现
javascript·react native·react.js·游戏·ecmascript·harmonyos
念念不忘 必有回响6 小时前
Vue页面布局与路由映射实战:RouterView嵌套及动态组件生成详解
前端·javascript·vue.js