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 方法提供了核心实现。

相关推荐
程序员大澈6 分钟前
3个 Vue Scoped 的核心原理
javascript·vue.js
hyyyyy!9 分钟前
《原型链的故事:JavaScript 对象模型的秘密》
javascript·原型模式
程序员大澈19 分钟前
3个好玩且免费的api接口
javascript·vue.js
程序员大澈1 小时前
4个 Vue 路由实现的过程
javascript·vue.js·uni-app
几度泥的菜花1 小时前
如何禁用移动端页面的多点触控和手势缩放
前端·javascript
狼性书生1 小时前
electron + vue3 + vite 渲染进程到主进程的双向通信
前端·javascript·electron
肥肠可耐的西西公主1 小时前
前端(AJAX)学习笔记(CLASS 4):进阶
前端·笔记·学习
拉不动的猪1 小时前
Node.js(Express)
前端·javascript·面试
Re.不晚1 小时前
Web前端开发——HTML基础下
前端·javascript·html
几何心凉1 小时前
如何处理前端表单验证,确保用户输入合法?
前端·css·前端框架