功能概述
isArguments 函数是 Lodash 中用于检测给定值是否为 arguments 对象的工具函数。它通过组合标签检测和特性检测两种策略,确保在不同 JavaScript 环境下都能准确识别 arguments 对象。
源码实现
var isArguments = baseIsArguments(
(function () {
return arguments;
})()
)
? baseIsArguments
: function (value) {
return (
isObjectLike(value) &&
hasOwnProperty.call(value, "callee") &&
!propertyIsEnumerable.call(value, "callee")
);
};
function baseIsArguments(value) {
return isObjectLike(value) && baseGetTag(value) == argsTag;
}
实现原理解析
1. 两种判断方式的巧妙结合
来看看这段代码:
var isArguments = baseIsArguments(
(function () {
return arguments;
})()
)
? baseIsArguments
: function (value) {
// 降级实现...
};
这里的思路其实很巧妙:
- 首先测试基于标签的检测方法(baseIsArguments)是否在当前环境有效
- 如果有效,则使用更高效的标签检测
- 如果无效,则降级使用基于特性的检测
其中,baseIsArguments(function() { return arguments; }()) 这段代码的实现原理是:
function() { return arguments; }定义了一个返回 arguments 对象的匿名函数- 通过立即执行函数
()调用这个匿名函数,获得一个真实的 arguments 对象 - 将这个真实的 arguments 对象传入 baseIsArguments 函数进行测试
- 如果 baseIsArguments 能够正确识别这个真实的 arguments 对象(返回 true),说明当前环境支持通过标签检测识别 arguments 对象
- 这种运行时的环境检测确保了在不同的 JavaScript 引擎中都能选择最合适的实现方式
示例:
// 创建一个真实的 arguments 对象进行测试
function testFunc() {
return arguments;
}
var realArgs = testFunc(1, 2, 3);
// 在大多数现代浏览器中,这里会使用 baseIsArguments
console.log(isArguments(realArgs)); // true
2. 标签检测方式(baseIsArguments)
function baseIsArguments(value) {
return isObjectLike(value) && baseGetTag(value) == argsTag;
}
这个检测方式分两步走:
2.1 先看看是不是像对象
isObjectLike(value);
这一步就是确保这个值看起来像个对象(不是 null,typeof 得到 'object')。来看几个例子:
// 这些都不是 arguments,肯定返回 false
isArguments(null); // false
isArguments(undefined); // false
isArguments(42); // false
isArguments("string"); // false
2.2 再看看标签对不对
baseGetTag(value) == argsTag;
这步是通过 Object.prototype.toString 看看这个对象的内部标签是不是 '[object Arguments]':
// 在支持标准 arguments 对象的环境中
function foo() {
console.log(Object.prototype.toString.call(arguments)); // '[object Arguments]'
}
foo(1, 2, 3);
3. 特征检测方式(备选方案)
function(value) {
return isObjectLike(value) && hasOwnProperty.call(value, 'callee') &&
!propertyIsEnumerable.call(value, 'callee');
}
这个方案就是看看这个值是不是具备 arguments 对象的特征,主要检查三点:
3.1 还是先看看像不像对象
isObjectLike(value);
跟前面一样,确保是个对象类型的值。
3.2 看看有没有 callee 属性
hasOwnProperty.call(value, "callee");
arguments 对象有个特别的属性叫 callee,指向当前正在执行的函数:
// arguments 对象特有的 callee 属性
function example() {
console.log(arguments.callee === example); // true
}
example();
// 普通对象没有 callee 属性
var obj = { length: 3, 0: "a", 1: "b", 2: "c" };
console.log("callee" in obj); // false
3.3 确认 callee 属性不能被遍历
!propertyIsEnumerable.call(value, "callee");
真正的 arguments 对象的 callee 属性是不能被 for...in 遍历的:
// 原生 arguments 对象的 callee 属性不可枚举
function test() {
for (var prop in arguments) {
console.log(prop); // 不会输出 'callee'
}
console.log(Object.propertyIsEnumerable.call(arguments, "callee")); // false
}
test(1, 2, 3);
// 模拟的类 arguments 对象可能有可枚举的 callee
var fakeArgs = { length: 3, 0: "a", 1: "b", 2: "c", callee: function () {} };
console.log(Object.propertyIsEnumerable.call(fakeArgs, "callee")); // true
总结
isArguments 它用了两种方式来识别 arguments 对象:一种是看对象的标签,另一种是看对象的特征。这样不管在什么环境下都能准确判断。既保证了兼容性,又没有变得很复杂。