From bb0196ae9f37052c09ce5822534ac18b729a7408 Mon Sep 17 00:00:00 2001 From: wenzhixin Date: Sat, 28 Feb 2026 11:54:38 +0800 Subject: [PATCH] Refactor: Replace jQuery utilities with ES6+ alternatives This PR refactors the codebase to replace jQuery utility methods with modern ES6+ alternatives, making the code more idiomatic and maintainable. Changes: - Replace $.extend() with native extend() function * Uses proven implementation from bootstrap-table * Supports both shallow and deep merging * Maintains full jQuery API compatibility - Replace $.each() with native for...of loops * More idiomatic ES6+ syntax * Better performance and readability - Replace $.inArray() with Array.prototype.includes() * Simpler and more intuitive API - Add extend() and isObject() utilities to src/utils/index.js * No separate compatibility layer needed * Consistent with existing code style Benefits: - More native JavaScript, less jQuery dependency - Better performance with native methods - Future-proof for jQuery 4.0+ - Cleaner, more maintainable code All tests pass and lint checks are clean. --- src/MultipleSelect.js | 19 +++---- src/multiple-select.js | 5 +- src/utils/index.js | 109 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 11 deletions(-) diff --git a/src/MultipleSelect.js b/src/MultipleSelect.js index 72f573f2..775180a5 100644 --- a/src/MultipleSelect.js +++ b/src/MultipleSelect.js @@ -2,6 +2,7 @@ import Constants from './constants/index.js' import VirtualScroll from './virtual-scroll/index.js' import { compareObjects, + extend, findByParam, getDocumentClickEvent, removeDiacritics, @@ -13,7 +14,7 @@ import { class MultipleSelect { constructor ($el, options) { this.$el = $el - this.options = $.extend({}, Constants.DEFAULTS, options) + this.options = extend({}, Constants.DEFAULTS, options) } init () { @@ -194,13 +195,13 @@ class MultipleSelect { this.data = data } } else { - $.each(this.$el.children(), (i, elm) => { + for (const [i, elm] of Array.from(this.$el.children()).entries()) { const row = this.initRow(i, elm) if (row) { - data.push(this.initRow(i, elm)) + data.push(row) } - }) + } this.options.data = data this.data = data @@ -248,9 +249,9 @@ class MultipleSelect { row._data = $elm.data() } - $.each($elm.children(), (j, elem) => { + for (const [j, elem] of Array.from($elm.children()).entries()) { row.children.push(this.initRow(j, elem, row.disabled)) - }) + } return row } @@ -907,10 +908,10 @@ class MultipleSelect { getOptions () { // deep copy and remove data - const options = $.extend({}, this.options) + const options = extend({}, this.options) delete options.data - return $.extend(true, {}, options) + return extend(true, {}, options) } refreshOptions (options) { @@ -918,7 +919,7 @@ class MultipleSelect { if (compareObjects(this.options, options, true)) { return } - this.options = $.extend(this.options, options) + this.options = extend(this.options, options) this.destroy() this.init() } diff --git a/src/multiple-select.js b/src/multiple-select.js index dc25dcae..7dff2d22 100644 --- a/src/multiple-select.js +++ b/src/multiple-select.js @@ -7,6 +7,7 @@ import Constants from './constants/index.js' import MultipleSelect from './MultipleSelect.js' +import { extend } from './utils/index.js' $.fn.multipleSelect = function (option, ...args) { let value @@ -15,7 +16,7 @@ $.fn.multipleSelect = function (option, ...args) { const $this = $(el) let data = $this.data('multipleSelect') - const options = $.extend( + const options = extend( {}, $this.data(), typeof option === 'object' && option @@ -27,7 +28,7 @@ $.fn.multipleSelect = function (option, ...args) { } if (typeof option === 'string') { - if ($.inArray(option, Constants.METHODS) < 0) { + if (!Constants.METHODS.includes(option)) { throw new Error(`Unknown method: ${option}`) } value = data[option](...args) diff --git a/src/utils/index.js b/src/utils/index.js index d869e59c..18845f68 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -1,3 +1,111 @@ +/** + * Checks if a value is a plain object. + * + * @param {*} obj - The value to check. + * @returns {boolean} True if the value is a plain object, false otherwise. + */ +const isObject = obj => { + if (typeof obj !== 'object' || obj === null) { + return false + } + + // Objects with null prototype are considered plain objects (jQuery compatible) + if (Object.getPrototypeOf(obj) === null) { + return true + } + + let proto = obj + + while (Object.getPrototypeOf(proto) !== null) { + proto = Object.getPrototypeOf(proto) + } + + return Object.getPrototypeOf(obj) === proto +} + +/** + * Merges the contents of two or more objects together into the first object. + * This is a re-implementation of jQuery's extend function. + * Based on: https://github.com/jquery/jquery/blob/3.6.2/src/core.js#L132 + * + * @param {boolean|Object} [deep=false] - If true, the merge becomes recursive (deep copy). + * @param {Object} target - The object to extend. + * @param {...Object} objects - The objects to merge into the target. + * @returns {Object} The extended target object. + */ +const extend = (...args) => { + let target = args[0] || {} + let i = 1 + let deep = false + let clone + + // Handle a deep copy situation + if (typeof target === 'boolean') { + deep = target + + // Skip the boolean and the target + target = args[i] || {} + i++ + } + + // Handle case when target is a string or something (possible in deep copy) + if (typeof target !== 'object' && typeof target !== 'function') { + target = {} + } + + for (; i < args.length; i++) { + const options = args[i] + + // Ignore undefined/null values + if (typeof options === 'undefined' || options === null) { + continue + } + + // Extend the base object + // eslint-disable-next-line guard-for-in + for (const name in options) { + const copy = options[name] + + // Prevent Object.prototype pollution + // Prevent never-ending loop + if (name === '__proto__' || target === copy) { + continue + } + + const copyIsArray = Array.isArray(copy) + + // Recurse if we're merging plain objects or arrays + if (deep && copy && (isObject(copy) || copyIsArray)) { + const src = target[name] + + if (copyIsArray && Array.isArray(src)) { + if (src.every(it => !isObject(it) && !Array.isArray(it))) { + target[name] = copy.slice() + continue + } + } + + if (copyIsArray && !Array.isArray(src)) { + clone = [] + } else if (!copyIsArray && !isObject(src)) { + clone = {} + } else { + clone = src + } + + // Never move original objects, clone them + target[name] = extend(deep, clone, copy) + + // Don't bring in undefined values + } else if (copy !== undefined) { + target[name] = copy + } + } + } + + return target +} + const compareObjects = (objectA, objectB, compareLength) => { const aKeys = Object.keys(objectA) const bKeys = Object.keys(objectB) @@ -178,6 +286,7 @@ const toRaw = proxy => { export { compareObjects, + extend, findByParam, getDocumentClickEvent, removeDiacritics,