功能概述
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
不是一个静态定义的正则表达式,而是通过运行时动态生成的。这种设计有几个重要原因:
- 跨环境兼容性:不同的 JavaScript 引擎(如 V8、SpiderMonkey、JavaScriptCore)可能对原生函数有略微不同的字符串表示
- 防止欺骗:动态生成的正则表达式增加了恶意代码伪造原生函数特征的难度
- 适应未来变化: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 - 这是一个反向引用,引用了正则表达式中第一个捕获组(即
(function)
)匹配到的内容。- 当匹配到 "hasOwnProperty" 时,没有捕获组被匹配,所以 $1 为空
- 当匹配到 "function" 时,$1 的值为 "function"
- 当匹配到 " for " 时,没有捕获组被匹配,所以 $1 为空
-
.*? - 这是一个通配模式:
.
匹配任意单个字符(除了换行符)*
表示前面的字符可以重复零次或多次?
使匹配变成非贪婪模式(匹配尽可能少的字符)
这种巧妙的替换设计使得:
- 对于函数名部分(如 "hasOwnProperty"),它被替换为空字符串(因为$1 为空)加上".?",实际效果是将函数名替换为".?"
- 对于 "function" 关键字,它被保留(通过 $1)并跟随一个通配模式
- 对于 " for " 部分,它也被替换为".*?"(因为$1 为空)
完整替换过程示例
以 function hasOwnProperty() { [native code] }
为例,完整的替换过程如下:
-
首先,字符串被转义为:
function hasOwnProperty\\(\\) \\{ \\[native code\\] \\}
-
然后,应用三种模式的替换:
-
当匹配到 "hasOwnProperty" 时:
- 没有捕获组,所以 $1 为空
- 替换结果是 ".*?"
- 字符串变为:
function .*?\\(\\) \\{ \\[native code\\] \\}
-
当匹配到 "function " 时:
- 捕获组捕获到 "function"
- 替换结果是 "function.*?"
- 字符串变为:
function.*?\\(\\) \\{ \\[native code\\] \\}
-
-
最终,添加开始和结束锚点,得到:
/^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
总结
-
动态生成的特性 :由于
reIsNative
是动态生成的,它的具体模式会根据当前 JavaScript 引擎而有所不同。 -
防伪造考虑 :虽然
reIsNative
增加了伪造难度,但恶意代码仍可能通过特殊手段伪造原生函数的特征。因此,在安全敏感的场景中,不应仅依赖此检测。 -
引擎差异 :不同 JavaScript 引擎对原生函数的字符串表示可能有细微差异,
reIsNative
的设计考虑了这一点,但可能无法覆盖所有极端情况。
这个正则表达式是 Lodash 类型检测系统中的重要组成部分,为 baseIsNative
函数和 _.isNative
方法提供了核心实现。