前端一面手寫面試題總結

hello_world_1024發表於2023-03-13

使用Promise封裝AJAX請求

// promise 封裝實現:
function getJSON(url) {
  // 建立一個 promise 物件
  let promise = new Promise(function(resolve, reject) {
    let xhr = new XMLHttpRequest();
    // 新建一個 http 請求
    xhr.open("GET", url, true);
    // 設定狀態的監聽函式
    xhr.onreadystatechange = function() {
      if (this.readyState !== 4) return;
      // 當請求成功或失敗時,改變 promise 的狀態
      if (this.status === 200) {
        resolve(this.response);
      } else {
        reject(new Error(this.statusText));
      }
    };
    // 設定錯誤監聽函式
    xhr.onerror = function() {
      reject(new Error(this.statusText));
    };
    // 設定響應的資料型別
    xhr.responseType = "json";
    // 設定請求頭資訊
    xhr.setRequestHeader("Accept", "application/json");
    // 傳送 http 請求
    xhr.send(null);
  });
  return promise;
}

實現一個add方法完成兩個大數相加

// 題目
let a = "9007199254740991";
let b = "1234567899999999999";

function add(a ,b){
   //...
}

實現程式碼如下:

function add(a ,b){
   //取兩個數字的最大長度
   let maxLength = Math.max(a.length, b.length);
   //用0去補齊長度
   a = a.padStart(maxLength , 0);//"0009007199254740991"
   b = b.padStart(maxLength , 0);//"1234567899999999999"
   //定義加法過程中需要用到的變數
   let t = 0;
   let f = 0;   //"進位"
   let sum = "";
   for(let i=maxLength-1 ; i>=0 ; i--){
      t = parseInt(a[i]) + parseInt(b[i]) + f;
      f = Math.floor(t/10);
      sum = t%10 + sum;
   }
   if(f!==0){
      sum = '' + f + sum;
   }
   return sum;
}

實現雙向資料繫結

let obj = {}
let input = document.getElementById('input')
let span = document.getElementById('span')
// 資料劫持
Object.defineProperty(obj, 'text', {
  configurable: true,
  enumerable: true,
  get() {
    console.log('獲取資料了')
  },
  set(newVal) {
    console.log('資料更新了')
    input.value = newVal
    span.innerHTML = newVal
  }
})
// 輸入監聽
input.addEventListener('keyup', function(e) {
  obj.text = e.target.value
})

實現釋出-訂閱模式

class EventCenter{
  // 1. 定義事件容器,用來裝事件陣列
    let handlers = {}

  // 2. 新增事件方法,引數:事件名 事件方法
  addEventListener(type, handler) {
    // 建立新陣列容器
    if (!this.handlers[type]) {
      this.handlers[type] = []
    }
    // 存入事件
    this.handlers[type].push(handler)
  }

  // 3. 觸發事件,引數:事件名 事件引數
  dispatchEvent(type, params) {
    // 若沒有註冊該事件則丟擲錯誤
    if (!this.handlers[type]) {
      return new Error('該事件未註冊')
    }
    // 觸發事件
    this.handlers[type].forEach(handler => {
      handler(...params)
    })
  }

  // 4. 事件移除,引數:事件名 要刪除事件,若無第二個引數則刪除該事件的訂閱和釋出
  removeEventListener(type, handler) {
    if (!this.handlers[type]) {
      return new Error('事件無效')
    }
    if (!handler) {
      // 移除事件
      delete this.handlers[type]
    } else {
      const index = this.handlers[type].findIndex(el => el === handler)
      if (index === -1) {
        return new Error('無該繫結事件')
      }
      // 移除事件
      this.handlers[type].splice(index, 1)
      if (this.handlers[type].length === 0) {
        delete this.handlers[type]
      }
    }
  }
}

手寫節流函式

函式節流是指規定一個單位時間,在這個單位時間內,只能有一次觸發事件的回撥函式執行,如果在同一個單位時間內某事件被觸發多次,只有一次能生效。節流可以使用在 scroll 函式的事件監聽上,透過事件節流來降低事件呼叫的頻率。

// 函式節流的實現;
function throttle(fn, delay) {
  let curTime = Date.now();

  return function() {
    let context = this,
        args = arguments,
        nowTime = Date.now();

    // 如果兩次時間間隔超過了指定時間,則執行函式。
    if (nowTime - curTime >= delay) {
      curTime = Date.now();
      return fn.apply(context, args);
    }
  };
}

Promise並行限制

就是實現有並行限制的Promise排程器問題

class Scheduler {
  constructor() {
    this.queue = [];
    this.maxCount = 2;
    this.runCounts = 0;
  }
  add(promiseCreator) {
    this.queue.push(promiseCreator);
  }
  taskStart() {
    for (let i = 0; i < this.maxCount; i++) {
      this.request();
    }
  }
  request() {
    if (!this.queue || !this.queue.length || this.runCounts >= this.maxCount) {
      return;
    }
    this.runCounts++;

    this.queue.shift()().then(() => {
      this.runCounts--;
      this.request();
    });
  }
}

const timeout = time => new Promise(resolve => {
  setTimeout(resolve, time);
})

const scheduler = new Scheduler();

const addTask = (time,order) => {
  scheduler.add(() => timeout(time).then(()=>console.log(order)))
}


addTask(1000, '1');
addTask(500, '2');
addTask(300, '3');
addTask(400, '4');
scheduler.taskStart()
// 2
// 3
// 1
// 4

參考 前端進階面試題詳細解答

Promise.all

Promise.all是支援鏈式呼叫的,本質上就是返回了一個Promise例項,透過resolvereject來改變例項狀態。

Promise.myAll = function(promiseArr) {
  return new Promise((resolve, reject) => {
    const ans = [];
    let index = 0;
    for (let i = 0; i < promiseArr.length; i++) {
      promiseArr[i]
      .then(res => {
        ans[i] = res;
        index++;
        if (index === promiseArr.length) {
          resolve(ans);
        }
      })
      .catch(err => reject(err));
    }
  })
}

實現淺複製

淺複製是指,一個新的物件對原始物件的屬性值進行精確地複製,如果複製的是基本資料型別,複製的就是基本資料型別的值,如果是引用資料型別,複製的就是記憶體地址。如果其中一個物件的引用記憶體地址發生改變,另一個物件也會發生變化。

(1)Object.assign()

Object.assign()是ES6中物件的複製方法,接受的第一個引數是目標物件,其餘引數是源物件,用法:Object.assign(target, source_1, ···),該方法可以實現淺複製,也可以實現一維物件的深複製。

注意:

  • 如果目標物件和源物件有同名屬性,或者多個源物件有同名屬性,則後面的屬性會覆蓋前面的屬性。
  • 如果該函式只有一個引數,當引數為物件時,直接返回該物件;當引數不是物件時,會先將引數轉為物件然後返回。
  • 因為nullundefined 不能轉化為物件,所以第一個引數不能為nullundefined,會報錯。
let target = {a: 1};
let object2 = {b: 2};
let object3 = {c: 3};
Object.assign(target,object2,object3);  
console.log(target);  // {a: 1, b: 2, c: 3}

(2)擴充套件運運算元

使用擴充套件運運算元可以在構造字面量物件的時候,進行屬性的複製。語法:let cloneObj = { ...obj };

let obj1 = {a:1,b:{c:1}}
let obj2 = {...obj1};
obj1.a = 2;
console.log(obj1); //{a:2,b:{c:1}}
console.log(obj2); //{a:1,b:{c:1}}
obj1.b.c = 2;
console.log(obj1); //{a:2,b:{c:2}}
console.log(obj2); //{a:1,b:{c:2}}

(3)陣列方法實現陣列淺複製

1)Array.prototype.slice
  • slice()方法是JavaScript陣列的一個方法,這個方法可以從已有陣列中返回選定的元素:用法:array.slice(start, end),該方法不會改變原始陣列。
  • 該方法有兩個引數,兩個引數都可選,如果兩個引數都不寫,就可以實現一個陣列的淺複製。
let arr = [1,2,3,4];
console.log(arr.slice()); // [1,2,3,4]
console.log(arr.slice() === arr); //false
2)Array.prototype.concat
  • concat() 方法用於合併兩個或多個陣列。此方法不會更改現有陣列,而是返回一個新陣列。
  • 該方法有兩個引數,兩個引數都可選,如果兩個引數都不寫,就可以實現一個陣列的淺複製。
let arr = [1,2,3,4];
console.log(arr.concat()); // [1,2,3,4]
console.log(arr.concat() === arr); //false

(4)手寫實現淺複製

// 淺複製的實現;

function shallowCopy(object) {
  // 只複製物件
  if (!object || typeof object !== "object") return;

  // 根據 object 的型別判斷是新建一個陣列還是物件
  let newObject = Array.isArray(object) ? [] : {};

  // 遍歷 object,並且判斷是 object 的屬性才複製
  for (let key in object) {
    if (object.hasOwnProperty(key)) {
      newObject[key] = object[key];
    }
  }

  return newObject;
}// 淺複製的實現;

function shallowCopy(object) {
  // 只複製物件
  if (!object || typeof object !== "object") return;

  // 根據 object 的型別判斷是新建一個陣列還是物件
  let newObject = Array.isArray(object) ? [] : {};

  // 遍歷 object,並且判斷是 object 的屬性才複製
  for (let key in object) {
    if (object.hasOwnProperty(key)) {
      newObject[key] = object[key];
    }
  }

  return newObject;
}// 淺複製的實現;
function shallowCopy(object) {
  // 只複製物件
  if (!object || typeof object !== "object") return;
  // 根據 object 的型別判斷是新建一個陣列還是物件
  let newObject = Array.isArray(object) ? [] : {};
  // 遍歷 object,並且判斷是 object 的屬性才複製
  for (let key in object) {
    if (object.hasOwnProperty(key)) {
      newObject[key] = object[key];
    }
  }
  return newObject;
}

手寫 new 運運算元

在呼叫 new 的過程中會發生以上四件事情:

(1)首先建立了一個新的空物件

(2)設定原型,將物件的原型設定為函式的 prototype 物件。

(3)讓函式的 this 指向這個物件,執行建構函式的程式碼(為這個新物件新增屬性)

(4)判斷函式的返回值型別,如果是值型別,返回建立的物件。如果是引用型別,就返回這個引用型別的物件。

function objectFactory() {
  let newObject = null;
  let constructor = Array.prototype.shift.call(arguments);
  let result = null;
  // 判斷引數是否是一個函式
  if (typeof constructor !== "function") {
    console.error("type error");
    return;
  }
  // 新建一個空物件,物件的原型為建構函式的 prototype 物件
  newObject = Object.create(constructor.prototype);
  // 將 this 指向新建物件,並執行函式
  result = constructor.apply(newObject, arguments);
  // 判斷返回物件
  let flag = result && (typeof result === "object" || typeof result === "function");
  // 判斷返回結果
  return flag ? result : newObject;
}
// 使用方法
objectFactory(建構函式, 初始化引數);

Object.assign

Object.assign()方法用於將所有可列舉屬性的值從一個或多個源物件複製到目標物件。它將返回目標物件(請注意這個操作是淺複製)

Object.defineProperty(Object, 'assign', {
  value: function(target, ...args) {
    if (target == null) {
      return new TypeError('Cannot convert undefined or null to object');
    }

    // 目標物件需要統一是引用資料型別,若不是會自動轉換
    const to = Object(target);

    for (let i = 0; i < args.length; i++) {
      // 每一個源物件
      const nextSource = args[i];
      if (nextSource !== null) {
        // 使用for...in和hasOwnProperty雙重判斷,確保只拿到本身的屬性、方法(不包含繼承的)
        for (const nextKey in nextSource) {
          if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
            to[nextKey] = nextSource[nextKey];
          }
        }
      }
    }
    return to;
  },
  // 不可列舉
  enumerable: false,
  writable: true,
  configurable: true,
})

實現 add(1)(2)(3)

函式柯里化概念: 柯里化(Currying)是把接受多個引數的函式轉變為接受一個單一引數的函式,並且返回接受餘下的引數且返回結果的新函式的技術。

1)粗暴版

function add (a) {
return function (b) {
     return function (c) {
      return a + b + c;
     }
}
}
console.log(add(1)(2)(3)); // 6

2)柯里化解決方案

  • 引數長度固定
var add = function (m) {
  var temp = function (n) {
    return add(m + n);
  }
  temp.toString = function () {
    return m;
  }
  return temp;
};
console.log(add(3)(4)(5)); // 12
console.log(add(3)(6)(9)(25)); // 43

對於add(3)(4)(5),其執行過程如下:

  1. 先執行add(3),此時m=3,並且返回temp函式;
  2. 執行temp(4),這個函式內執行add(m+n),n是此次傳進來的數值4,m值還是上一步中的3,所以add(m+n)=add(3+4)=add(7),此時m=7,並且返回temp函式
  3. 執行temp(5),這個函式內執行add(m+n),n是此次傳進來的數值5,m值還是上一步中的7,所以add(m+n)=add(7+5)=add(12),此時m=12,並且返回temp函式
  4. 由於後面沒有傳入引數,等於返回的temp函式不被執行而是列印,瞭解JS的朋友都知道物件的toString是修改物件轉換字串的方法,因此程式碼中temp函式的toString函式return m值,而m值是最後一步執行函式時的值m=12,所以返回值是12。
  5. 引數長度不固定
function add (...args) {
    //求和
    return args.reduce((a, b) => a + b)
}
function currying (fn) {
    let args = []
    return function temp (...newArgs) {
        if (newArgs.length) {
            args = [
                ...args,
                ...newArgs
            ]
            return temp
        } else {
            let val = fn.apply(this, args)
            args = [] //保證再次呼叫時清空
            return val
        }
    }
}
let addCurry = currying(add)
console.log(addCurry(1)(2)(3)(4, 5)())  //15
console.log(addCurry(1)(2)(3, 4, 5)())  //15
console.log(addCurry(1)(2, 3, 4, 5)())  //15

字串解析問題

var a = {
    b: 123,
    c: '456',
    e: '789',
}
var str=`a{a.b}aa{a.c}aa {a.d}aaaa`;
// => 'a123aa456aa {a.d}aaaa'

實現函式使得將str字串中的{}內的變數替換,如果屬性不存在保持原樣(比如{a.d}

類似於模版字串,但有一點出入,實際上原理大差不差

const fn1 = (str, obj) => {
    let res = '';
    // 標誌位,標誌前面是否有{
    let flag = false;
    let start;
    for (let i = 0; i < str.length; i++) {
        if (str[i] === '{') {
            flag = true;
            start = i + 1;
            continue;
        }
        if (!flag) res += str[i];
        else {
            if (str[i] === '}') {
                flag = false;
                res += match(str.slice(start, i), obj);
            }
        }
    }
    return res;
}
// 物件匹配操作
const match = (str, obj) => {
    const keys = str.split('.').slice(1);
    let index = 0;
    let o = obj;
    while (index < keys.length) {
        const key = keys[index];
        if (!o[key]) {
            return `{${str}}`;
        } else {
            o = o[key];
        }
        index++;
    }
    return o;
}

使用 setTimeout 實現 setInterval

setInterval 的作用是每隔一段指定時間執行一個函式,但是這個執行不是真的到了時間立即執行,它真正的作用是每隔一段時間將事件加入事件佇列中去,只有噹噹前的執行棧為空的時候,才能去從事件佇列中取出事件執行。所以可能會出現這樣的情況,就是當前執行棧執行的時間很長,導致事件佇列裡邊積累多個定時器加入的事件,當執行棧結束的時候,這些事件會依次執行,因此就不能到間隔一段時間執行的效果。

針對 setInterval 的這個缺點,我們可以使用 setTimeout 遞迴呼叫來模擬 setInterval,這樣我們就確保了只有一個事件結束了,我們才會觸發下一個定時器事件,這樣解決了 setInterval 的問題。

實現思路是使用遞迴函式,不斷地去執行 setTimeout 從而達到 setInterval 的效果

function mySetInterval(fn, timeout) {
  // 控制器,控制定時器是否繼續執行
  var timer = {
    flag: true
  };
  // 設定遞迴函式,模擬定時器執行。
  function interval() {
    if (timer.flag) {
      fn();
      setTimeout(interval, timeout);
    }
  }
  // 啟動定時器
  setTimeout(interval, timeout);
  // 返回控制器
  return timer;
}

封裝非同步的fetch,使用async await方式來使用

(async () => {
    class HttpRequestUtil {
        async get(url) {
            const res = await fetch(url);
            const data = await res.json();
            return data;
        }
        async post(url, data) {
            const res = await fetch(url, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify(data)
            });
            const result = await res.json();
            return result;
        }
        async put(url, data) {
            const res = await fetch(url, {
                method: 'PUT',
                headers: {
                    'Content-Type': 'application/json'
                },
                data: JSON.stringify(data)
            });
            const result = await res.json();
            return result;
        }
        async delete(url, data) {
            const res = await fetch(url, {
                method: 'DELETE',
                headers: {
                    'Content-Type': 'application/json'
                },
                data: JSON.stringify(data)
            });
            const result = await res.json();
            return result;
        }
    }
    const httpRequestUtil = new HttpRequestUtil();
    const res = await httpRequestUtil.get('http://golderbrother.cn/');
    console.log(res);
})();

實現Event(event bus)

event bus既是node中各個模組的基石,又是前端元件通訊的依賴手段之一,同時涉及了訂閱-釋出設計模式,是非常重要的基礎。

簡單版:

class EventEmeitter {
  constructor() {
    this._events = this._events || new Map(); // 儲存事件/回撥鍵值對
    this._maxListeners = this._maxListeners || 10; // 設立監聽上限
  }
}


// 觸發名為type的事件
EventEmeitter.prototype.emit = function(type, ...args) {
  let handler;
  // 從儲存事件鍵值對的this._events中獲取對應事件回撥函式
  handler = this._events.get(type);
  if (args.length > 0) {
    handler.apply(this, args);
  } else {
    handler.call(this);
  }
  return true;
};

// 監聽名為type的事件
EventEmeitter.prototype.addListener = function(type, fn) {
  // 將type事件以及對應的fn函式放入this._events中儲存
  if (!this._events.get(type)) {
    this._events.set(type, fn);
  }
};

面試版:

class EventEmeitter {
  constructor() {
    this._events = this._events || new Map(); // 儲存事件/回撥鍵值對
    this._maxListeners = this._maxListeners || 10; // 設立監聽上限
  }
}

// 觸發名為type的事件
EventEmeitter.prototype.emit = function(type, ...args) {
  let handler;
  // 從儲存事件鍵值對的this._events中獲取對應事件回撥函式
  handler = this._events.get(type);
  if (args.length > 0) {
    handler.apply(this, args);
  } else {
    handler.call(this);
  }
  return true;
};

// 監聽名為type的事件
EventEmeitter.prototype.addListener = function(type, fn) {
  // 將type事件以及對應的fn函式放入this._events中儲存
  if (!this._events.get(type)) {
    this._events.set(type, fn);
  }
};

// 觸發名為type的事件
EventEmeitter.prototype.emit = function(type, ...args) {
  let handler;
  handler = this._events.get(type);
  if (Array.isArray(handler)) {
    // 如果是一個陣列說明有多個監聽者,需要依次此觸發裡面的函式
    for (let i = 0; i < handler.length; i++) {
      if (args.length > 0) {
        handler[i].apply(this, args);
      } else {
        handler[i].call(this);
      }
    }
  } else {
    // 單個函式的情況我們直接觸發即可
    if (args.length > 0) {
      handler.apply(this, args);
    } else {
      handler.call(this);
    }
  }

  return true;
};

// 監聽名為type的事件
EventEmeitter.prototype.addListener = function(type, fn) {
  const handler = this._events.get(type); // 獲取對應事件名稱的函式清單
  if (!handler) {
    this._events.set(type, fn);
  } else if (handler && typeof handler === "function") {
    // 如果handler是函式說明只有一個監聽者
    this._events.set(type, [handler, fn]); // 多個監聽者我們需要用陣列儲存
  } else {
    handler.push(fn); // 已經有多個監聽者,那麼直接往陣列裡push函式即可
  }
};

EventEmeitter.prototype.removeListener = function(type, fn) {
  const handler = this._events.get(type); // 獲取對應事件名稱的函式清單

  // 如果是函式,說明只被監聽了一次
  if (handler && typeof handler === "function") {
    this._events.delete(type, fn);
  } else {
    let postion;
    // 如果handler是陣列,說明被監聽多次要找到對應的函式
    for (let i = 0; i < handler.length; i++) {
      if (handler[i] === fn) {
        postion = i;
      } else {
        postion = -1;
      }
    }
    // 如果找到匹配的函式,從陣列中清除
    if (postion !== -1) {
      // 找到陣列對應的位置,直接清除此回撥
      handler.splice(postion, 1);
      // 如果清除後只有一個函式,那麼取消陣列,以函式形式儲存
      if (handler.length === 1) {
        this._events.set(type, handler[0]);
      }
    } else {
      return this;
    }
  }
};
實現具體過程和思路見實現event

解析 URL Params 為物件

let url = 'http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled';
parseParam(url)
/* 結果
{ user: 'anonymous',
  id: [ 123, 456 ], // 重複出現的 key 要組裝成陣列,能被轉成數字的就轉成數字型別
  city: '北京', // 中文需解碼
  enabled: true, // 未指定值得 key 約定為 true
}
*/

function parseParam(url) {
  const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 將 ? 後面的字串取出來
  const paramsArr = paramsStr.split('&'); // 將字串以 & 分割後存到陣列中
  let paramsObj = {};
  // 將 params 存到物件中
  paramsArr.forEach(param => {
    if (/=/.test(param)) { // 處理有 value 的引數
      let [key, val] = param.split('='); // 分割 key 和 value
      val = decodeURIComponent(val); // 解碼
      val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判斷是否轉為數字

      if (paramsObj.hasOwnProperty(key)) { // 如果物件有 key,則新增一個值
        paramsObj[key] = [].concat(paramsObj[key], val);
      } else { // 如果物件沒有這個 key,建立 key 並設定值
        paramsObj[key] = val;
      }
    } else { // 處理沒有 value 的引數
      paramsObj[param] = true;
    }
  })

  return paramsObj;
}

驗證是否是身份證

function isCardNo(number) {
    var regx = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
    return regx.test(number);
}

實現觀察者模式

觀察者模式(基於釋出訂閱模式) 有觀察者,也有被觀察者

觀察者需要放到被觀察者中,被觀察者的狀態變化需要通知觀察者 我變化了 內部也是基於釋出訂閱模式,收集觀察者,狀態變化後要主動通知觀察者

class Subject { // 被觀察者 學生
  constructor(name) {
    this.state = 'happy'
    this.observers = []; // 儲存所有的觀察者
  }
  // 收集所有的觀察者
  attach(o){ // Subject. prototype. attch
    this.observers.push(o)
  }
  // 更新被觀察者 狀態的方法
  setState(newState) {
    this.state = newState; // 更新狀態
    // this 指被觀察者 學生
    this.observers.forEach(o => o.update(this)) // 通知觀察者 更新它們的狀態
  }
}

class Observer{ // 觀察者 父母和老師
  constructor(name) {
    this.name = name
  }
  update(student) {
    console.log('當前' + this.name + '被通知了', '當前學生的狀態是' + student.state)
  }
}

let student = new Subject('學生'); 

let parent = new Observer('父母'); 
let teacher = new Observer('老師'); 

// 被觀察者儲存觀察者的前提,需要先接納觀察者
student. attach(parent); 
student. attach(teacher); 
student. setState('被欺負了');

實現陣列的filter方法

Array.prototype._filter = function(fn) {
    if (typeof fn !== "function") {
        throw Error('引數必須是一個函式');
    }
    const res = [];
    for (let i = 0, len = this.length; i < len; i++) {
        fn(this[i]) && res.push(this[i]);
    }
    return res;
}

實現千位分隔符

// 保留三位小數
parseToMoney(1234.56); // return '1,234.56'
parseToMoney(123456789); // return '123,456,789'
parseToMoney(1087654.321); // return '1,087,654.321'

function parseToMoney(num) {
  num = parseFloat(num.toFixed(3));
  let [integer, decimal] = String.prototype.split.call(num, '.');
  integer = integer.replace(/\d(?=(\d{3})+$)/g, '$&,');
  return integer + '.' + (decimal ? decimal : '');
}

正規表示式(運用了正則的前向宣告和反前向宣告):

function parseToMoney(str){
    // 僅僅對位置進行匹配
    let re = /(?=(?!\b)(\d{3})+$)/g; 
   return str.replace(re,','); 
}

相關文章