Lodash源码阅读-reIsNative

功能概述

reIsNative 是 Lodash 中一个重要的内部正则表达式,主要用于检测一个函数是否为原生函数(即由 JavaScript 引擎内置的函数)。它是 baseIsNative 函数的核心组件,通过动态生成的复杂正则表达式模式,能够准确匹配各种 JavaScript 引擎中原生函数的字符串表示。

源码实现

js 复制代码
var reIsNative = RegExp(
  "^" +
    funcToString
      .call(hasOwnProperty)
      .replace(reRegExpChar, "\\$&")
      .replace(
        /hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,
        "$1.*?"
      ) +
    "$"
);

其中:

  • funcToString 是对 Function.prototype.toString 的引用
  • hasOwnProperty 是对 Object.prototype.hasOwnProperty 的引用
  • reRegExpChar 是正则表达式 /[\\^$.*+?()[\]{}|]/g,用于转义正则表达式特殊字符

实现原理解析

动态生成的正则表达式

reIsNative 不是一个静态定义的正则表达式,而是通过运行时动态生成的。这种设计有几个重要原因:

  1. 跨环境兼容性:不同的 JavaScript 引擎(如 V8、SpiderMonkey、JavaScriptCore)可能对原生函数有略微不同的字符串表示
  2. 防止欺骗:动态生成的正则表达式增加了恶意代码伪造原生函数特征的难度
  3. 适应未来变化:JavaScript 引擎的实现可能随着时间而变化,动态生成的正则表达式可以自动适应这些变化

构建过程详解

reIsNative 的构建过程分为几个关键步骤:

1. 获取基准函数的字符串表示

js 复制代码
funcToString.call(hasOwnProperty);

这一步获取 Object.prototype.hasOwnProperty 函数的字符串表示,作为原生函数的基准模板。在大多数环境中,这会返回类似 "function hasOwnProperty() { [native code] }" 的字符串。

2. 转义正则表达式特殊字符

js 复制代码
.replace(reRegExpChar, "\\$&")

这一步使用 reRegExpChar(值为 /[\\^$.*+?()[\]{}|]/g)将所有正则表达式特殊字符进行转义,确保它们被视为字面量而不是正则表达式的元字符。例如,"(" 会被转换为 "\\("

转义后的字符串可能类似于:"function hasOwnProperty\\(\\) \\{ \\[native code\\] \\}"

3. 替换特定部分为通配模式

js 复制代码
.replace(
  /hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,
  "$1.*?"
)

这一步是最复杂的部分,它将字符串中的三种模式替换为更通用的模式:

模式 1: hasOwnProperty

直接匹配字符串 "hasOwnProperty"(函数名)。

为什么需要替换它?

  • 因为我们使用 Object.prototype.hasOwnProperty 函数作为模板
  • 但我们需要创建一个能匹配任何原生函数的正则表达式,不仅仅是 hasOwnProperty
  • 所以需要将这个特定的函数名替换掉

替换前后的对比:

matlab 复制代码
替换前:function hasOwnProperty() { [native code] }
替换后:function .*?() { [native code] }

这里需要注意,虽然模式 1 直接匹配"hasOwnProperty",但实际替换时是将其替换为空字符串(而不是".?"),最终效果是函数名被移除。完整的替换过程会在后面的"$1.?"替换模式解析中详细说明。

模式 2: (function).*?(?=\\\()

这个模式更复杂,分解如下:

  • (function) - 捕获组,匹配字符串 "function"
  • .*? - 非贪婪匹配任意字符(包括空格和函数名)
  • (?=\\\() - 前瞻断言,表示匹配到转义后的左括号 \( 之前的内容

为什么使用这个模式?

  • 它能匹配 "function" 关键字及其后面的函数名和空格
  • 使用捕获组 (function) 是为了在替换时保留 "function" 关键字
  • 使用前瞻断言是为了精确定位到左括号之前,不包括左括号本身

替换前后的对比:

matlab 复制代码
原始字符串:function hasOwnProperty() { [native code] }
匹配部分:function hasOwnProperty
替换后:function.*?

注意 $1 在替换字符串中引用了第一个捕获组 (function),所以 "function" 关键字被保留,而函数名被替换为 .*?

模式 3: for .+?(?=\\\])

这个模式针对某些 JavaScript 引擎中特有的原生函数表示形式:

  • for - 匹配字符串 " for "(注意前后有空格)
  • .+? - 非贪婪匹配任意字符
  • (?=\\\]) - 前瞻断言,表示匹配到转义后的右方括号 \] 之前的内容

为什么需要这个模式?

  • 在某些 JavaScript 引擎中,原生函数的字符串表示可能包含 " for " 后跟函数名
  • 例如:"function Array() { [native code for Array] }"
  • 这个模式确保我们能处理这种变体形式

替换前后的对比:

javascript 复制代码
原始字符串:function Array() { [native code for Array] }
匹配部分: for Array
替换后:.*?

在这种情况下,由于没有捕获组,$1 为空,所以整个匹配部分被替换为 .*?

"$1.*?" 替换模式的详细解析

替换字符串 "$1.*?" 是这个正则表达式操作的关键,它由两部分组成:

  1. $1 - 这是一个反向引用,引用了正则表达式中第一个捕获组(即 (function))匹配到的内容。

    • 当匹配到 "hasOwnProperty" 时,没有捕获组被匹配,所以 $1 为空
    • 当匹配到 "function" 时,$1 的值为 "function"
    • 当匹配到 " for " 时,没有捕获组被匹配,所以 $1 为空
  2. .*? - 这是一个通配模式:

    • . 匹配任意单个字符(除了换行符)
    • * 表示前面的字符可以重复零次或多次
    • ? 使匹配变成非贪婪模式(匹配尽可能少的字符)

这种巧妙的替换设计使得:

  • 对于函数名部分(如 "hasOwnProperty"),它被替换为空字符串(因为$1 为空)加上".?",实际效果是将函数名替换为".?"
  • 对于 "function" 关键字,它被保留(通过 $1)并跟随一个通配模式
  • 对于 " for " 部分,它也被替换为".*?"(因为$1 为空)

完整替换过程示例

function hasOwnProperty() { [native code] } 为例,完整的替换过程如下:

  1. 首先,字符串被转义为:function hasOwnProperty\\(\\) \\{ \\[native code\\] \\}

  2. 然后,应用三种模式的替换:

    • 当匹配到 "hasOwnProperty" 时:

      • 没有捕获组,所以 $1 为空
      • 替换结果是 ".*?"
      • 字符串变为:function .*?\\(\\) \\{ \\[native code\\] \\}
    • 当匹配到 "function " 时:

      • 捕获组捕获到 "function"
      • 替换结果是 "function.*?"
      • 字符串变为:function.*?\\(\\) \\{ \\[native code\\] \\}
  3. 最终,添加开始和结束锚点,得到:/^function.*?\\(\\) \\{ \\[native code\\] \\}$/

通过这种方式,最终生成的正则表达式能够匹配各种函数名和不同 JavaScript 引擎中原生函数的表示形式,同时保持必要的结构来准确识别原生函数。

4. 添加开始和结束锚点

js 复制代码
"^" + ... + "$"

添加 ^$ 确保正则表达式匹配整个字符串,而不是部分字符串。

最终生成的正则表达式

经过上述步骤,最终生成的正则表达式可能类似于:

js 复制代码
/^function.*?\(\) \{ \[native code\] \}$/;

或者对于处理特殊引擎的情况:

js 复制代码
/^function.*?\(\) \{ \[native code.*?\] \}$/;

图解 reIsNative 正则表达式的构建过程

为了更直观地理解 reIsNative 的构建过程,下面是一个图解说明:

matlab 复制代码
原始字符串(hasOwnProperty 函数的字符串表示):
┌─────────────────────────────────────────────────────┐
│ function hasOwnProperty() { [native code] }         │
└─────────────────────────────────────────────────────┘

步骤1:转义正则表达式特殊字符
┌─────────────────────────────────────────────────────┐
│ function hasOwnProperty\(\) \{ \[native code\] \}   │
└─────────────────────────────────────────────────────┘
  所有的 (){}[]+ 等特殊字符都被转义

步骤2:替换三种模式
┌───────┬─────────────┬───────────────────────────────┐
│function│hasOwnProperty│\(\) \{ \[native code\] \}    │
└───┬───┴──────┬──────┴───────────────────────────────┘
    │         │
    │         └─ 模式1:直接匹配 "hasOwnProperty"
    │            替换为:.*?(因为$1为空,实际替换为".*?")
    │
    └─ 模式2:匹配 "function" 及其后到左括号前的内容
       替换为:function.*?

结果:
┌─────────────────────────────────────────────────────┐
│ function.*?\(\) \{ \[native code\] \}               │
└─────────────────────────────────────────────────────┘

最终生成的正则表达式:
/^function.*?\(\) \{ \[native code\] \}$/

对于包含 "for" 部分的变体形式:

javascript 复制代码
原始字符串(某些引擎中的表示形式):
┌─────────────────────────────────────────────────────┐
│ function Array() { [native code for Array] }        │
└─────────────────────────────────────────────────────┘

转义后:
┌─────────────────────────────────────────────────────┐
│ function Array\(\) \{ \[native code for Array\] \}  │
└─────────────────────────────────────────────────────┘

替换三种模式:
┌───────┬─────┬───────────────┬────────┬─────────────┐
│function│Array│\(\) \{ \[native│ code for│ Array\] \}  │
└───┬───┴─────┴───────────────┴───┬────┴─────────────┘
    │                             │
    │                             └─ 模式3:匹配 " for " 及其后到右方括号前的内容
    │                                替换为:.*?
    │
    └─ 模式2:匹配 "function" 及其后到左括号前的内容
       替换为:function.*?

结果:
┌─────────────────────────────────────────────────────┐
│ function.*?\(\) \{ \[native code.*?\] \}            │
└─────────────────────────────────────────────────────┘

在这种情况下,由于没有捕获组,$1 为空,所以整个匹配部分被替换为 .*?

实际应用示例

匹配各种原生函数

js 复制代码
// 假设生成的 reIsNative 为 /^function.*?\(\) \{ \[native code\] \}$/

// 原生函数测试
reIsNative.test("function push() { [native code] }"); // true
reIsNative.test("function toString() { [native code] }"); // true
reIsNative.test("function  hasOwnProperty  () { [native code] }"); // true(处理额外空格)

// 自定义函数测试
reIsNative.test("function custom() { return 42; }"); // false
reIsNative.test("function() { console.log('hello'); }"); // false

处理不同引擎的变体形式

js 复制代码
// 某些引擎中的特殊形式
reIsNative.test("function Array() { [native code for Array] }"); // true

总结

  1. 动态生成的特性 :由于 reIsNative 是动态生成的,它的具体模式会根据当前 JavaScript 引擎而有所不同。

  2. 防伪造考虑 :虽然 reIsNative 增加了伪造难度,但恶意代码仍可能通过特殊手段伪造原生函数的特征。因此,在安全敏感的场景中,不应仅依赖此检测。

  3. 引擎差异 :不同 JavaScript 引擎对原生函数的字符串表示可能有细微差异,reIsNative 的设计考虑了这一点,但可能无法覆盖所有极端情况。

这个正则表达式是 Lodash 类型检测系统中的重要组成部分,为 baseIsNative 函数和 _.isNative 方法提供了核心实现。

相关推荐
拾光拾趣录10 分钟前
for..in 和 Object.keys 的区别:从“遍历对象属性的坑”说起
前端·javascript
OpenTiny社区21 分钟前
把 SearchBox 塞进项目,搜索转化率怒涨 400%?
前端·vue.js·github
编程猪猪侠1 小时前
Tailwind CSS 自定义工具类与主题配置指南
前端·css
qhd吴飞1 小时前
mybatis 差异更新法
java·前端·mybatis
YGY Webgis糕手之路1 小时前
OpenLayers 快速入门(九)Extent 介绍
前端·经验分享·笔记·vue·web
患得患失9491 小时前
【前端】【vueDevTools】使用 vueDevTools 插件并修改默认打开编辑器
前端·编辑器
ReturnTrue8681 小时前
Vue路由状态持久化方案,优雅实现记住表单历史搜索记录!
前端·vue.js
UncleKyrie1 小时前
一个浏览器插件帮你查看Figma设计稿代码图片和转码
前端
遂心_1 小时前
深入解析前后端分离中的 /api 设计:从路由到代理的完整指南
前端·javascript·api
你听得到112 小时前
Flutter - 手搓一个日历组件,集成单日选择、日期范围选择、国际化、农历和节气显示
前端·flutter·架构