普通视图

发现新文章,点击刷新页面。
昨天 — 2026年1月12日首页

Underscore.js 整体设计思路与架构分析

作者 Anita_Sun
2026年1月12日 07:50

源码分析: bgithub.xyz/lessfish/un…

官网中所带注释的源码:

整体分析:underscorejs.org/docs/unders…

模块分析:underscorejs.org/docs/module…

核心架构模式

模块结构

Underscore.js 采用了 立即执行函数表达式 (IIFE) 作为核心模块结构,创建了一个封闭的作用域,避免了全局变量污染:

这种设计方式能够让 Underscore.js :

  • 支持多种模块系统(CommonJS、AMD、全局变量)
  • 提供 noConflict 方法,避免命名冲突
  • 在不同环境中(浏览器、Node.js)正常工作
(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? 
  // 场景1:CommonJS 环境(Node.js)
  module.exports = factory() :
  typeof define === 'function' && define.amd ? 
  // 场景2:AMD 环境(如 RequireJS)
  define('underscore', factory) :
  // 场景3:无模块化的浏览器全局环境(兜底)
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, (function () {
    var current = global._; // 保存当前全局的 _ 变量
    var exports = global._ = factory(); // 把 underscore 挂载到全局 _
    // 解决命名冲突的 noConflict 方法
    exports.noConflict = function () { global._ = current; return exports; };
  }()));
}(this, (function () {
  // 核心实现...
})));

双模式 API 设计

Underscore.js 同时支持两种调用方式:

函数式调用

_.map([123], function(num) { return 
num * 2; });

面向对象调用(链式)

_([123]).map(function(num) { return 
num * 2; }).value();

这种设计通过以下核心构造函数实现:

function _$1(obj) {
  if (obj instanceof _$1) return obj;
  if (!(this instanceof _$1)) return new 
  _$1(obj);
  this._wrapped = obj;
}

方法挂载机制

函数定义与收集

Underscore.js 首先将所有功能实现为独立函数,然后通过 allExports 对象统一收集:

var allExports = {
  __proto__: null,
  VERSION: VERSION,
  restArguments: restArguments,
  isObject: isObject,
  // ... 其他函数
};

方法挂载

通过 mixin 方法,将所有函数同时挂载到构造函数和原型链上:

function mixin(obj) {
  each(functions(obj), function(name) {
    var func = _$1[name] = obj[name];
    _$1.prototype[name] = function() {
      var args = [this._wrapped];
      push.apply(args, arguments);
      return chainResult(this, func.apply
      (_$1, args));
    };
  });
  return _$1;
}

// 执行挂载
var _ = mixin(allExports);

这种设计使得:

  • 所有函数既可以通过 _.func() 方式调用
  • 也可以通过_().func() 链式调用

数组方法集成

Underscore.js 还集成了原生数组的方法,分为两类:

变更方法(Mutator)

pop/push/reverse/shift/sort/splice/unshift 这些方法的核心是修改原数组(比如 push 往原数组加元素,shift 从原数组删第一个元素),执行后原数组本身变了,方法返回值只是 “操作结果”(比如 pop 返回删除的元素),而非新数组。

// 假设包装类实例:_([1,2,3])
const arrWrapper = _([1,2,3]);

// 调用mutator方法push
arrWrapper.push(4);
console.log(arrWrapper._wrapped); // [1,2,3,4](原数组被修改)
each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
  // 从数组原型(ArrayProto是Array.prototype的简写)获取对应的原生方法
  var method = ArrayProto[name];

  // 为包装类原型添加当前遍历的方法(如pop/push等)
  _$1.prototype[name] = function() {
    // 获取包装类实例中包裹的原始数组(_wrapped是包装类存储原始数据的核心属性)
    var obj = this._wrapped;

    // 仅当原始数组不为null/undefined时执行(避免空指针错误)
    if (obj != null) {
      // 调用原生数组方法,将当前方法的参数透传给原生方法,直接修改原数组
      // apply作用:绑定方法执行上下文为原始数组obj,参数以数组形式传递
      method.apply(obj, arguments);

      // 特殊边界处理:修复shift/splice清空数组后可能残留的索引0问题
      // 原因:部分场景下shift/splice把数组清空(length=0)后,obj[0]仍可能残留undefined
      // 删除索引0可保证空数组的结构完全干净,符合原生数组的预期行为
      if ((name === 'shift' || name === 'splice') && obj.length === 0) {
        delete obj[0];
      }
    }

    // 返回链式调用结果:保证调用该方法后仍能继续调用包装类的其他方法
    // chainResult会根据是否开启链式调用,返回包装类实例(this)或修改后的原数组(obj)
    return chainResult(this, obj);
  };
});

function chainResult(instance, obj) {
    return instance._chain ? _$1(obj).chain() : obj;
}

function chain(obj) {
    var instance = _$1(obj);
    instance._chain = true;
    return instance;
}

访问方法(Accessor)

concat/join/slice 这些方法的核心是返回新结果,原数组完全不变(比如 concat 拼接后返回新数组,原数组还是原样;slice 截取后返回新子数组)。

// 假设包装类实例:_([1,2,3])
const arrWrapper = _([1,2,3]);

// 2. 调用accessor方法concat
const newWrapper = arrWrapper.concat([5,6]);
console.log(arrWrapper._wrapped); // [1,2,3,4](原数组仍不变)
console.log(newWrapper._wrapped); // [1,2,3,4,5,6](新结果)
// 批量为自定义数组包装类的原型挂载数组非可变方法(不会修改原数组,返回新值)
// 目标:让自定义包装类实例调用这些方法时,获取原生方法的返回结果,并保持链式调用特性
each(['concat', 'join', 'slice'], function(name) {
  // 从数组原型(ArrayProto)中获取对应的原生方法(如Array.prototype.concat)
  // ArrayProto是Array.prototype的简写,常见于Underscore/Lodash等工具库
  var method = ArrayProto[name];

  // 为自定义数组包装类(_$1)的原型挂载当前遍历的方法
  _$1.prototype[name] = function() {
    // 获取包装类实例中包裹的原始数组/值(_wrapped是包装类存储原始数据的核心属性)
    var obj = this._wrapped;

    // 仅当原始数据不为null/undefined时执行原生方法(避免空指针错误)
    if (obj != null) {
      // 调用原生方法,透传参数并接收返回值
      // 核心差异:这类方法不修改原数组,而是返回新值,因此需要用新值覆盖obj
      obj = method.apply(obj, arguments);
    }

    // 返回链式调用结果:将新的返回值(obj)传入chainResult,保证链式调用的正确性
    // 若开启链式则返回包装类实例,未开启则返回新的数组/值
    return chainResult(this, obj);
  };
});

链式调用实现

Underscore.js 的链式调用是其一大特色,通过以下机制实现:

调用 _.chain() 后,所有方法执行完都会通过 chainResult 返回「新的包装对象」(而非原始数据),因此可以继续调用原型上的方法;直到调用 value() 方法(需补充实现),取出 _wrapped 里的原始数据,结束链式。

举个栗子

看下链式调用如何工作:

// 链式调用示例:过滤出大于2的数,再乘以2,最后获取结果
var finalResult = _.chain([1, 2, 3, 4])
  .filter(function(x) { return x > 2; })
  .map(function(x) { return x * 2; })
  .value();

console.log(finalResult); // 输出:[6, 8]

代码实现

下面看下核心代码是怎么实现的吧 ~

// 核心包装类 _$1(对应 Underscore 的 _ 函数)
function _$1(obj) {
  // 如果是 _$1 实例,直接返回
  if (obj instanceof _$1) return obj;
  // 如果不是实例,创建实例并存储原始数据
  if (!(this instanceof _$1)) return new _$1(obj);
  this._wrapped = obj; // 存储被包装的原始数据(数组/对象)
  this._chain = false; // 链式标记,默认关闭
}

// 结束链式:获取最终结果
_$1.prototype.value = function() {
  return this._wrapped; // 取出包装对象里的原始数据
};

function chain(obj) {
  var instance = _$1(obj);
  instance._chain = true; // 开启链式标记
  return instance;
}

function chainResult(instance, obj) {
  // 关键判断:如果开启链式,返回新的包装对象(继续链式);否则返回原始数据
  return instance._chain ? _$1(obj).chain() : obj;
}

// 模拟 Underscore 的 each/functions 工具函数(简化版)
function each(arr, callback) {
  for (var i = 0; i < arr.length; i++) callback(arr[i], i);
}
function functions(obj) {
  return Object.keys(obj).filter(key => typeof obj[key] === 'function');
}

function mixin(obj) {
  each(functions(obj), function(name) {
    var func = _$1[name] = obj[name]; // 挂载到 _$1 静态方法
    _$1.prototype[name] = function() {
      // 1. 构造参数:第一个参数是包装的原始数据 this._wrapped,后续是方法入参
      var args = [this._wrapped];
      push.apply(args, arguments);
      // 2. 执行工具函数,得到结果
      var result = func.apply(_$1, args);
      // 3. 调用 chainResult,决定返回包装对象(链式)还是原始数据
      return chainResult(this, result);
    };
  });
  return _$1;
}

// 挂载常用工具方法(模拟 Underscore 的 filter/map)
mixin({
  filter: function(arr, fn) {
    return arr.filter(fn);
  },
  map: function(arr, fn) {
    return arr.map(fn);
  }
});

// 给 _$1 原型挂载 chain 方法(对应用户代码里的 instance.chain())
_$1.prototype.chain = function() {
  return chain(this._wrapped);
};

核心设计特点

函数式编程风格

Underscore.js 采用函数式编程范式,提供了大量高阶函数:

  • 纯函数 :如 map 、 filter 等,不修改原数据,避免污染原数据

  • 函数工厂 :如 tagTester 、 createPredicateIndexFinder 等。会返回一个新函数的函数,用于复用函数逻辑,减少重复代码

    // 函数工厂:生成检测特定类型的函数
    const tagTester = function(tag) {
      // 返回新函数(检测类型)
      return function(obj) {
        return Object.prototype.toString.call(obj) === `[object ${tag}]`;
      };
    };
    
    // 生产具体的检测函数
    _.isArray = tagTester('Array');
    _.isObject = tagTester('Object');
    _.isFunction = tagTester('Function');
    
    // 使用
    console.log(_.isArray([1,2])); // true
    console.log(_.isObject({a:1})); // true
    
  • 函数组合 :如 compose 函数,将多个函数组合成一个新函数,执行顺序为 “从右到左”,前一个函数的输出作为后一个函数的输入

    // 函数组合核心实现
    _.compose = function(...funcs) {
      return function(...args) {
        // 从右到左执行函数
        return funcs.reduceRight((result, func) => [func.apply(this, result)], args)[0];
      };
    };
    
    // 示例:先过滤大于2的数,再乘以2,最后求和
    const filterBig = arr => _.filter(arr, x => x > 2);
    const double = arr => _.map(arr, x => x * 2);
    const sum = arr => _.reduce(arr, (a, b) => a + b, 0);
    
    // 组合函数:sum(double(filterBig(arr)))
    const process = _.compose(sum, double, filterBig);
    
    console.log(process([1,2,3,4])); // (3,4)→[6,8]→14
    
  • 函数柯里化 :如 partial 函数,将多参数函数拆解为单参数函数链,可分步传参,延迟执行。

    // 柯里化核心实现(简化版)
    _.partial = function(func, ...fixedArgs) {
      return function(...remainingArgs) {
        // 合并固定参数和剩余参数,执行原函数
        return func.apply(this, fixedArgs.concat(remainingArgs));
      };
    };
    
    // 示例:固定乘法的第一个参数为2(创建“乘以2”的函数)
    const multiply = (a, b) => a * b;
    const double = _.partial(multiply, 2);
    
    // 分步传参:先传2,后传3/4
    console.log(double(3)); // 6
    console.log(double(4)); // 8
    

跨环境兼容性

Underscore.js 设计了完善的跨环境兼容机制,核心是 先检测、后适配、再降级 的策略:

  • 环境检测 :自动检测运行环境(浏览器、Node.js)
  • 特性检测 :检测原生方法是否存在
  • 优雅降级 :当原生方法不可用时,使用自定义实现
  • IE 兼容性 :特别处理了 IE < 9 的兼容性问题

性能优化

Underscore.js 在设计中融入了多种性能优化策略:

  • 缓存 :如 memoize 函数,缓存计算结果

    // memoize 核心实现(简化版)
    _.memoize = function(func, hashFunction) {
      const cache = {}; // 缓存容器
      hashFunction = hashFunction || function(args) {
        return args[0]; // 默认用第一个参数作为缓存key
      };
    
      return function(...args) {
        const key = hashFunction.apply(this, args);
        // 缓存存在则直接返回,否则执行函数并缓存
        if (!cache.hasOwnProperty(key)) {
          cache[key] = func.apply(this, args);
        }
        return cache[key];
      };
    };
    
    // 示例:缓存斐波那契计算结果(避免重复递归)
    const fib = _.memoize(function(n) {
      return n < 2 ? n : fib(n - 1) + fib(n - 2);
    });
    
    console.log(fib(10)); // 55(首次计算,缓存结果)
    console.log(fib(10)); // 55(直接取缓存,无需计算)
    
  • 延迟执行 :如 debounce 、 throttle 函数

    • debounce(防抖) :延迟执行函数,若短时间内重复触发,重置延迟(如搜索框输入、窗口 resize);
    • throttle(节流) :限制函数在指定时间内仅执行一次(如滚动事件、按钮点击)。
    // 防抖核心实现(简化版)
    _.debounce = function(func, wait) {
      let timeoutId;
      return function(...args) {
        clearTimeout(timeoutId); // 重置延迟
        timeoutId = setTimeout(() => {
          func.apply(this, args);
        }, wait);
      };
    };
    
    // 示例:搜索框输入后500ms执行搜索
    const search = _.debounce(function(keyword) {
      console.log('搜索:', keyword);
    }, 500);
    
    // 快速输入时,仅最后一次输入后500ms执行
    search('a');
    search('ab');
    search('abc'); // 仅执行这一次
    
  • 惰性求值 :通过链式调用实现 链式调用时,并非每一步都立即计算,而是延迟到最后一步 value () 才执行最终计算,减少中间临时数据的生成:

    // 惰性求值示例:链式调用仅在value()时执行最终逻辑
    const result = _.chain([1,2,3,4])
      .filter(x => x > 2) // 暂存逻辑,不立即执行
      .map(x => x * 2)    // 暂存逻辑,不立即执行
      .value();           // 执行所有逻辑,返回结果 [6,8]
    
  • 原生方法优先 :当原生方法可用时,优先使用原生方法,JavaScript 原生方法(如 Array.prototype.mapObject.keys)由引擎底层实现(C++),比纯 JS 实现快得多。

可扩展性

Underscore.js 设计了良好的扩展机制:

  • mixin 方法 :允许用户添加自定义函数,可将自定义函数挂载到 Underscore 原型上,支持链式调用:

    // 示例:自定义一个“求平方和”的方法,通过mixin挂载
    _.mixin({
      sumOfSquares: function(arr) {
        return _.reduce(arr, (sum, x) => sum + x * x, 0);
      }
    });
    
    // 直接调用 + 链式调用都支持
    console.log(_.sumOfSquares([1,2,3])); // 1+4+9=14
    
    const result = _.chain([1,2,3])
      .filter(x => x > 1) // [2,3]
      .sumOfSquares()     // 4+9=13
      .value();
    console.log(result); // 13
    
  • 自定义 iteratee :允许用户自定义迭代器行为

    // 示例:自定义迭代器,处理对象数组的特定属性
    const users = [
      { name: '张三', age: 20 },
      { name: '李四', age: 30 }
    ];
    
    // 自定义迭代器:提取age属性并判断是否大于25
    const ageIterator = user => user.age > 25;
    const result = _.filter(users, ageIterator);
    console.log(result); // [{ name: '李四', age: 30 }]
    
  • 模板系统 :支持自定义分隔符、变量插值规则,适配不同场景

    // 示例:自定义模板分隔符(默认是<% %>,改为{{ }})
    _.templateSettings = {
      evaluate: /{{(.+?)}}/g,    // 执行代码:{{ code }}
      interpolate: /{{=(.+?)}}/g // 插值:{{= value }}
    };
    
    // 使用自定义模板
    const template = _.template('Hello {{= name }}! {{ if (age > 18) { }}成年{{ } else { }}未成年{{ } }}');
    const html = template({ name: '张三', age: 20 });
    console.log(html); // Hello 张三! 成年
    

缺点

性能层面的损耗

链式调用的额外开销

Underscore 链式调用依赖每次方法调用创建新的 _$1 包装对象,且需通过 value() 触发最终计算:

  • 对象创建成本:每一步链式操作(如 map()/filter())都会实例化新的包装对象,频繁操作大型数据集时,内存分配和垃圾回收开销显著;
  • 原型链查找损耗:包装对象的方法挂载在 _$1.prototype 上,每次调用需遍历原型链,效率低于原生方法的直接调用;
  • 对比示例_.chain([1,2,3]).map(x=>x*2).value() 比原生 [1,2,3].map(x=>x*2) 多了「包装对象创建→原型链查找→结果重新包装」三层开销。

具体函数实现的效率瓶颈

  • 类型检测冗余:早期版本未优先使用原生 Array.isArray(),而是通过 Object.prototype.toString.call() 做类型判断,效率比原生 API 低 30% 以上;
  • 遍历策略不优:统一用通用遍历逻辑处理数组 / 对象,未针对数组使用更高效的 for 循环(而非 for...in),对象遍历未优先用 Object.keys() 过滤原型属性;
  • 高阶函数调用栈开销map/filter 等方法的迭代器需通过 optimizeCb 封装闭包,每次迭代都会产生函数调用栈损耗,而原生方法由引擎内联优化,无此开销。

闭包与内存占用

Underscore 基于 IIFE 封装核心逻辑,闭包会长期持有内部变量(如 _ 构造函数、mixin 缓存、工具函数):

  • 即使仅使用 _.isArray() 一个方法,整个闭包内的所有变量也无法被垃圾回收,造成内存冗余;
  • 非模块化环境下,全局挂载的 _ 变量常驻内存,进一步增加无意义的内存占用。

API 设计层面:一致性与易用性缺陷

双模式调用的混淆性

同时支持「函数式调用(_.map())」和「对象链式调用(_().map())」,带来双重问题:

  • 学习成本高:新手需理解两种模式的底层差异(如链式模式依赖 _wrapped 包装数据,函数式模式直接传参);
  • 行为不一致风险:部分方法在两种模式下参数传递有细微差异(如 _.reduce() 链式调用时 this 指向包装对象,函数式调用时需手动传 context)。

参数与行为的不一致性

  • 参数顺序混乱_.reduce(collection, iteratee, [accumulator]) 与原生 Array.prototype.reduce(callback, [initialValue]) 参数顺序相反,用户切换使用时易出错;
  • 边界处理不统一:对 null/undefined/ 空对象的处理逻辑混乱(如 _.map(null) 返回 []_.keys(null) 抛出错误);
  • 可选参数模糊_.defaults()/_.extend() 对默认值、浅拷贝的规则未明确标注,导致相同输入可能产生不同预期结果。

功能覆盖的冗余与缺失

  • 冗余覆盖:部分方法(如 _.each())仅对原生方法做简单封装,无额外价值却增加调用层级;
  • 核心功能缺失:早期版本无原生的深拷贝方法(_.cloneDeep() 为后期补充),需手动嵌套 _.extend() 实现,易用性差。

生态兼容层面:与现代开发体系脱节

模块化适配严重不足

  • 无原生 ES 模块支持:仅通过 IIFE 兼容 CommonJS/AMD,无法直接使用 import { map } from 'underscore' 按需导入;
  • 树摇(Tree-shaking)失效:模块结构设计导致现代打包工具(Webpack/Rollup)无法移除未使用的函数,即使仅用 _.isArray(),也会打包整个库(约 5KB),而原生 Array.isArray() 无体积成本;
  • 对比 Lodash-eslodash-es/map 可按需导入,体积仅几百字节,Underscore 无此能力。

现代语法与工具链适配差

  • 旧语法的陈旧性:依赖构造函数 + 原型链实现包装对象(_$1.prototype[name] = ...),与 ES6 class 语法脱节,现代开发者可读性差;
  • 箭头函数冲突:链式调用依赖 this 指向包装对象,而箭头函数的词法 this 会导致 this._wrapped 报错,增加使用复杂度;
  • 框架集成不契合:在 React/Vue 等现代框架中,其函数式风格与框架响应式系统(如 Vue 的 ref/reactive)适配性差,不如 Lodash/Ramda 灵活。

类型系统支持缺失

  • 无内置 TypeScript 类型:完全依赖第三方 @types/underscore,存在类型覆盖不全(如链式调用返回类型推断错误)、版本不匹配(库更新后类型定义滞后)等问题;
  • 类型安全不足:方法参数 / 返回值无类型约束,运行时易因类型错误导致 bug,而现代库(如 Lodash)原生支持 TS 类型。

扩展性与维护层面:原型链设计的硬伤

扩展机制的局限性

  • 原型污染风险mixin 方法直接挂载函数到 _$1.prototype,若自定义方法名与内置方法冲突(如自定义 map),会覆盖原生逻辑,导致意外行为;
  • 无结构化插件系统:扩展方式仅依赖 mixin,无法像 Vue/React 那样通过插件注册、生命周期管理复杂扩展,生态扩展性差。

代码结构与维护成本

  • 闭包嵌套复杂:核心逻辑通过多层闭包封装,早期版本包含大量 “魔法逻辑”(如 optimizeCb 优化迭代器),代码可读性极低;
  • 测试覆盖不全面:虽有基础测试用例,但跨环境(如旧版 IE)、边界场景(如空值 / 超大数组)的测试覆盖不足,修复 bug 易引入新问题;
  • 兼容负担重:为适配 IE6+ 等老旧环境,保留大量冗余的兼容代码,无法精简核心逻辑。

功能设计层面:能力不足与场景覆盖不全

异步操作支持缺失

  • 无原生支持 Promise/async/await,处理异步数据流(如接口请求→数据处理)时,需手动封装 _.map + Promise.all,代码冗余;
  • 防抖 / 节流函数(debounce/throttle)仅支持同步逻辑,无法处理异步回调的时序问题。

对象操作能力有限

  • 浅拷贝局限_.extend()/_.defaults() 仅支持浅拷贝,深度拷贝需手动实现或依赖第三方扩展,而原生 structuredClone() 或 Lodash _.cloneDeep() 已原生支持;
  • 对象遍历低效:无针对嵌套对象的遍历方法(如 _.deepKeys),处理复杂对象需多层嵌套调用。

函数式特性不完整

  • 柯里化 / 组合能力弱:仅通过 _.partial() 模拟柯里化(无法自动柯里化多参数函数),_.compose() 仅支持同步函数组合,无异步组合能力;
  • 惰性求值不彻底:链式调用虽有惰性特征,但仅在 value() 时执行,无法像 Ramda 那样实现 “按需计算”,处理超大数据集时效率低。

安全性层面:潜在的风险隐患

原型污染风险

早期版本的 _.extend()/_.defaults() 未过滤 __proto__ 属性,若传入包含 __proto__: { evil: true } 的用户输入,会修改 Object.prototype,导致全局原型污染:

// 原型污染示例(旧版本 Underscore)
const obj = {};
_.extend(obj, { __proto__: { test: 123 } });
console.log({}.test); // 123(全局原型被污染)

模板注入隐患

_.template() 方法默认使用 eval 执行模板中的代码,若未过滤用户输入的模板字符串,易引发代码注入攻击:

// 模板注入风险
const userInput = "{{= alert('XSS') }}";
const template = _.template(userInput);
template(); // 执行恶意代码

时代适配层面:原生 ES6+ 的全面替代

核心功能被原生方法覆盖

ES6+ 引入的原生 API 完全覆盖 Underscore 核心能力,且性能更优(引擎级优化):

Underscore 方法 原生替代方案 优势
_.map() Array.prototype.map() 无包装对象开销,引擎内联优化
_.keys() Object.keys() 原生实现,效率更高
_.extend() Object.assign() 原生支持,无需额外依赖
_.debounce() 浏览器原生 requestIdleCallback(或框架内置) 更贴合现代浏览器调度机制

旧语法与现代开发习惯脱节

  • 依赖 arguments 对象处理参数(如 _.partial()),而现代 JS 已支持剩余参数(...args),代码更简洁;
  • 构造函数 + 原型链的实现方式,与现代开发者熟悉的 class 语法相悖,学习和维护成本高。

核心总结

Underscore.js 的所有缺点本质是 “早期设计无法适配现代 JavaScript 生态”

  1. 性能层面:链式调用、闭包、低效实现带来多维度损耗,无法与原生引擎优化的 API 竞争;
  2. 生态层面:模块化、类型系统、现代工具链适配不足,无法满足现代工程化开发需求;
  3. 功能层面:异步、深拷贝、函数式特性的缺失,无法覆盖复杂业务场景;
  4. 安全 / 维护层面:原型污染、代码复杂、测试不足,增加生产环境风险。
❌
❌