JavaScript 数组合并与去重(解析 [...value, ...ids] 技巧)

在 JavaScript 开发中,我们经常需要合并两个数组并去除重复项。一个常见的解决方案是使用扩展运算符Set 数据结构:

const uniqueArray = Array.from(new Set([...value, ...ids]));

这行简洁的代码实现了数组合并与去重的功能。本文将深入解析这一技巧,补充相关基础知识,并详细讨论其优缺点。

基础知识补充

1. 数组基础

JavaScript 数组是一种特殊的对象,用于存储有序的数据集合。与普通对象不同,数组的键是数字索引,从 0 开始。

const fruits = ['apple', 'banana', 'orange'];
// fruits[0] => 'apple'
// fruits.length => 3
2. Set 数据结构

Set 是 ES6 引入的一种新的数据结构,它类似于数组,但成员的值都是唯一的,没有重复的值。

const set = new Set([1, 2, 2, 3, 4, 4]);
console.log(set); // Set {1, 2, 3, 4}

Set 的主要特点:

  • 自动去重
  • 可以快速判断元素是否存在 (has 方法)
  • 可以获取元素数量 (size 属性)

拓展阅读:JavaScript 的 Set 是什么?一篇让你彻底搞懂的入门手册

3. Array.from 方法

Array.from() 方法从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。

// 从类数组对象创建数组
const arrayLike = {0: 'a', 1: 'b', length: 2};
const arr1 = Array.from(arrayLike); // ['a', 'b']

// 从可迭代对象创建数组
const set = new Set(['a', 'b', 'c']);
const arr2 = Array.from(set); // ['a', 'b', 'c']

代码逐步解析

让我们详细分解这行代码:

const uniqueArray = Array.from(new Set([...value, ...ids]));
第一步:扩展运算符合并数组 […value, …ids]

扩展运算符 (...) 允许一个可迭代对象(如数组或字符串)在需要多个元素的地方展开。

const value = [1, 2];
const ids = [2, 3];
const merged = [...value, ...ids]; // [1, 2, 2, 3]

优点​:

  • 语法简洁,比 concat 方法更直观
  • 可以轻松合并多个数组
  • 可以与其他解构操作结合使用

缺点​:

  • 创建了一个新的中间数组,可能会对性能有轻微影响(对于非常大的数组)
第二步:创建 Set 去重 new Set([…value, …ids])

将合并后的数组传递给 Set 构造函数,Set 会自动去除重复项。

const merged = [1, 2, 2, 3];
const uniqueSet = new Set(merged); // Set {1, 2, 3}

优点​:

  • 自动去重,无需手动实现
  • 去重操作非常高效
  • 代码简洁

​缺点​:

  • Set 不保留元素的原始顺序(但实际上会保留首次出现的顺序)
  • Set 不是数组,不能直接使用数组方法
第三步:转换回数组 Array.from(new Set([…value, …ids]))

使用 Array.from() 将 Set 转换回数组。

const uniqueSet = new Set([1, 2, 3]);
const uniqueArray = Array.from(uniqueSet); // [1, 2, 3]

优点​:

  • 得到的是标准的数组,可以使用所有数组方法
  • 代码清晰表达意图

缺点​:

  • 需要额外的转换步骤

替代方案比较

1. 使用 filter 和 indexOf (传统方法)
const uniqueArray = [...value, ...ids].filter((item, index, array) => array.indexOf(item) === index);

缺点​:

  • 性能较差,特别是对于大数组,因为 indexOf 在每次迭代中都要遍历数组
  • 代码较冗长
2. 使用 reduce 方法
const uniqueArray = [...value, ...ids].reduce((acc, current) => {
  if (!acc.includes(current)) {
    acc.push(current);
  }
  return acc;
}, []);

缺点​:

  • 代码较复杂
  • includes 方法在每次迭代中都要检查数组,性能不如 Set
3. 使用 lodash 的 uniq 方法
const _ = require('lodash');
const uniqueArray = _.uniq([...value, ...ids]);

优点​:

  • 代码简洁
  • 经过充分测试,可靠性高

​缺点​:

  • 需要引入额外的库
  • 对于简单任务可能过度设计

性能考虑

对于小到中等大小的数组,[...value, ...ids] 方法性能很好。但对于非常大的数组(数万或更多元素),可能需要考虑:

  • 如果顺序不重要,可以先将数组转换为 Set 再转换回来,可能更快
  • 如果内存是瓶颈,可以考虑流式处理或分批处理

实际应用场景

这种数组合并去重技巧在以下场景中特别有用:

  • 合并用户ID列表​:从不同来源获取的用户ID合并并去重
  • ​标签系统​:合并多个标签数组并确保没有重复标签
  • 配置合并​:合并默认配置和用户自定义配置
  • ​数据聚合​:从多个API响应中合并数据并去重

注意事项

  1. 类型一致性​:确保合并的数组元素类型一致,否则可能无法正确去重
const a = [1, '1']; // 数字1和字符串'1'会被视为不同
const unique = Array.from(new Set([...a])); // [1, '1']
  1. 对象引用​:Set 使用严格相等(===)比较,所以对象即使内容相同也会被视为不同
const obj1 = {id: 1};
const obj2 = {id: 1};
const arr = [obj1, obj2];
const unique = Array.from(new Set(arr)); // [obj1, obj2]
  1. 非数组输入​:如果 value 或 ids 不是数组,代码会抛出错误
const value = 'not an array';
const ids = [1, 2];
const unique = Array.from(new Set([...value, ...ids])); // TypeError

结论

[...value, ...ids] 这种数组合并去重技巧是现代 JavaScript 开发中的一个强大工具。它结合了扩展运算符的简洁性、Set 的高效去重能力和 Array.from 的灵活转换,提供了一种优雅的解决方案。

优点总结​:

  • 代码简洁易读
  • 性能良好
  • 保留首次出现的顺序
  • 纯函数式,不修改原数组

缺点总结​:

  • 创建中间数组可能对极大数组有性能影响
  • 不适用于非数组输入(需要额外处理)
  • 对象比较基于引用而非内容

在实际开发中,根据具体场景选择最适合的方法。对于大多数情况,这种扩展运算符加 Set 的方法已经足够好,也是当前 JavaScript 社区中的推荐做法。


推荐更多阅读内容
如何让 Linux 主机“隐身”:禁用 Ping 响应
深入理解 CSS 高度塌陷问题及解决方案
深入理解 JavaScript的空值合并运算符(提升代码精确性)
深入理解 JavaScript 的可选链操作符(提升代码健壮性和可维护性)
解决Antd Form组件初次渲染时禁用交互逻辑失效的问题(说明初始值的重要性)
:is() 伪类选择器(使代码更简洁)

import time from flask import Flask, request, render_template, jsonify from typing import List, Dict, Optional from SPARQLWrapper import SPARQLWrapper, JSON import json import copy import os import logging from dotenv import load_dotenv # Neo4j 驱动 from neo4j import GraphDatabase # 加载 .env 文件中的环境变量 load_dotenv() # 创建Flask应用实例 app = Flask(__name__) # 配置日志 logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # 缓存结构:uri -> { status: bool, timestamp: int } endpoint_health_cache = {} CACHE_TTL = 300 # 5 分钟缓存时间 # Neo4j 配置 NEO4J_URI = os.getenv("NEO4J_URI") NEO4J_USER = os.getenv("NEO4J_USER") NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD") driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USER, NEO4J_PASSWORD)) # 定义Relation类,表示知识图谱路径中的一个步骤 class Relation: def __init__(self, start: str, end: str, relation: str, sparql_uri: str): self.start = start self.end = end self.relation = relation self.sparql_uri = sparql_uri def to_dict(self): return { "start": self.start, "end": self.end, "relation": self.relation, "sparql_uri": self.sparql_uri } def __str__(self): return f"Start: {self.start}\nEnd: {self.end}\nRelation: {self.relation}\nSPARQL Endpoint: {self.sparql_uri}" # 定义PathInfo类,封装知识图谱路径的所有信息 class PathInfo: def __init__(self, start_iri: str, end_iri: str, path: List['Relation'], end_class_iri: Optional[str] = None): self.start_iri = start_iri self.end_iri = end_iri self.path = copy.deepcopy(path) self.end_class_iri = end_class_iri def __str__(self): path_str = ', '.join(str(rel) for rel in self.path) return f"Start: {self.start_iri}\nEnd: {self.end_iri}\nPath: {path_str}\nEnd Class IRI: {self.end_class_iri}" def __repr__(self): return f"<PathInfo(start_iri='{self.start_iri}', end_iri='{self.end_iri}', path_length={len(self.path)}, end_class_iri='{self.end_class_iri}')>" # SPARQL 查询服务 class SPARQLQueryService: @staticmethod def get_entity_info(entity_name: str, endpoints: List[str]) -> List[Dict]: """ 从 SPARQL 端点查询实体信息(如 rdf:type, rdfs:label) """ results = [] for endpoint in endpoints: sparql = SPARQLWrapper(endpoint) sparql.setQuery(f""" SELECT DISTINCT ?s ?type WHERE {{ ?s a ?type . ?s rdfs:label ?label . VALUES ?label {{ "{entity_name}" "{entity_name}"@zh "{entity_name}"@en "{entity_name}"@la "{entity_name}"@ja }} }} """ ) sparql.setReturnFormat(JSON) try: response = sparql.query().convert() if isinstance(response, bytes): logger.info(f"SPARQL 查询结果({endpoint}): {response.decode('utf-8')}") else: logger.info(f"SPARQL 查询结果({endpoint}): {json.dumps(response, indent=2)}") for result in response["results"]["bindings"]: results.append({ "entityIri": result["s"]["value"], "type": result["type"]["value"] }) except Exception as e: logger.error(f"SPARQL 查询失败({endpoint}): {e}") return results class Neo4jService: def __init__(self, uri: str, username: str, password: str): self._driver = GraphDatabase.driver(uri, auth=(username, password)) def close(self): self._driver.close() def find_by_start_and_end(self, start_node_name: str, end_node_name: str, depth: int, sparql_uri_list: List[str]) -> List[List[Dict]]: if not sparql_uri_list: raise ValueError(f"端点数组不能为空: {json.dumps(sparql_uri_list)}") result = [] with self._driver.session() as session: query = f""" MATCH p=(s:Point)-[r*1..{depth}]-(e:Point) WHERE s.name = '{start_node_name}' AND e.name = '{end_node_name}' AND ALL(node IN nodes(p)[1..-1] WHERE node <> s AND node <> e) AND ALL(rel IN relationships(p) WHERE rel.sparqlURI IN {sparql_uri_list}) RETURN nodes(p) AS nodes, relationships(p) AS relations """ print(query) query_result = session.run(query) for record in query_result: node_list = record["nodes"] rel_list = record["relations"] path = [] for i, rel in enumerate(rel_list): start_node = node_list[i] end_node = node_list[i + 1] # 确定关系方向 if rel.start_node.element_id == start_node.element_id: s = start_node["name"] o = end_node["name"] else: s = end_node["name"] o = start_node["name"] path.append({ "start": s, "end": o, "relation": rel["name"], "sparql_uri": rel["sparqlURI"] }) result.append(path) return result # SPARQL 生成器 class SparqlGenerator: @staticmethod def generate_from_paths(real_paths: List[List[Dict]]) -> Dict: if not real_paths: return {"sparql": None, "graph_data": None} template = """CONSTRUCT { $construct } WHERE { $where } LIMIT 20""" construct = [] where = [] variables = {} node_ids = {} graph_data = {"nodes": [], "links": []} var_counter = 1 for path_index, path in enumerate(real_paths): for step_index, step in enumerate(path): s = step["start"] o = step["end"] p = step["relation"] endpoint = step.get("sparql_uri", "http://default/sparql") # 注册变量和节点 for uri in [s, o]: if uri not in variables: variables[uri] = f"?s{var_counter}" var_counter += 1 if uri not in node_ids: node_id = len(node_ids) node_ids[uri] = node_id graph_data["nodes"].append({ "id": node_id, "name": uri.split("/")[-1], "uri": uri, "isClass": False }) # 构造 triple triple = f"{variables[s]} <{p}> {variables[o]} ." construct.append(triple) where.append(f"SERVICE SILENT <{endpoint}> {{\n{triple}\n}}") # 构造 links 据 graph_data["links"].append({ "source": node_ids[s], "target": node_ids[o], "relation": p, "sparqlEndpoint": endpoint }) # 处理 construct_str = "\n".join(set(construct)) where_str = "\n".join(set(where)) return { "sparql": template.replace("$construct", construct_str).replace("$where", where_str), "graph_data": graph_data } def check_sparql_endpoint(uri): """ 检查 SPARQL 端点是否可用(带缓存) """ current_time = time.time() # 如果缓存存在且未过期,直接返回缓存结果 if uri in endpoint_health_cache: cached = endpoint_health_cache[uri] if current_time - cached['timestamp'] < CACHE_TTL: return cached['status'] # 使用轻量 SPARQL 查询 sparql_query = """ SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 1 """ sparql = SPARQLWrapper(uri) sparql.setQuery(sparql_query) sparql.setReturnFormat(JSON) sparql.setTimeout(5) # 设置超时时间 flag = True try: results = sparql.query().convert() if results["results"]["bindings"]: logger.info(f"Endpoint {uri} is accessible.") flag = True else: logger.warning(f"Endpoint {uri} returned no results.") flag = False except Exception as e: flag = False if uri in endpoint_health_cache and endpoint_health_cache[uri]['status']: logger.error(f"端点 {uri} 不可用: {e}") # 统一更新缓存 endpoint_health_cache[uri] = { 'status': flag, 'timestamp': current_time } return flag # 根路由 @app.route('/') def index(): return render_template('index.html') @app.route('/health-check', methods=['POST']) def health_check(): data = request.get_json() endpoints = data.get('endpoints', []) if not endpoints or not isinstance(endpoints, list): return jsonify({"error": "缺少有效的端点列表"}), 400 # 查询每个端点的健康状态(使用缓存) status = {} for uri in endpoints: status[uri] = check_sparql_endpoint(uri) return jsonify({"status": status}) # 处理路径生成查询 @app.route('/generate', methods=['POST']) def generate(): try: data = request.json # Step 1: 提取参 start_name = data['startName'] end_name = data['endName'] depth = data.get('depth', 3) sparql_endpoints = data.get('sparqlEndpoints', []) if not sparql_endpoints: return jsonify({"success": False, "error": "必须指定至少一个 SPARQL 端点"}) # Step 2: 查询 SPARQL 获取实体信息(模拟) start_entity_info = SPARQLQueryService.get_entity_info(start_name, sparql_endpoints) end_entity_info = SPARQLQueryService.get_entity_info(end_name, sparql_endpoints) if not start_entity_info: return jsonify({"success": False, "error": f"无法获取实体{start_name}信息"}) if not end_entity_info: return jsonify({"success": False, "error": f"无法获取实体{end_name}信息"}) start_iri = start_entity_info[0]["entityIri"] end_iri = end_entity_info[0]["entityIri"] start_type = start_entity_info[0]["type"] end_type = end_entity_info[0]["type"] # Step 3: 查询 Neo4j 获取真实路径 neo4j_service = Neo4jService(NEO4J_URI, NEO4J_USER, NEO4J_PASSWORD) real_paths = neo4j_service.find_by_start_and_end(start_type, end_type, depth, sparql_endpoints) neo4j_service.close() if not real_paths: return jsonify({"success": True, "graph_data": {"nodes": [], "links": []}, "sparql": ""}) # Step 4: 根据真实路径生成 SPARQL 查询和 graph_data result = SparqlGenerator.generate_from_paths(real_paths) # Step 5: 添加 start_iri 和 end_iri 到返回结果 result["startIri"] = start_iri result["endIri"] = end_iri return jsonify({"success": True, **result}) except Exception as e: return jsonify({"success": False, "error": str(e)}) if __name__ == '__main__': app.run(debug=True) 代码返回的图是一个由uri构成的图,最终想要的效果是根据uri查询到节点的具体信息,图中显示节点的名称例如地不容等,鼠标悬浮节点的时候显示所有信息
最新发布
07-24
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

漠月瑾

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值