最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

JS手撕代碼之高頻對(duì)象與數(shù)組工具方法解析

 更新時(shí)間:2026年04月13日 09:56:23   作者:We?ct  
在JavaScript中對(duì)象是一種數(shù)據(jù)結(jié)構(gòu),用于將數(shù)據(jù)和功能組織在一起,描述一類(lèi)對(duì)象所具有的屬性和方法,這篇文章主要介紹了JS手撕代碼之高頻對(duì)象與數(shù)組工具方法的相關(guān)資料,需要的朋友可以參考下

前言

在日常 JavaScript 開(kāi)發(fā)中,對(duì)象和數(shù)組是最常用的數(shù)據(jù)結(jié)構(gòu),無(wú)論是接口返回?cái)?shù)據(jù)的格式化、復(fù)雜數(shù)據(jù)的對(duì)比,還是請(qǐng)求性能優(yōu)化、數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)換,都需要熟練掌握一系列高頻操作技巧。本文將整合對(duì)象與數(shù)組的核心操作,結(jié)合通俗解讀+專業(yè)代碼解析,詳細(xì)講解各類(lèi)實(shí)用技巧,每個(gè)技巧都附帶完整代碼、核心原理和實(shí)際應(yīng)用場(chǎng)景,新手能看懂,老手能復(fù)用,同時(shí)兼顧面試手撕需求與實(shí)際開(kāi)發(fā)適配。

一、類(lèi)數(shù)組轉(zhuǎn)數(shù)組:從“偽數(shù)組”到真數(shù)組

首先要明確:什么是類(lèi)數(shù)組?類(lèi)數(shù)組(Array-like)是 具有l(wèi)ength屬性、按索引存儲(chǔ)數(shù)據(jù),但不具備數(shù)組原生方法(如push、slice) 的對(duì)象,比如 document.querySelectorAll('div') 返回的NodeList、函數(shù)的arguments對(duì)象、自定義的{0: ‘a’, 1: ‘b’, length: 2}。

我們的目標(biāo)是將這類(lèi)“偽數(shù)組”轉(zhuǎn)換成真正的數(shù)組,從而使用數(shù)組的所有原生方法,以下是3種常用實(shí)現(xiàn)方式,各有適配場(chǎng)景:

1. 推薦方案:Array.from()(最靈活)

const arrLike = document.querySelectorAll('div') // NodeList(類(lèi)數(shù)組)
const realArr = Array.from(arrLike) // 轉(zhuǎn)換為真正的數(shù)組

通俗解讀:Array.from就像一個(gè)“轉(zhuǎn)換器”,專門(mén)接收類(lèi)數(shù)組或可迭代對(duì)象(如Set、Map),直接輸出數(shù)組。

專業(yè)解析:Array.from有兩個(gè)核心特點(diǎn):① 支持第二個(gè)參數(shù)(映射函數(shù)),可在轉(zhuǎn)換時(shí)同步處理數(shù)據(jù),比如 Array.from(arrLike, item => item.textContent);② 兼容性良好(ES6+),能處理所有類(lèi)數(shù)組和可迭代對(duì)象,是日常開(kāi)發(fā)的首選。

2. 簡(jiǎn)潔方案:擴(kuò)展運(yùn)算符 …(最簡(jiǎn)潔)

const arrLike = document.querySelectorAll('div')
const realArr = [...arrLike]

通俗解讀:擴(kuò)展運(yùn)算符就像“把類(lèi)數(shù)組的每一項(xiàng)都拆出來(lái),再重新裝進(jìn)一個(gè)新數(shù)組里”,寫(xiě)法非常簡(jiǎn)潔。

專業(yè)解析:擴(kuò)展運(yùn)算符的本質(zhì)是遍歷可迭代對(duì)象(類(lèi)數(shù)組需滿足可迭代條件,如NodeList、arguments均滿足),將遍歷結(jié)果組裝成數(shù)組。優(yōu)點(diǎn)是代碼簡(jiǎn)潔,缺點(diǎn)是無(wú)法在轉(zhuǎn)換時(shí)同步處理數(shù)據(jù),需額外寫(xiě)map方法。

3. 兼容方案:Array.prototype.slice.call()(兼容舊環(huán)境)

// 示例1:轉(zhuǎn)換arguments(函數(shù)內(nèi)部的類(lèi)數(shù)組)
function fn() {
  const realArr = Array.prototype.slice.call(arguments)
  console.log(realArr) // 數(shù)組形式的參數(shù)列表
}
fn(1, 2, 3)
// 示例2:轉(zhuǎn)換NodeList
const arrLike = document.querySelectorAll('div')
const realArr = Array.prototype.slice.call(arrLike)

通俗解讀:slice方法原本是用來(lái)“截取數(shù)組”的,當(dāng)用call改變它的this指向?yàn)轭?lèi)數(shù)組時(shí),它會(huì)把類(lèi)數(shù)組當(dāng)作數(shù)組來(lái)處理,返回一個(gè)新的真正數(shù)組。

專業(yè)解析:slice方法的底層邏輯是“遍歷this指向的對(duì)象,從start到end截取元素,組裝成新數(shù)組”,只要對(duì)象有l(wèi)ength屬性和索引,就能被slice處理。該方案兼容ES5及以下環(huán)境,是舊項(xiàng)目的兼容首選,但寫(xiě)法比前兩種繁瑣。

避坑點(diǎn)

  • 類(lèi)數(shù)組必須有l(wèi)ength屬性,否則轉(zhuǎn)換后會(huì)是空數(shù)組(如{0: ‘a’, 1: ‘b’} 轉(zhuǎn)換后為空,需補(bǔ)充length: 2);

  • 擴(kuò)展運(yùn)算符和Array.from() 不兼容IE11及以下,需兼容舊環(huán)境優(yōu)先用slice.call()。

二、實(shí)現(xiàn)數(shù)組負(fù)索引:像Python一樣用arr[-1]取最后一項(xiàng)

JS原生數(shù)組不支持負(fù)索引(如arr[-1]無(wú)法獲取最后一項(xiàng),只會(huì)返回undefined),但我們可以通過(guò)Proxy代理,實(shí)現(xiàn)“負(fù)索引轉(zhuǎn)正索引”的效果,讓數(shù)組支持負(fù)索引訪問(wèn)。

完整實(shí)現(xiàn)代碼

// 接收一個(gè)數(shù)組,返回一個(gè)代理后的新數(shù)組
const proxyArray = arr => {
  // 保存【數(shù)組原始長(zhǎng)度】(重點(diǎn):固定不變!避免數(shù)組長(zhǎng)度變化導(dǎo)致負(fù)索引計(jì)算錯(cuò)誤)
  const originLength = arr.length;
  // 返回 Proxy 代理對(duì)象,攔截?cái)?shù)組的屬性讀取操作
  return new Proxy(arr, {
    // 攔截所有【讀取屬性/索引】操作(如arr[0]、arr[-1]、arr.length)
    get(target, key) {
      // 判斷:訪問(wèn)的 key 是不是【數(shù)字索引】(1、'2'、-3 都算)
      const isNumberKey = 
        (typeof key === 'number') ||  // 數(shù)字類(lèi)型 key:arr[1]、arr[-1]
        (typeof key === 'string' && !isNaN(Number(key)));  // 字符串?dāng)?shù)字:arr['2']、arr['-3']
      // 如果是數(shù)字索引 → 處理負(fù)索引
      if (isNumberKey) {
        let index = Number(key); // 統(tǒng)一轉(zhuǎn)成數(shù)字,避免字符串索引的問(wèn)題
        // 核心邏輯:負(fù)索引轉(zhuǎn)正索引(循環(huán)處理,支持多重負(fù)索引如-2、-3)
        while (index < 0) {
          index += originLength;
        }
        // 返回處理后的索引對(duì)應(yīng)的值(超出范圍會(huì)返回undefined,和原生數(shù)組一致)
        return target[index];
      }
      // 不是索引(比如 length、push、pop 等數(shù)組方法),正常返回,不影響原生功能
      return target[key];
    }
  });
};

使用示例

const arr = [10, 20, 30, 40];
const proxyArr = proxyArray(arr);
console.log(proxyArr[-1]); // 40(最后一項(xiàng))
console.log(proxyArr[-2]); // 30(倒數(shù)第二項(xiàng))
console.log(proxyArr[0]);  // 10(正常索引)
console.log(proxyArr.length); // 4(原生屬性正常訪問(wèn))
proxyArr.push(50); // 正常執(zhí)行,數(shù)組變成[10,20,30,40,50]
console.log(proxyArr[-1]); // 50(注意:originLength是原始長(zhǎng)度4,這里為什么是50?)

關(guān)鍵解析(重點(diǎn)避坑)

1. 為什么要固定originLength?

通俗說(shuō):如果不固定原始長(zhǎng)度,當(dāng)數(shù)組新增/刪除元素(如push、pop)后,length會(huì)變化,此時(shí)負(fù)索引計(jì)算會(huì)出錯(cuò)。比如上面的示例,push(50)后數(shù)組長(zhǎng)度變成5,但originLength還是4,proxyArr[-1]會(huì)計(jì)算為 -1 + 4 = 3,對(duì)應(yīng)的值是40?但實(shí)際運(yùn)行結(jié)果是50,這是因?yàn)閠arget是原始數(shù)組,target[index]會(huì)讀取當(dāng)前數(shù)組的第3項(xiàng)(40),但示例中輸出50,說(shuō)明哪里錯(cuò)了?

糾正:示例中push(50)后,target(原始數(shù)組)的長(zhǎng)度變成5,但originLength還是4,此時(shí)proxyArr[-1] = -1 + 4 = 3,target[3]是40,而非50。這就是“固定originLength”的特點(diǎn)——負(fù)索引基于數(shù)組初始長(zhǎng)度計(jì)算,不隨數(shù)組長(zhǎng)度變化而變化。如果想讓負(fù)索引隨數(shù)組長(zhǎng)度動(dòng)態(tài)變化,可將originLength改為“實(shí)時(shí)獲取target.length”,但會(huì)失去“固定索引”的意義,按需選擇即可。

2. Proxy的作用

Proxy是ES6新增的代理對(duì)象,能攔截對(duì)象的屬性讀取、設(shè)置等操作。這里我們只攔截了get(讀?。┎僮鳎挥绊憯?shù)組的其他原生方法(如push、pop、splice),保證數(shù)組的正常使用。

3. 兼容問(wèn)題

Proxy兼容ES6+,不支持IE瀏覽器,若需兼容舊環(huán)境,可通過(guò)重寫(xiě)數(shù)組的getter方法實(shí)現(xiàn)(復(fù)雜度更高,不推薦)。

三、數(shù)組扁平化:將嵌套數(shù)組“拍平”成一維數(shù)組

數(shù)組扁平化是指將多層嵌套的數(shù)組(如[1, [2, [3]]])轉(zhuǎn)換為一維數(shù)組(如[1,2,3]),日常開(kāi)發(fā)中處理后端返回的嵌套數(shù)據(jù)時(shí)非常常用。以下是5種實(shí)現(xiàn)方式,涵蓋“原生方法、暴力方案、面試手撕方案”,各有優(yōu)劣。

測(cè)試用例(含邊界值)

// 包含空值、null、undefined、對(duì)象嵌套、空字符串,覆蓋常見(jiàn)邊界場(chǎng)景
const arr = [1, , [2, [3, [{ a: [4] }, 5, ["", null, undefined]]]], 6];

1. ES6原生方法:flat(Infinity)(推薦,日常開(kāi)發(fā)首選)

const flat1 = arr.flat(Infinity);
console.log(flat1); // [1, 2, 3, {a: [4]}, 5, "", null, undefined, 6]

通俗解讀:flat是ES6新增的數(shù)組原生方法,參數(shù)是“扁平化的層級(jí)”,Infinity表示“無(wú)限層級(jí)”,不管數(shù)組嵌套多少層,都能拍平成一維。

專業(yè)解析

  • flat會(huì)自動(dòng)過(guò)濾數(shù)組中的空項(xiàng)(如測(cè)試用例中的空值“,”,會(huì)被過(guò)濾);

  • 參數(shù)為0時(shí),不進(jìn)行扁平化,返回原數(shù)組;參數(shù)為1時(shí),只拍平一層(如[1,[2,[3]]] → [1,2,[3]]);

  • 兼容性:ES6+,不支持IE,日常開(kāi)發(fā)(如Vue、React項(xiàng)目)可直接使用,簡(jiǎn)潔高效。

2. 暴力方案:toString() + split()(僅適用于純數(shù)字?jǐn)?shù)組)

const flat2 = arr.toString().split(',').map(Number);
console.log(flat2); // [1, NaN, 2, 3, NaN, 5, 0, NaN, NaN, 6]

通俗解讀:先把數(shù)組轉(zhuǎn)換成字符串(會(huì)自動(dòng)去掉嵌套結(jié)構(gòu),用逗號(hào)分隔),再按逗號(hào)分割成數(shù)組,最后轉(zhuǎn)成數(shù)字。

專業(yè)解析

  • 優(yōu)點(diǎn):寫(xiě)法簡(jiǎn)單,無(wú)需遞歸;

  • 致命缺點(diǎn):① 僅適用于純數(shù)字?jǐn)?shù)組,非數(shù)字類(lèi)型(對(duì)象、字符串、null)會(huì)被轉(zhuǎn)成NaN或異常值;② 會(huì)過(guò)濾空項(xiàng),但會(huì)改變非數(shù)字?jǐn)?shù)據(jù)的類(lèi)型,實(shí)際開(kāi)發(fā)中很少用,僅用于簡(jiǎn)單的純數(shù)字場(chǎng)景。

3. 面試首選:forEach遞歸(易理解,手寫(xiě)無(wú)壓力)

// 接收數(shù)組和扁平化層級(jí)(默認(rèn)無(wú)限層級(jí))
function eachFlat(arr, depth = Infinity) {
  const res = []; // 存儲(chǔ)最終的扁平化數(shù)組
  arr.forEach(item => {
    // 判斷:當(dāng)前項(xiàng)是數(shù)組,且層級(jí)未耗盡 → 遞歸扁平化,層級(jí)-1
    if (Array.isArray(item) && depth > 0) {
      res.push(...eachFlat(item, depth - 1)); // 展開(kāi)遞歸結(jié)果,存入res
    } else {
      // 非數(shù)組項(xiàng),直接存入res(保留原類(lèi)型,不過(guò)濾空項(xiàng))
      res.push(item);
    }
  });
  return res;
}
const flat3 = eachFlat(arr);
console.log(flat3); // [1, undefined, 2, 3, {a: [4]}, 5, "", null, undefined, 6]

通俗解讀:遍歷數(shù)組的每一項(xiàng),如果當(dāng)前項(xiàng)是數(shù)組,就遞歸處理這個(gè)子數(shù)組,把處理后的結(jié)果合并到最終數(shù)組里;如果不是數(shù)組,直接放進(jìn)最終數(shù)組。

專業(yè)解析

  • 優(yōu)點(diǎn):邏輯清晰,易手寫(xiě),支持指定扁平化層級(jí),保留原數(shù)據(jù)類(lèi)型,不過(guò)濾空項(xiàng)(符合原生flat的默認(rèn)行為,空項(xiàng)會(huì)轉(zhuǎn)為undefined);

  • 缺點(diǎn):遞歸層級(jí)過(guò)深時(shí)(如嵌套1000層),會(huì)出現(xiàn)棧溢出(stack overflow),但實(shí)際開(kāi)發(fā)中嵌套層級(jí)不會(huì)這么深,面試中手寫(xiě)此方案即可。

4. 優(yōu)雅方案:reduce遞歸(代碼簡(jiǎn)潔,面試加分)

function reduceFlat(arr, depth = Infinity) {
  // reduce接收兩個(gè)參數(shù):回調(diào)函數(shù)和初始值(空數(shù)組)
  return arr.reduce((acc, val) => 
    // 回調(diào)邏輯:如果當(dāng)前項(xiàng)是數(shù)組且層級(jí)未耗盡,遞歸處理后合并;否則直接加入acc
    acc.concat(Array.isArray(val) && depth > 0 ? reduceFlat(val, depth - 1) : val), []);
}
const flat4 = reduceFlat(arr);
console.log(flat4); // 和forEach遞歸結(jié)果一致

通俗解讀:利用reduce的“累加”特性,將數(shù)組的每一項(xiàng)累加(合并)到初始空數(shù)組中,遇到子數(shù)組就遞歸處理后再合并。

專業(yè)解析

  • 優(yōu)點(diǎn):代碼比f(wàn)orEach遞歸更簡(jiǎn)潔,邏輯和forEach一致,支持指定層級(jí),面試中手寫(xiě)此方案會(huì)更顯優(yōu)雅;

  • 缺點(diǎn):同樣存在遞歸棧溢出問(wèn)題,適用于常規(guī)嵌套層級(jí)。

5. 非遞歸方案:棧實(shí)現(xiàn)(解決遞歸棧溢出,面試進(jìn)階)

function stackFlat(arr) {
  const res = []; // 最終結(jié)果數(shù)組
  const stack = [...arr]; // 用棧存儲(chǔ)待處理的元素,初始存入原數(shù)組的所有項(xiàng)
  // 循環(huán):棧不為空就繼續(xù)處理
  while (stack.length) {
    const item = stack.pop(); // 從棧尾取出一個(gè)元素(后進(jìn)先出)
    // 如果是數(shù)組,就把它的所有項(xiàng)壓入棧(展開(kāi)后壓入,相當(dāng)于扁平化一層)
    Array.isArray(item) ? stack.push(...item) : res.unshift(item); // 非數(shù)組項(xiàng),加入res的開(kāi)頭
  }
  return res;
}
const flat5 = stackFlat(arr);
console.log(flat5); // 和遞歸方案結(jié)果一致

通俗解讀:用棧來(lái)“存放”需要處理的元素,每次從棧尾取出一個(gè)元素,如果是數(shù)組,就把它拆解開(kāi),所有項(xiàng)再壓入棧;如果不是數(shù)組,就放進(jìn)最終結(jié)果數(shù)組,直到棧為空。

專業(yè)解析

  • 優(yōu)點(diǎn):非遞歸實(shí)現(xiàn),避免棧溢出問(wèn)題,支持任意嵌套層級(jí);

  • 關(guān)鍵邏輯:stack.pop()(從尾取)+ stack.push(…item)(從尾壓入),保證子數(shù)組的元素能被優(yōu)先處理;res.unshift(item)(從開(kāi)頭插入),保證最終數(shù)組的順序和原數(shù)組一致。

總結(jié)(選型建議)

  • 日常開(kāi)發(fā):優(yōu)先用 flat(Infinity),簡(jiǎn)潔高效;

  • 面試手寫(xiě):基礎(chǔ)版用forEach遞歸,進(jìn)階版用棧實(shí)現(xiàn)(體現(xiàn)對(duì)棧數(shù)據(jù)結(jié)構(gòu)的理解);

  • 純數(shù)字場(chǎng)景:可臨時(shí)用toString()方案(不推薦長(zhǎng)期使用)。

四、嵌套數(shù)組的深度:計(jì)算數(shù)組嵌套的最大層級(jí)

有時(shí)我們需要知道一個(gè)嵌套數(shù)組的最大嵌套深度(如[1, [2, [3]]]的深度是3),用于數(shù)據(jù)校驗(yàn)或渲染邏輯,以下是遞歸實(shí)現(xiàn)方案(面試高頻)。

完整實(shí)現(xiàn)代碼

const recursiveMax = (arr) => {
  // 邊界條件:如果傳入的不是數(shù)組,深度為0(遞歸終止條件)
  if (!Array.isArray(arr)) {
    return 0;
  }
  // 初始值:當(dāng)前數(shù)組本身的層級(jí)是1(即使是空數(shù)組,深度也是1)
  let maxDepth = 1;
  // 遍歷數(shù)組的每一項(xiàng),遞歸計(jì)算子數(shù)組的深度
  for (let i = 0; i < arr.length; i++) {
    // 如果當(dāng)前項(xiàng)是數(shù)組,遞歸計(jì)算它的深度,再加上當(dāng)前層級(jí)1(因?yàn)樗钱?dāng)前數(shù)組的子項(xiàng))
    if (Array.isArray(arr[i])) {
      let depth = 1 + recursiveMax(arr[i]);
      // 保留最大的深度(對(duì)比當(dāng)前maxDepth和子數(shù)組的深度,取較大值)
      maxDepth = Math.max(maxDepth, depth);
    }
  }
  return maxDepth;
}

測(cè)試用例

console.log(recursiveMax([1, [2, [3]]])); // 3(最外層1層,中間2層,最里3層)
console.log(recursiveMax([1, 2, 3])); // 1(無(wú)嵌套)
console.log(recursiveMax([[[]], [1, [2]]])); // 3(最里層的空數(shù)組是3層)
console.log(recursiveMax(123)); // 0(不是數(shù)組)

關(guān)鍵解析

  • 遞歸終止條件:當(dāng)傳入的參數(shù)不是數(shù)組時(shí),返回0,停止遞歸;

  • 初始值maxDepth=1:因?yàn)榧词故强諗?shù)組([]),它本身也是1層,不能從0開(kāi)始;

  • 核心邏輯:遍歷每一項(xiàng),若為子數(shù)組,遞歸計(jì)算其深度并+1(當(dāng)前層級(jí)),再用Math.max保留最大深度。

五、數(shù)組去重:去除數(shù)組中的重復(fù)元素

數(shù)組去重是前端高頻需求(如處理用戶選擇的標(biāo)簽、后端返回的重復(fù)數(shù)據(jù)),以下是基礎(chǔ)手寫(xiě)方案(面試必寫(xiě)),同時(shí)補(bǔ)充原生優(yōu)化方案。

基礎(chǔ)手寫(xiě)方案:indexOf判斷(易理解,面試首選)

function unique(arr) {
  const result = []; // 存儲(chǔ)去重后的數(shù)組
  for (let i = 0; i < arr.length; i++) {
    // indexOf返回-1,表示當(dāng)前元素不在result中(未重復(fù))
    if (result.indexOf(arr[i]) === -1) {
      result.push(arr[i]); // 未重復(fù)則加入result
    }
  }
  return result;
}

測(cè)試用例

console.log(unique([1, 2, 2, 3, 3, 3])); // [1,2,3]
console.log(unique([1, '1', 2, 2])); // [1, '1', 2](區(qū)分?jǐn)?shù)字和字符串)
console.log(unique([null, null, undefined, undefined])); // [null, undefined]

進(jìn)階優(yōu)化(補(bǔ)充)

基礎(chǔ)方案的時(shí)間復(fù)雜度是O(n²)(forEach嵌套indexOf),數(shù)據(jù)量較大時(shí)效率較低,以下是兩種優(yōu)化方案(面試可補(bǔ)充,加分):

// 方案1:Set去重(ES6+,最簡(jiǎn)潔,時(shí)間復(fù)雜度O(n))
function uniqueSet(arr) {
  return [...new Set(arr)]; // Set自動(dòng)去重,再轉(zhuǎn)成數(shù)組
}
// 方案2:對(duì)象鍵值對(duì)去重(兼容ES5,時(shí)間復(fù)雜度O(n))
function uniqueObj(arr) {
  const obj = {};
  const result = [];
  arr.forEach(item => {
    if (!obj[item]) { // 利用對(duì)象鍵名唯一的特性
      obj[item] = true;
      result.push(item);
    }
  });
  return result;
}

避坑點(diǎn)

  • 基礎(chǔ)方案(indexOf)和Set方案,都會(huì)保留數(shù)組的原始順序;

  • 對(duì)象鍵值對(duì)方案,會(huì)將數(shù)字1和字符串’1’判定為重復(fù)(因?yàn)閛bj[1]和obj[‘1’]是同一個(gè)鍵),需根據(jù)需求選擇;

  • Set方案無(wú)法去重NaN(因?yàn)镹aN === NaN為false,但Set會(huì)將多個(gè)NaN視為重復(fù)),需特殊處理。

六、數(shù)組轉(zhuǎn)樹(shù):扁平數(shù)組轉(zhuǎn)樹(shù)形結(jié)構(gòu)(高頻業(yè)務(wù)需求)

后端返回的數(shù)據(jù)通常是扁平數(shù)組(如用戶列表、菜單列表,包含id和parentId),而前端渲染時(shí)需要樹(shù)形結(jié)構(gòu)(如菜單導(dǎo)航、樹(shù)形表格),因此“數(shù)組轉(zhuǎn)樹(shù)”是高頻業(yè)務(wù)需求,以下是高效實(shí)現(xiàn)方案。

完整實(shí)現(xiàn)代碼(帶注釋)

/**
 * 扁平數(shù)組 轉(zhuǎn) 樹(shù)形結(jié)構(gòu)
 * @param {Array} arr - 扁平數(shù)組(每一項(xiàng)必須包含id和parentId)
 * @param {number} rootValue - 根節(jié)點(diǎn)的 parentId 值,默認(rèn) 0(通常根節(jié)點(diǎn)parentId為0或null)
 * @returns {Array} 樹(shù)形結(jié)構(gòu)數(shù)組(根節(jié)點(diǎn)數(shù)組,每個(gè)節(jié)點(diǎn)包含children屬性)
 */
function arrayToTree(arr, rootValue = 0) {
  // 1. 創(chuàng)建 ID 映射表,快速查找節(jié)點(diǎn)(核心優(yōu)化:避免嵌套循環(huán),時(shí)間復(fù)雜度從O(n2)降至O(n))
  const map = {};
  const tree = []; // 最終的樹(shù)形結(jié)構(gòu)數(shù)組
  // 第一步:先把所有節(jié)點(diǎn)存入map,并初始化children屬性(避免后續(xù)判斷children是否存在)
  arr.forEach(item => {
    // 深拷貝當(dāng)前節(jié)點(diǎn)(避免修改原始數(shù)組),并初始化children為空數(shù)組
    map[item.id] = { ...item, children: [] };
  });
  // 第二步:遍歷扁平數(shù)組,構(gòu)建父子關(guān)系
  arr.forEach(item => {
    const currentNode = map[item.id]; // 當(dāng)前節(jié)點(diǎn)(從map中快速獲取)
    const parentNode = map[item.parentId]; // 父節(jié)點(diǎn)(從map中快速獲?。?
    // 判斷:如果當(dāng)前節(jié)點(diǎn)是根節(jié)點(diǎn)(parentId === rootValue),直接加入tree
    if (item.parentId === rootValue) {
      tree.push(currentNode);
    } 
    // 否則:如果父節(jié)點(diǎn)存在,將當(dāng)前節(jié)點(diǎn)加入父節(jié)點(diǎn)的children
    else if (parentNode) {
      parentNode.children.push(currentNode);
    }
  });
  return tree;
}

測(cè)試用例(模擬后端返回的扁平數(shù)組)

const arr = [
  { id: 1, name: '節(jié)點(diǎn)1', parentId: 0 }, // 根節(jié)點(diǎn)(parentId=0)
  { id: 2, name: '節(jié)點(diǎn)2', parentId: 0 }, // 根節(jié)點(diǎn)(parentId=0)
  { id: 3, name: '節(jié)點(diǎn)1-1', parentId: 1 }, // 子節(jié)點(diǎn)(父節(jié)點(diǎn)id=1)
  { id: 4, name: '節(jié)點(diǎn)1-2', parentId: 1 }, // 子節(jié)點(diǎn)(父節(jié)點(diǎn)id=1)
  { id: 5, name: '節(jié)點(diǎn)1-1-1', parentId: 3 }, // 孫節(jié)點(diǎn)(父節(jié)點(diǎn)id=3)
];
const tree = arrayToTree(arr);
console.log(JSON.stringify(tree, null, 2)); // 格式化輸出,便于查看樹(shù)形結(jié)構(gòu)

輸出結(jié)果(樹(shù)形結(jié)構(gòu))

[
  {
    "id": 1,
    "name": "節(jié)點(diǎn)1",
    "parentId": 0,
    "children": [
      {
        "id": 3,
        "name": "節(jié)點(diǎn)1-1",
        "parentId": 1,
        "children": [
          {
            "id": 5,
            "name": "節(jié)點(diǎn)1-1-1",
            "parentId": 3,
            "children": []
          }
        ]
      },
      {
        "id": 4,
        "name": "節(jié)點(diǎn)1-2",
        "parentId": 1,
        "children": []
      }
    ]
  },
  {
    "id": 2,
    "name": "節(jié)點(diǎn)2",
    "parentId": 0,
    "children": []
  }
]

核心優(yōu)化點(diǎn)(面試重點(diǎn))

  • 用map映射表優(yōu)化效率:傳統(tǒng)方案會(huì)嵌套循環(huán)(遍歷每個(gè)節(jié)點(diǎn),再遍歷所有節(jié)點(diǎn)找父節(jié)點(diǎn)),時(shí)間復(fù)雜度O(n²);用map存儲(chǔ)每個(gè)節(jié)點(diǎn)的id和節(jié)點(diǎn)本身,可通過(guò)id快速獲取父節(jié)點(diǎn),時(shí)間復(fù)雜度降至O(n),適合大數(shù)據(jù)量場(chǎng)景。

  • 初始化children:提前給每個(gè)節(jié)點(diǎn)添加children空數(shù)組,避免后續(xù)判斷parentNode.children是否存在,簡(jiǎn)化邏輯。

  • 深拷貝節(jié)點(diǎn):用{ …item }深拷貝當(dāng)前節(jié)點(diǎn),避免修改原始扁平數(shù)組,符合“函數(shù)純性”(輸入不變,輸出不變)。

避坑點(diǎn)

  • 扁平數(shù)組的每一項(xiàng)必須包含id和parentId,否則無(wú)法構(gòu)建父子關(guān)系;

  • 根節(jié)點(diǎn)的parentId需和傳入的rootValue一致(默認(rèn)0,若后端根節(jié)點(diǎn)parentId為null,需傳入rootValue=null);

  • 若存在“孤立節(jié)點(diǎn)”(parentId不存在于map中),會(huì)被忽略(不加入樹(shù)形結(jié)構(gòu)),可根據(jù)需求添加異常處理。

七、深拷貝與淺拷貝:避免“修改副本影響原對(duì)象”

在JS中,對(duì)象和數(shù)組是引用類(lèi)型,賦值時(shí)傳遞的是“引用地址”,而非值本身。這就導(dǎo)致了“修改副本,原對(duì)象也會(huì)被修改”的問(wèn)題,因此需要通過(guò)淺拷貝或深拷貝來(lái)解決。

核心區(qū)別:淺拷貝只復(fù)制一層,深層引用類(lèi)型仍共享地址;深拷貝復(fù)制所有層級(jí),深層引用類(lèi)型也會(huì)被重新創(chuàng)建,副本和原對(duì)象完全獨(dú)立。

一、淺拷貝(復(fù)制一層,適用于單層對(duì)象/數(shù)組)

淺拷貝的核心是“只復(fù)制頂層屬性”,如果頂層屬性是引用類(lèi)型(如對(duì)象、數(shù)組),則復(fù)制的是引用地址,修改副本的深層屬性會(huì)影響原對(duì)象。

1. 手寫(xiě)通用淺拷貝(修復(fù)所有錯(cuò)誤,面試必寫(xiě))

const shallowCopy = function(obj) {
  // 邊界條件:如果不是對(duì)象(如數(shù)字、字符串、null),直接返回(值類(lèi)型無(wú)需拷貝)
  if (typeof obj !== "object" || obj === null) return obj;
  // 初始化新對(duì)象/數(shù)組:如果是數(shù)組,新建數(shù)組;否則新建對(duì)象
  let newObj = obj instanceof Array ? [] : {};
  // 遍歷原對(duì)象的自有屬性(不遍歷原型鏈上的屬性)
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 只復(fù)制頂層屬性,深層屬性仍為引用
      newObj[key] = obj[key];
    }
  }
  return newObj;
};

2. 官方原生方法(日常開(kāi)發(fā)首選)

// (1)對(duì)象淺拷貝
const obj = { a: 1, b: { c: 2 } };
const copy1 = Object.assign({}, obj); // Object.assign:合并對(duì)象,實(shí)現(xiàn)淺拷貝
const copy2 = { ...obj }; // 擴(kuò)展運(yùn)算符:最簡(jiǎn)潔的對(duì)象淺拷貝方式
// (2)數(shù)組淺拷貝
const arr = [1, 2, [3, 4]];
const copy3 = arr.slice(); // slice:截取整個(gè)數(shù)組,返回新數(shù)組(淺拷貝)
const copy4 = [].concat(arr); // concat:合并數(shù)組,返回新數(shù)組(淺拷貝)

淺拷貝測(cè)試(驗(yàn)證深層引用)

const obj = { a: 1, b: { c: 2 } };
const copy = { ...obj };
copy.a = 100; // 修改頂層屬性,原對(duì)象不受影響
console.log(obj.a); // 1(原對(duì)象不變)
copy.b.c = 200; // 修改深層屬性,原對(duì)象受影響
console.log(obj.b.c); // 200(原對(duì)象被修改)

二、深拷貝(復(fù)制所有層級(jí),適用于嵌套對(duì)象/數(shù)組)

深拷貝會(huì)遞歸復(fù)制對(duì)象的所有層級(jí),不管嵌套多少層,副本和原對(duì)象都是完全獨(dú)立的,修改副本不會(huì)影響原對(duì)象。以下是3種實(shí)現(xiàn)方案,從簡(jiǎn)單到完善。

1. 最簡(jiǎn)單方案:JSON.parse(JSON.stringify())(日常開(kāi)發(fā)首選,有缺陷)

function deepCopyJSON(obj) {
  return JSON.parse(JSON.stringify(obj));
}

通俗解讀:先把對(duì)象轉(zhuǎn)換成JSON字符串(會(huì)忽略函數(shù)、undefined等類(lèi)型),再把字符串轉(zhuǎn)回對(duì)象,相當(dāng)于“重新創(chuàng)建”了一個(gè)對(duì)象,實(shí)現(xiàn)深拷貝。

缺陷(重點(diǎn)避坑)

  • 會(huì)丟失函數(shù)、undefined、Symbol類(lèi)型;

  • 無(wú)法處理Date類(lèi)型(會(huì)被轉(zhuǎn)成字符串,再轉(zhuǎn)回后變成字符串,而非Date對(duì)象);

  • 無(wú)法處理RegExp類(lèi)型(正則表達(dá)式會(huì)被轉(zhuǎn)成空對(duì)象);

  • 無(wú)法處理循環(huán)引用(如obj.self = obj,會(huì)報(bào)錯(cuò));

  • 適用于“無(wú)特殊類(lèi)型、無(wú)循環(huán)引用”的簡(jiǎn)單嵌套對(duì)象/數(shù)組(如后端返回的JSON數(shù)據(jù))。

2. 基礎(chǔ)遞歸深拷貝(支持對(duì)象/數(shù)組/函數(shù),無(wú)循環(huán)引用處理)

function deepCopy(obj) {
  // 邊界條件:非對(duì)象(值類(lèi)型)直接返回
  if (typeof obj !== 'object' || obj === null) {
    return obj;
  }
  // 初始化新對(duì)象/數(shù)組
  const result = Array.isArray(obj) ? [] : {};
  // 遍歷自有屬性,遞歸拷貝每一項(xiàng)
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 遞歸拷貝深層屬性
      result[key] = deepCopy(obj[key]);
    }
  }
  return result;
}

優(yōu)點(diǎn):支持函數(shù)、Date、RegExp類(lèi)型(不會(huì)丟失),解決了JSON深拷貝的部分缺陷;

缺點(diǎn):無(wú)法處理循環(huán)引用(如obj.self = obj,會(huì)導(dǎo)致遞歸棧溢出)。

3. 完整版深拷貝(支持循環(huán)引用+Date+RegExp+Map+Set,面試進(jìn)階)

function deepClone(obj, map = new WeakMap()) {
  // 邊界條件:非對(duì)象直接返回
  if (obj === null || typeof obj !== 'object') return obj;
  // 處理循環(huán)引用:如果map中已有當(dāng)前對(duì)象,直接返回緩存的對(duì)象(避免遞歸死循環(huán))
  if (map.has(obj)) return map.get(obj);
  let result;
  // 特殊類(lèi)型處理:Date
  if (obj instanceof Date) result = new Date(obj);
  // 特殊類(lèi)型處理:RegExp(保留正則的source和flags)
  else if (obj instanceof RegExp) result = new RegExp(obj.source, obj.flags);
  // 特殊類(lèi)型處理:Map
  else if (obj instanceof Map) {
    result = new Map();
    map.set(obj, result); // 緩存當(dāng)前對(duì)象,避免循環(huán)引用
    obj.forEach((val, key) => result.set(key, deepClone(val, map))); // 遞歸拷貝Map的每一項(xiàng)
  }
  // 特殊類(lèi)型處理:Set
  else if (obj instanceof Set) {
    result = new Set();
    map.set(obj, result); // 緩存當(dāng)前對(duì)象
    obj.forEach(val => result.add(deepClone(val, map))); // 遞歸拷貝Set的每一項(xiàng)
  }
  // 普通對(duì)象/數(shù)組
  else {
    result = Array.isArray(obj) ? [] : {};
    map.set(obj, result); // 緩存當(dāng)前對(duì)象
    // 遍歷自有屬性,遞歸拷貝
    for (const key in obj) {
      if (obj.hasOwnProperty(key)) {
        result[key] = deepClone(obj[key], map);
      }
    }
  }
  return result;
}

測(cè)試用例(覆蓋所有特殊場(chǎng)景)

const testObj = {
  a: 1,
  b: { c: 2 },
  d: [3, 4],
  func: () => console.log(1), // 函數(shù)
  date: new Date(), // Date類(lèi)型
  reg: /test/g, // RegExp類(lèi)型
  map: new Map([[1, 'a']]), // Map類(lèi)型
  set: new Set([1, 2, 3]) // Set類(lèi)型
};
testObj.self = testObj; // 循環(huán)引用(自身引用自身)
const copy1 = deepCopyJSON(testObj);   // 有丟失(函數(shù)、map、set丟失,date轉(zhuǎn)成字符串)
const copy2 = deepCopy(testObj);      // 基礎(chǔ)版(循環(huán)引用報(bào)錯(cuò))
const copy3 = deepClone(testObj);     // 完美版(所有類(lèi)型都保留,循環(huán)引用正常)
console.log(copy3.func); // () => console.log(1)(函數(shù)保留)
console.log(copy3.date instanceof Date); // true(Date類(lèi)型保留)
console.log(copy3.reg.test('test')); // true(正則正常使用)
console.log(copy3.self === copy3); // true(循環(huán)引用正常)

總結(jié)(選型建議)

  • 日常開(kāi)發(fā)(無(wú)特殊類(lèi)型、無(wú)循環(huán)引用):優(yōu)先用 JSON.parse(JSON.stringify()),簡(jiǎn)潔高效;

  • 有特殊類(lèi)型(函數(shù)、Date、Map、Set):用基礎(chǔ)遞歸深拷貝;

  • 有循環(huán)引用+特殊類(lèi)型(面試/復(fù)雜場(chǎng)景):用完整版深拷貝(deepClone)。

八、對(duì)象扁平化與反扁平化:把嵌套對(duì)象“壓平”或“還原”

在處理后端返回?cái)?shù)據(jù)時(shí),我們經(jīng)常會(huì)遇到多層嵌套的對(duì)象(比如 {a:{b:{c:1}}}),這種結(jié)構(gòu)在某些場(chǎng)景下(如表格渲染、表單提交)使用不便,此時(shí)就需要“扁平化”;反之,當(dāng)我們拿到扁平化的對(duì)象,需要還原成嵌套結(jié)構(gòu)時(shí),就需要“反扁平化”。

1. 對(duì)象扁平化:嵌套結(jié)構(gòu) → 單層結(jié)構(gòu)

核心邏輯:通過(guò)遞歸遍歷嵌套對(duì)象,將每個(gè)層級(jí)的 key 用“.”拼接,最終生成單層對(duì)象,value 保持不變。

// 對(duì)象扁平化:{a:{b:{c:1}}} → { "a.b.c": 1 }
const objectFlatten = (obj) => {
  const res = {}; // 存儲(chǔ)最終扁平化結(jié)果
  // 遞歸扁平化函數(shù):item是當(dāng)前遍歷的對(duì)象,preKey是父級(jí)key(初始為空)
  function flat(item, preKey = "") {
    // 遍歷對(duì)象的所有[key, value]對(duì)(Object.entries能獲取自身可枚舉屬性)
    Object.entries(item).forEach(([key, val]) => {
      // 拼接新key:如果有父級(jí)key,就用“父級(jí).key”,否則直接用當(dāng)前key
      const newKey = preKey ? `${preKey}.${key}` : key;
      // 關(guān)鍵判斷:如果當(dāng)前value是對(duì)象(且不是數(shù)組),繼續(xù)遞歸;否則直接賦值
      // 注意:這里排除數(shù)組,是因?yàn)閿?shù)組扁平化有專門(mén)的方法,避免混淆
      if (val && typeof val === 'object' && !Array.isArray(val)) {
        flat(val, newKey); // 遞歸,把當(dāng)前newKey作為父級(jí)key傳入
      } else {
        res[newKey] = val; // 非對(duì)象/數(shù)組,直接存入結(jié)果
      }
    });
  }
  flat(obj); // 啟動(dòng)遞歸
  return res;
};

關(guān)鍵細(xì)節(jié)(通俗解讀):

  • 遞歸的作用:像“剝洋蔥”一樣,一層一層剝開(kāi)嵌套對(duì)象,直到拿到最底層的 value。

  • key 拼接:比如嵌套對(duì)象 {a:{b:2, c:{d:3}}},遍歷到 d 時(shí),preKey 是“a.c”,newKey 就是“a.c.d”,最終存入 {'a.c.d':3}。

  • 排除數(shù)組:如果不排除數(shù)組,數(shù)組會(huì)被當(dāng)作對(duì)象遍歷(數(shù)組的 key 是索引),比如 {a:[1,2]} 會(huì)變成 {'a.0':1, 'a.1':2},不符合預(yù)期。

2. 反扁平化:?jiǎn)螌狱c(diǎn)分隔 key → 嵌套對(duì)象

核心邏輯:將扁平化對(duì)象的 key 按“.”分割成數(shù)組,通過(guò) reduce 方法逐層構(gòu)建嵌套結(jié)構(gòu),最終還原成原始嵌套對(duì)象。

// 反扁平化(點(diǎn)分隔 key → 嵌套對(duì)象)
const unFlatten = (obj) => {
  const res = {}; // 最終還原的嵌套對(duì)象
  // 遍歷扁平化對(duì)象的所有[key, value]對(duì)
  for (const [key, value] of Object.entries(obj)) {
    const keys = key.split('.'); // 把key按“.”分割成數(shù)組,比如“a.b.c”→["a","b","c"]
    // reduce遍歷keys數(shù)組,逐層構(gòu)建嵌套對(duì)象
    // acc:累加器(當(dāng)前層級(jí)的對(duì)象),k:當(dāng)前key,index:當(dāng)前索引
    keys.reduce((acc, k, index) => {
      // 最后一個(gè)key:直接賦值value(最底層值)
      if (index === keys.length - 1) {
        acc[k] = value;
      } else {
        // 非最后一個(gè)key:如果當(dāng)前層級(jí)沒(méi)有這個(gè)key,就創(chuàng)建空對(duì)象,避免報(bào)錯(cuò)
        acc[k] = acc[k] || {};
      }
      // 返回當(dāng)前層級(jí)的對(duì)象,供下一次reduce使用
      return acc[k];
    }, res); // 初始累加器是res(最外層空對(duì)象)
  }
  return res;
};

測(cè)試案例(直觀驗(yàn)證):

const origin = { a: { b: { c: 1 }, d: 2 } };
const flat = objectFlatten(origin);
const nested = unFlatten(flat);
console.log('扁平化:', flat);       // { 'a.b.c': 1, 'a.d': 2 } ?
console.log('還原后:', nested);     // { a: { b: { c: 1 }, d: 2 } } ?

實(shí)際應(yīng)用場(chǎng)景:

  • 扁平化:后端返回的嵌套數(shù)據(jù),需要在表格中展示(表格列對(duì)應(yīng)單層 key)。

  • 反扁平化:前端存儲(chǔ)的扁平化配置,需要還原成嵌套結(jié)構(gòu)供組件使用。

九、循環(huán)引用判斷:避免代碼“卡死”的關(guān)鍵技巧

循環(huán)引用是指對(duì)象 A 包含對(duì)象 B,對(duì)象 B 又包含對(duì)象 A(比如 obj1.someProp = obj2; obj2.someProp = obj1)。如果直接對(duì)這種對(duì)象進(jìn)行遞歸操作(如扁平化、深拷貝),會(huì)導(dǎo)致無(wú)限遞歸,最終觸發(fā)棧溢出,代碼卡死。因此,判斷對(duì)象是否存在循環(huán)引用,是前端開(kāi)發(fā)中必須掌握的防御性技巧。

核心實(shí)現(xiàn)(標(biāo)準(zhǔn)面試版):

// 檢測(cè)對(duì)象是否存在循環(huán)引用(標(biāo)準(zhǔn)面試版)
function isCycleObject(obj) {
  // 關(guān)鍵:用WeakSet存儲(chǔ)已遍歷過(guò)的對(duì)象,避免內(nèi)存泄漏
  // WeakSet的特點(diǎn):存儲(chǔ)的是對(duì)象的弱引用,不會(huì)阻止垃圾回收,適合這種場(chǎng)景
  const seenObjects = new WeakSet();
  // 遞歸檢測(cè)函數(shù)
  function detect(target) {
    // 只檢查對(duì)象/數(shù)組(基礎(chǔ)類(lèi)型:數(shù)字、字符串等,不會(huì)有循環(huán)引用)
    if (target && typeof target === 'object') {
      // 如果當(dāng)前對(duì)象已經(jīng)在seenObjects中 → 存在循環(huán)引用
      if (seenObjects.has(target)) {
        return true;
      }
      // 標(biāo)記當(dāng)前對(duì)象為已訪問(wèn),存入WeakSet
      seenObjects.add(target);
      // 遍歷當(dāng)前對(duì)象的所有自身屬性,遞歸檢測(cè)
      for (const key in target) {
        // 只遍歷自身屬性,避免遍歷原型鏈上的屬性(優(yōu)化性能)
        if (Object.prototype.hasOwnProperty.call(target, key)) {
          // 遞歸檢測(cè)當(dāng)前屬性的值,如果有循環(huán)引用,直接返回true
          if (detect(target[key])) {
            return true;
          }
        }
      }
    }
    // 基礎(chǔ)類(lèi)型、null,或者沒(méi)有循環(huán)引用,返回false
    return false;
  }
  return detect(obj);
}

關(guān)鍵細(xì)節(jié)(通俗解讀):

  • 為什么用 WeakSet?如果用普通 Set,存儲(chǔ)的是對(duì)象的強(qiáng)引用,即使對(duì)象已經(jīng)不需要了,也無(wú)法被垃圾回收,會(huì)導(dǎo)致內(nèi)存泄漏;WeakSet 是弱引用,不影響垃圾回收,更安全。

  • 遞歸終止條件:要么遍歷到基礎(chǔ)類(lèi)型(直接返回 false),要么遇到已經(jīng)遍歷過(guò)的對(duì)象(返回 true,說(shuō)明有循環(huán))。

  • hasOwnProperty 判斷:避免遍歷原型鏈上的屬性(比如 Object.prototype.toString),減少無(wú)效遍歷,提升性能。

測(cè)試案例:

const obj1 = {};
const obj2 = {};
obj1.someProp = obj2;
obj2.someProp = obj1; // 循環(huán)引用
const normalObj = { a: 1, b: { c: 2 } }; // 無(wú)循環(huán)
console.log(isCycleObject(obj1));    // true ?(有循環(huán)引用)
console.log(isCycleObject(normalObj)); // false ?(無(wú)循環(huán)引用)

實(shí)際應(yīng)用場(chǎng)景:

在進(jìn)行深拷貝、對(duì)象扁平化、JSON 序列化之前,先判斷是否存在循環(huán)引用,避免代碼報(bào)錯(cuò)或卡死(比如 JSON.stringify 遇到循環(huán)引用會(huì)直接報(bào)錯(cuò))。

十、對(duì)象深合并:實(shí)現(xiàn) lodash.merge 核心功能

我們知道,Object.assign 是淺合并(只合并第一層屬性,嵌套對(duì)象會(huì)直接覆蓋),而實(shí)際開(kāi)發(fā)中,我們經(jīng)常需要“深合并”——即嵌套對(duì)象也會(huì)逐層合并,不會(huì)直接覆蓋。比如 {a: [{b:2}]}{a: [{c:3}]},深合并后應(yīng)該是 {a: [{b:2, c:3}]},這就是 lodash.merge 的核心功能,我們手動(dòng)實(shí)現(xiàn)它。

核心實(shí)現(xiàn):

// 判斷是否為對(duì)象(排除 null 和基礎(chǔ)類(lèi)型)
function isObject(value) {
  return typeof value === 'object' && value !== null;
}
// 判斷是否為數(shù)組
function isArray(value) {
  return Object.prototype.toString.call(value) === '[object Array]';
}
/**
 * 深度合并對(duì)象 / 數(shù)組
 * @param {any} target - 目標(biāo)數(shù)據(jù)(被覆蓋的對(duì)象)
 * @param {any} source - 源數(shù)據(jù)(覆蓋目標(biāo)的對(duì)象)
 * @returns {any} 合并后的新數(shù)據(jù)
 */
function merge(target, source) {
  // 邊界處理:如果target或source不是對(duì)象,直接返回source(source優(yōu)先)
  // 比如 target是1,source是2 → 返回2;source是undefined,返回target
  if (!isObject(target) || !isObject(source)) {
    return source !== undefined ? source : target;
  }
  // 初始化結(jié)果:如果source是數(shù)組,結(jié)果就是數(shù)組;否則是對(duì)象(和source類(lèi)型一致)
  const result = isArray(source) ? [] : {};
  // 第一步:復(fù)制target的內(nèi)容到result(保留target的原有屬性)
  if (isArray(result)) {
    // 結(jié)果是數(shù)組 → 復(fù)制target的數(shù)組元素(如果target也是數(shù)組)
    if (isArray(target)) {
      target.forEach((item, i) => {
        result[i] = item;
      });
    }
  } else {
    // 結(jié)果是對(duì)象 → 復(fù)制target的自身屬性
    for (const key in target) {
      if (target.hasOwnProperty(key)) {
        result[key] = target[key];
      }
    }
  }
  // 第二步:合并source的內(nèi)容到result(覆蓋target的同名屬性,嵌套對(duì)象遞歸合并)
  if (isArray(source)) {
    // source是數(shù)組 → 遍歷數(shù)組元素,遞歸合并
    source.forEach((item, i) => {
      result[i] = merge(result[i], item);
    });
  } else {
    // source是對(duì)象 → 遍歷自身屬性,遞歸合并
    for (const key in source) {
      if (source.hasOwnProperty(key)) {
        result[key] = merge(result[key], source[key]);
      }
    }
  }
  return result;
}

關(guān)鍵細(xì)節(jié)(通俗解讀):

  • 淺合并 vs 深合并:淺合并只處理第一層,深合并會(huì)遞歸處理所有嵌套層級(jí);比如 target 是 {a: {b:2}},source 是 {a: {c:3}},淺合并會(huì)得到 {a: {c:3}}(覆蓋),深合并會(huì)得到 {a: {b:2, c:3}}(合并)。

  • 類(lèi)型一致性:source 是數(shù)組,結(jié)果就是數(shù)組;source 是對(duì)象,結(jié)果就是對(duì)象,避免出現(xiàn)“數(shù)組被轉(zhuǎn)成對(duì)象”的異常。

  • source 優(yōu)先:當(dāng) target 和 source 類(lèi)型不一致(比如 target 是對(duì)象,source 是數(shù)組),直接返回 source,符合 lodash.merge 的邏輯。

測(cè)試案例:

const object = { a: [{ b: 2 }, { d: 4 }] };
const other = { a: [{ c: 3 }, { e: 5 }] };
console.log(merge(object, other));
// 輸出:{ a: [ { b: 2, c: 3 }, { d: 4, e: 5 } ] } ?(嵌套數(shù)組合并成功)

十一、深比較:判斷兩個(gè)對(duì)象是否“完全相等”

我們知道,JavaScript 中 === 對(duì)對(duì)象的比較是“引用比較”——只有兩個(gè)對(duì)象指向同一個(gè)內(nèi)存地址,才會(huì)返回 true;即使兩個(gè)對(duì)象的結(jié)構(gòu)和值完全一樣,只要是不同的內(nèi)存地址,就會(huì)返回 false。深比較的作用,就是忽略引用,只比較對(duì)象的結(jié)構(gòu)和值,判斷兩個(gè)對(duì)象是否“完全相等”。

核心實(shí)現(xiàn)(覆蓋所有場(chǎng)景):

const deepEqual = (a, b) => {
  // 1. 引用完全相同 → 直接相等(比如 a和b指向同一個(gè)對(duì)象)
  if (a === b) return true;
  // 2. 類(lèi)型不同 → 不相等(比如 a是對(duì)象,b是數(shù)組;a是數(shù)字,b是字符串)
  if (typeof a !== typeof b) return false;
  // 3. 處理 null(null的typeof是object,單獨(dú)判斷)
  if (a === null || b === null) return a === b;
  // 4. 專門(mén)處理:兩個(gè)都是 NaN 的情況(NaN === NaN 是 false,需要單獨(dú)判斷)
  const type = typeof a;
  if (type !== 'object' && type !== 'function') {
    // 基礎(chǔ)類(lèi)型(數(shù)字、字符串、布爾值),處理 NaN 特殊情況
    return Number.isNaN(a) && Number.isNaN(b);
  }
  // 5. 日期 / 正則 特殊判斷(它們的結(jié)構(gòu)相同,值也相同,才算是相等)
  const tagA = Object.prototype.toString.call(a);
  const tagB = Object.prototype.toString.call(b);
  if (tagA !== tagB) return false; // 標(biāo)簽不同,直接不相等(比如 Date 和 RegExp)
  if (tagA === '[object Date]') {
    // 日期:判斷時(shí)間戳是否相同(getTime()返回毫秒數(shù))
    return a.getTime() === b.getTime();
  }
  if (tagA === '[object RegExp]') {
    // 正則:判斷正則表達(dá)式的源文本(source)和標(biāo)志(flags)是否相同
    return a.source === b.source && a.flags === b.flags;
  }
  // 6. 獲取所有鍵(包含 Symbol 類(lèi)型的鍵,避免遺漏)
  const keysA = Reflect.ownKeys(a);
  const keysB = Reflect.ownKeys(b);
  if (keysA.length !== keysB.length) return false; // 鍵的數(shù)量不同,直接不相等
  // 7. 遞歸比較每個(gè)鍵的值(確保所有層級(jí)的結(jié)構(gòu)和值都相同)
  for (const key of keysA) {
    if (!deepEqual(a[key], b[key])) {
      return false;
    }
  }
  // 所有條件都滿足 → 完全相等
  return true;
};

關(guān)鍵細(xì)節(jié)(通俗解讀):

  • 覆蓋特殊類(lèi)型:處理了 null、NaN、Date、RegExp 這些特殊情況,避免出現(xiàn)“明明值相同,卻判斷為不相等”的問(wèn)題(比如 new Date(2024)new Date(2024),用 === 判斷是 false,深比較是 true)。

  • Symbol 鍵支持:用 Reflect.ownKeys 獲取所有鍵(包括 Symbol 類(lèi)型),而不是 Object.keys(只獲取可枚舉的字符串鍵),避免遺漏 Symbol 鍵導(dǎo)致判斷錯(cuò)誤。

  • 遞歸終止條件:要么引用相同,要么類(lèi)型不同、鍵數(shù)量不同,要么某個(gè)鍵的值不相等,否則繼續(xù)遞歸比較嵌套層級(jí)。

測(cè)試案例:

const objA = { a: 1, b: { c: 2 }, d: new Date(2024), e: /abc/g };
const objB = { a: 1, b: { c: 2 }, d: new Date(2024), e: /abc/g };
const objC = { a: 1, b: { c: 3 } };
console.log(deepEqual(objA, objB)); // true ?(結(jié)構(gòu)和值完全相同)
console.log(deepEqual(objA, objC)); // false ?(b.c的值不同)
console.log(deepEqual(NaN, NaN)); // true ?(特殊處理NaN)

十二、重寫(xiě) Object.assign:掌握淺拷貝核心邏輯

Object.assign 是 JavaScript 內(nèi)置的淺拷貝方法,作用是將一個(gè)或多個(gè)源對(duì)象的屬性復(fù)制到目標(biāo)對(duì)象,并返回目標(biāo)對(duì)象。它的核心特點(diǎn)是“淺拷貝”“源對(duì)象優(yōu)先”“跳過(guò) null/undefined”,我們手動(dòng)實(shí)現(xiàn)它,就能徹底理解其底層邏輯。

核心實(shí)現(xiàn):

// 手寫(xiě)實(shí)現(xiàn) Object.assign(淺拷貝、合并對(duì)象)
Object.assign2 = function(target, ...sources) {
  // 1. 邊界處理:target 是 null / undefined → 直接報(bào)錯(cuò)(和原生Object.assign一致)
  if (target == null) {
    throw new TypeError('Cannot convert undefined or null to object');
  }
  // 2. 把 target 包裝成對(duì)象(處理 target 是數(shù)字、字符串、布爾值的情況)
  // 比如 target是1 → 包裝成 Number {1};target是"abc" → 包裝成 String {"abc"}
  const ret = Object(target);
  // 3. 遍歷所有源對(duì)象(sources是剩余參數(shù),可能有多個(gè)源對(duì)象)
  sources.forEach(obj => {
    if (obj != null) { // 跳過(guò) null/undefined(原生Object.assign也會(huì)跳過(guò))
      // 4. 遍歷源對(duì)象自身可枚舉屬性(不遍歷原型鏈)
      for (const key in obj) {
        if (obj.hasOwnProperty(key)) {
          ret[key] = obj[key]; // 覆蓋賦值(源對(duì)象屬性覆蓋目標(biāo)對(duì)象同名屬性)
        }
      }
    }
  });
  return ret;
};

關(guān)鍵細(xì)節(jié)(通俗解讀):

  • target 包裝成對(duì)象:原生 Object.assign 會(huì)把 target 轉(zhuǎn)為對(duì)象,如果 target 是基礎(chǔ)類(lèi)型(比如 1、“abc”),會(huì)創(chuàng)建對(duì)應(yīng)的包裝對(duì)象,賦值后返回包裝對(duì)象(比如 Object.assign(1, {a:2}) 返回 Number {1, a:2})。

  • 跳過(guò) null/undefined:如果源對(duì)象是 null 或 undefined,不會(huì)報(bào)錯(cuò),直接跳過(guò)(比如 Object.assign({a:1}, null, {b:2}),只會(huì)合并 {b:2})。

  • 淺拷貝本質(zhì):只復(fù)制源對(duì)象的第一層屬性,如果屬性值是對(duì)象,復(fù)制的是對(duì)象的引用(比如源對(duì)象的屬性是 {b:2},目標(biāo)對(duì)象的屬性會(huì)指向同一個(gè) {b:2})。

測(cè)試案例:

const target = { a: 1, b: { c: 2 } };
const source1 = { b: { d: 3 }, e: 4 };
const source2 = null;
const result = Object.assign2(target, source1, source2);
console.log(result); // { a: 1, b: { d: 3 }, e: 4 } ?
console.log(result.b === source1.b); // true ?(淺拷貝,引用相同)

十三、駝峰和下劃線互轉(zhuǎn):前后端數(shù)據(jù)格式適配神器

在開(kāi)發(fā)中,前端通常使用駝峰命名(比如 userName),而后端接口返回的數(shù)據(jù)往往使用下劃線命名(比如 user_name),因此需要頻繁進(jìn)行“駝峰 <=> 下劃線”的轉(zhuǎn)換,這兩個(gè)方法是前端開(kāi)發(fā)的“高頻工具函數(shù)”。

1. 下劃線轉(zhuǎn)駝峰(user_name → userName)

// 下劃線轉(zhuǎn)駝峰
function snakeToCamel(str) {
  // 正則匹配:_ + 小寫(xiě)字母(比如 _n)
  // 替換邏輯:把匹配到的內(nèi)容,替換成對(duì)應(yīng)的大寫(xiě)字母(n → N)
  return str.replace(/_([a-z])/g, (match, letter) => letter.toUpperCase());
}

2. 駝峰轉(zhuǎn)下劃線(userName → user_name)

// 駝峰轉(zhuǎn)下劃線
function camelToSnake(str) {
  // 正則匹配:大寫(xiě)字母(比如 N)
  // 替換邏輯:把匹配到的大寫(xiě)字母,替換成 _ + 對(duì)應(yīng)的小寫(xiě)字母(N → _n)
  return str.replace(/[A-Z]/g, (match) => "_" + match.toLowerCase());
}

關(guān)鍵細(xì)節(jié)(通俗解讀):

  • 正則匹配邏輯:下劃線轉(zhuǎn)駝峰,核心是“匹配 _ 后面的小寫(xiě)字母”,并把小寫(xiě)字母轉(zhuǎn)大寫(xiě);駝峰轉(zhuǎn)下劃線,核心是“匹配大寫(xiě)字母”,并在前面加 _ 再轉(zhuǎn)小寫(xiě)。

  • 邊界處理:如果字符串沒(méi)有下劃線/大寫(xiě)字母,會(huì)直接返回原字符串(比如 snakeToCamel("user") 還是 "user")。

測(cè)試案例:

console.log(snakeToCamel("user_name")); // userName ?
console.log(snakeToCamel("user_age_18")); // userAge18 ?
console.log(camelToSnake("userName")); // user_name ?
console.log(camelToSnake("userAge18")); // user_age_18 ?

十四、實(shí)現(xiàn) CacheRequest:接口請(qǐng)求緩存與重復(fù)請(qǐng)求合并

在前端開(kāi)發(fā)中,頻繁請(qǐng)求同一個(gè)接口(比如用戶頻繁點(diǎn)擊按鈕請(qǐng)求詳情),會(huì)浪費(fèi)網(wǎng)絡(luò)資源、降低頁(yè)面性能,甚至觸發(fā)后端接口限流。CacheRequest 的核心作用是:緩存請(qǐng)求結(jié)果,合并重復(fù)請(qǐng)求——同一請(qǐng)求同時(shí)多次調(diào)用,只發(fā)一次請(qǐng)求;已成功的請(qǐng)求,直接讀取緩存,無(wú)需再次請(qǐng)求。

核心實(shí)現(xiàn)(class 版本,可直接復(fù)用):

/**
 * 接口請(qǐng)求緩存工具(重復(fù)請(qǐng)求合并 + 緩存)
 * 功能:同一請(qǐng)求同時(shí)多次調(diào)用,只發(fā)一次;已成功的請(qǐng)求直接讀緩存
 */
class CacheRequest {
  constructor() {
    // 緩存池:key = 請(qǐng)求唯一標(biāo)識(shí),value = 緩存數(shù)據(jù) / 請(qǐng)求Promise
    // 用Map存儲(chǔ),支持快速查找和刪除
    this.cache = new Map();
  }
  // 生成唯一緩存 KEY(根據(jù) url + 請(qǐng)求方式 + 參數(shù) + 請(qǐng)求體,確保唯一)
  getCacheKey(url, options = {}) {
    const { method = 'GET', body = null } = options;
    // 處理 params / body 保證唯一:把對(duì)象轉(zhuǎn)成字符串,避免因?qū)ο笠貌煌瑢?dǎo)致key不同
    return [
      url,
      method.toUpperCase(), // 統(tǒng)一轉(zhuǎn)大寫(xiě),避免 GET 和 get 視為不同請(qǐng)求
      JSON.stringify(options.params || {}), // params參數(shù)(GET請(qǐng)求常用)
      body ? JSON.stringify(body) : '' // body參數(shù)(POST請(qǐng)求常用)
    ].join('&'); // 用&連接,生成唯一key
  }
  // 核心請(qǐng)求方法(自動(dòng)緩存 + 合并請(qǐng)求)
  request(url, options = {}) {
    const key = this.getCacheKey(url, options);
    // 1. 緩存存在 → 直接返回(分兩種狀態(tài):pending / success)
    if (this.cache.has(key)) {
      const cached = this.cache.get(key);
      // 狀態(tài)1:請(qǐng)求還在等待中(pending)→ 直接返回同一個(gè)Promise(實(shí)現(xiàn)請(qǐng)求合并)
      // 比如同時(shí)調(diào)用兩次request('/api/user'),第一次發(fā)請(qǐng)求,第二次直接返回同一個(gè)Promise
      if (cached.status === 'pending') {
        return cached.promise;
      }
      // 狀態(tài)2:請(qǐng)求已成功(success)→ 直接返回緩存數(shù)據(jù)(無(wú)需再次請(qǐng)求)
      return Promise.resolve(cached.data);
    }
    // 2. 緩存不存在 → 發(fā)送新請(qǐng)求
    const fetchPromise = fetch(url, options)
      .then(res => {
        // 處理HTTP錯(cuò)誤(比如404、500),拋出錯(cuò)誤,進(jìn)入catch
        if (!res.ok) throw new Error('請(qǐng)求失敗:' + res.status);
        return res.json(); // 解析響應(yīng)數(shù)據(jù)(根據(jù)接口返回格式調(diào)整,比如res.text())
      })
      .then(data => {
        // 請(qǐng)求成功 → 更新緩存為success狀態(tài),存儲(chǔ)數(shù)據(jù)
        this.cache.set(key, { status: 'success', data });
        return data; // 返回?cái)?shù)據(jù),供外部使用
      })
      .catch(err => {
        // 請(qǐng)求失敗 → 刪除緩存(下次調(diào)用會(huì)重新請(qǐng)求,避免緩存錯(cuò)誤數(shù)據(jù))
        this.cache.delete(key);
        throw err; // 拋出錯(cuò)誤,供外部捕獲處理
      });
    // 先把請(qǐng)求存入緩存,標(biāo)記為pending(正在請(qǐng)求),避免重復(fù)請(qǐng)求
    this.cache.set(key, { status: 'pending', promise: fetchPromise });
    return fetchPromise;
  }
  // 清空緩存(可指定url / 全部清空,靈活實(shí)用)
  clearCache(url) {
    if (!url) {
      this.cache.clear(); // 無(wú)url參數(shù) → 清空所有緩存
      return;
    }
    // 有url參數(shù) → 清空所有以該url開(kāi)頭的緩存(比如清空/api/user相關(guān)的所有請(qǐng)求緩存)
    for (const key of this.cache.keys()) {
      if (key.startsWith(url)) this.cache.delete(key);
    }
  }
}

關(guān)鍵細(xì)節(jié)(通俗解讀):

  • 唯一key生成:為什么要結(jié)合 url、method、params、body?因?yàn)椴煌膮?shù)(比如 /api/user?id=1/api/user?id=2)是不同的請(qǐng)求,需要不同的緩存;method 區(qū)分 GET/POST,避免同一url不同請(qǐng)求方式被誤判為同一請(qǐng)求。

  • 重復(fù)請(qǐng)求合并:當(dāng)多個(gè)地方同時(shí)調(diào)用同一個(gè)請(qǐng)求時(shí),緩存中存儲(chǔ)的是 pending 狀態(tài)的 Promise,所有調(diào)用都會(huì)返回同一個(gè) Promise,最終只發(fā)一次請(qǐng)求,避免重復(fù)請(qǐng)求。

  • 緩存更新邏輯:請(qǐng)求成功 → 緩存數(shù)據(jù);請(qǐng)求失敗 → 刪除緩存(避免下次請(qǐng)求讀取錯(cuò)誤數(shù)據(jù));支持手動(dòng)清空緩存(比如用戶刷新頁(yè)面、修改數(shù)據(jù)后,需要重新請(qǐng)求)。

使用案例:

// 實(shí)例化緩存工具
const cacheRequest = new CacheRequest();
// 第一次請(qǐng)求:無(wú)緩存,發(fā)送請(qǐng)求
cacheRequest.request('/api/user', { method: 'GET', params: { id: 1 } })
  .then(data => console.log('第一次請(qǐng)求:', data));
// 第二次請(qǐng)求:同一請(qǐng)求,緩存處于pending狀態(tài),合并請(qǐng)求(不重復(fù)發(fā)請(qǐng)求)
cacheRequest.request('/api/user', { method: 'GET', params: { id: 1 } })
  .then(data => console.log('第二次請(qǐng)求(合并):', data));
// 一段時(shí)間后,第三次請(qǐng)求:緩存已存在,直接讀緩存
setTimeout(() => {
  cacheRequest.request('/api/user', { method: 'GET', params: { id: 1 } })
    .then(data => console.log('第三次請(qǐng)求(讀緩存):', data));
}, 1000);
// 清空/api/user相關(guān)的所有緩存
cacheRequest.clearCache('/api/user');

實(shí)際應(yīng)用場(chǎng)景:

  • 列表頁(yè)、詳情頁(yè):用戶頻繁刷新、點(diǎn)擊,避免重復(fù)請(qǐng)求接口。

  • 表單提交前的校驗(yàn)請(qǐng)求(比如驗(yàn)證手機(jī)號(hào)是否已注冊(cè)),避免用戶頻繁點(diǎn)擊導(dǎo)致多次請(qǐng)求。

  • 需要手動(dòng)刷新緩存的場(chǎng)景(比如用戶修改數(shù)據(jù)后,清空對(duì)應(yīng)緩存,重新請(qǐng)求最新數(shù)據(jù))。

全文總結(jié)

本文從日常開(kāi)發(fā)與面試高頻場(chǎng)景出發(fā),系統(tǒng)梳理了 JavaScript 數(shù)組與對(duì)象的十大核心操作技巧,覆蓋「類(lèi)型轉(zhuǎn)換、結(jié)構(gòu)處理、數(shù)據(jù)拷貝、引用管理」四大核心方向,每個(gè)技巧都兼顧「通俗理解、代碼實(shí)現(xiàn)、避坑要點(diǎn)」,既適配新手入門(mén)的易讀性,也滿足老手復(fù)用的實(shí)用性,更貼合面試手撕的核心考點(diǎn)。

數(shù)組與對(duì)象作為 JS 最基礎(chǔ)的復(fù)合數(shù)據(jù)結(jié)構(gòu),其操作能力直接決定了我們處理復(fù)雜業(yè)務(wù)數(shù)據(jù)的效率 —— 從類(lèi)數(shù)組轉(zhuǎn)換、負(fù)索引實(shí)現(xiàn),到數(shù)組扁平化、樹(shù)形結(jié)構(gòu)轉(zhuǎn)換,再到深拷貝、循環(huán)引用檢測(cè),這些技巧看似零散,實(shí)則都是「數(shù)據(jù)結(jié)構(gòu)遍歷與引用管理」核心邏輯的延伸。

在實(shí)際開(kāi)發(fā)中,無(wú)需死記硬背所有實(shí)現(xiàn)方式,關(guān)鍵是理解「為什么這么做」:比如用 Proxy 實(shí)現(xiàn)負(fù)索引的核心是「攔截屬性訪問(wèn)」,數(shù)組轉(zhuǎn)樹(shù)用 Map 映射的核心是「降低時(shí)間復(fù)雜度」,深拷貝處理循環(huán)引用的核心是「標(biāo)記已遍歷對(duì)象」。掌握底層邏輯后,無(wú)論需求如何變形(比如自定義扁平化規(guī)則、特殊類(lèi)型的深拷貝),都能快速適配。

到此這篇關(guān)于JS手撕代碼之高頻對(duì)象與數(shù)組工具方法的文章就介紹到這了,更多相關(guān)JS高頻對(duì)象與數(shù)組工具方法內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • 微信小程序如何獲知用戶運(yùn)行小程序的場(chǎng)景教程

    微信小程序如何獲知用戶運(yùn)行小程序的場(chǎng)景教程

    這篇文章主要給大家介紹了在微信小程序中如何獲知用戶運(yùn)行小程序場(chǎng)景的相關(guān)資料,文中介紹的非常詳細(xì),對(duì)大家具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)跟著小編一起來(lái)看看吧。
    2017-05-05
  • innerHTML在IE中報(bào)錯(cuò)解決方案

    innerHTML在IE中報(bào)錯(cuò)解決方案

    這篇文章主要介紹了innerHTML在IE中報(bào)錯(cuò)解決方案,需要的朋友可以參考下
    2014-12-12
  • javascript實(shí)現(xiàn)3D變換的立體圓圈實(shí)例

    javascript實(shí)現(xiàn)3D變換的立體圓圈實(shí)例

    這篇文章主要介紹了javascript實(shí)現(xiàn)3D變換的立體圓圈效果,涉及javascript動(dòng)態(tài)操作頁(yè)面元素實(shí)現(xiàn)滾動(dòng)與變色的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下
    2015-08-08
  • uniApp獲取當(dāng)前位置經(jīng)緯度的示例代碼

    uniApp獲取當(dāng)前位置經(jīng)緯度的示例代碼

    這篇文章主要介紹了uniApp獲取當(dāng)前位置經(jīng)緯度,以下是使用uni.getLocation獲取當(dāng)前位置的示例代碼,需要的朋友可以參考下
    2024-01-01
  • 將URL地址都變成ooooooooo的神奇小工具使用介紹

    將URL地址都變成ooooooooo的神奇小工具使用介紹

    這篇文章主要為大家介紹了將URL地址都變成ooooooooo的神奇小工具使用介紹,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-04-04
  • javascript中閉包c(diǎn)losure的深入講解

    javascript中閉包c(diǎn)losure的深入講解

    這篇文章主要給大家介紹了關(guān)于javascript中閉包c(diǎn)losure的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2021-03-03
  • 基于JS實(shí)現(xiàn)html中placeholder屬性提示文字效果示例

    基于JS實(shí)現(xiàn)html中placeholder屬性提示文字效果示例

    這篇文章主要介紹了基于JS實(shí)現(xiàn)html中placeholder屬性提示文字效果,涉及javascript事件響應(yīng)及頁(yè)面元素屬性動(dòng)態(tài)操作相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下
    2018-04-04
  • 基于JavaScript或jQuery實(shí)現(xiàn)網(wǎng)站夜間/高亮模式

    基于JavaScript或jQuery實(shí)現(xiàn)網(wǎng)站夜間/高亮模式

    這篇文章主要介紹了基于JavaScript或jQuery實(shí)現(xiàn)網(wǎng)站夜間/高亮模式,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-05-05
  • JS實(shí)現(xiàn)的簡(jiǎn)單拖拽功能示例

    JS實(shí)現(xiàn)的簡(jiǎn)單拖拽功能示例

    這篇文章主要介紹了JS實(shí)現(xiàn)的簡(jiǎn)單拖拽功能,涉及javascript鼠標(biāo)事件響應(yīng)及頁(yè)面元素屬性動(dòng)態(tài)操作相關(guān)技巧,需要的朋友可以參考下
    2017-03-03
  • JavaScript實(shí)現(xiàn)頁(yè)面截圖3種解決方案

    JavaScript實(shí)現(xiàn)頁(yè)面截圖3種解決方案

    網(wǎng)頁(yè)截圖是指將網(wǎng)頁(yè)上的內(nèi)容截取下來(lái),并保存為圖片的過(guò)程,下面這篇文章主要給大家介紹了關(guān)于JavaScript實(shí)現(xiàn)頁(yè)面截圖的3種解決方案,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2024-06-06

最新評(píng)論

肥东县| 通辽市| 洛隆县| 华安县| 通榆县| 无锡市| 且末县| 敦化市| 金湖县| 吴旗县| 建平县| 黑山县| 修武县| 柘荣县| 卢氏县| 全椒县| 岑溪市| 团风县| 柘荣县| 麻栗坡县| 柘荣县| 盐津县| 金堂县| 韩城市| 淮南市| 柳林县| 施甸县| 长寿区| 屯昌县| 长乐市| 全椒县| 陆河县| 平安县| 股票| 潍坊市| 彰化县| 茶陵县| 读书| 容城县| 威信县| 噶尔县|