有没有一种与环境无关的方法来检测JavaScript宿主对象?

9
我正在编写一个JavaScript堆栈跟踪库。该库需要检测特定对象或函数是由程序员创建还是作为环境的一部分存在(包括内置对象)。由于宿主对象的行为不可预测,因此它们变得有些棘手,所以我需要一种不受环境影响的方法来确定JavaScript中特定对象是否为宿主对象(参见ECMAScript 3-4.3.8)。然而,在其他项目中,将宿主对象与本地对象和原始值区分开对程序员很有用,特别是在无浏览器环境中,因此我希望专注于这一点,而不是我的库中宿主对象引起的问题或区分程序员创建的对象。
到目前为止,我只能想出依赖于运行JavaScript代码的环境的解决方案。例如:
// IE Only: does not implement valueOf() in Host Objects
var isHost = (typeof obj === 'object' && typeof obj.valueOf === 'undefined');

// Firefox Only: Host objects have own constructor
var isHost = (obj.constructor && obj.hasOwnProperty('constructor'));

我注意到jQuery自己的isPlainObject()方法也依赖于环境,并且逻辑相当复杂。
也许这是由于主机对象的本质(因为它们的行为是由环境定义的),但我想深入挖掘一下,看看是否有可能,并且想知道是否有人遇到过这个特定问题并准备好了解决方案。
那么,有人知道一个简单的平台无关的测试主机对象的解决方案吗?如果它可以在没有浏览器的环境中运行,例如Node或Rhino,那就更好了。
可能的方法(可能不起作用):
测试宿主对象的特性似乎是徒劳无功的,因为它们的行为没有明确定义,但是测试对象是否属于ES3规范可能是可行的。 我尝试使用Object.prototype.toString(),因为它被定义得非常明确,但结果并不确定,因为一些环境(即IE)选择为本地对象和宿主对象返回相同的值。 通过检查对象通过原型链的最终构造函数是否真的是Function的实例,可能可以实现这一点。

7
你试图解决的问题是什么? - Camilo Martin
1
我非常确定这在所有环境下都不起作用,但是您可以尝试Object.prototype.toString.call(obj)并查看是否返回[object Object] (这是纯对象/实例生成的内容)或类似[object HTMLBodyElement]的信息以确定它是否为主机对象。 - pimvdb
我真的怀疑你能找到一个跨平台的解决方案,但对于浏览器来说,pimvdb的建议是我会使用的。不过我有一个想法,我想在回答中发布它。 - Camilo Martin
你在主机对象方面遇到了什么问题? - Camilo Martin
@Steven de Salas:我刚注意到它在IE7上无法工作。 - pimvdb
显示剩余4条评论
5个回答

5
当你查看主机对象定义的定义“由主机环境提供的对象以完成ECMAScript执行环境”,就会很清楚,没有简单的方法来确定一个对象是主机还是本地的。
与本地对象不同,主机对象以实现特定的方式定义内部属性(例如[[Prototype]]、[[Class]]等)。这是因为规范允许它们这样做。然而,并没有“必须”要求主机对象在实现特定的方式下实现内部行为;这是一种“可以”的要求。因此我们不能依赖于这一点。这些对象可能会表现得“奇怪”,也可能不会。无法判断。
过去有过几次检测主机对象的尝试,但所有这些尝试都显然依赖于对某些环境的观察(其中之一是MSHTML DOM)-请记住,主机对象没有任何独特的模式/特征可供识别。Peter Michaux 在这里记录了大部分的推断(请看“Feature Testing a Host Object”部分)。臭名昭著的typeof ... == "unknown"来自于MSHTML DOM和基于其的宿主对象。请注意,Peter主要讨论了与浏览器脚本相关的主机对象,并将检查范围缩小到“这是一个主机方法吗?”,“这是一个主机集合对象吗?”等。
在某些环境中,主机对象不会从Object.prototype继承(使其易于检查),或者具有某些属性会抛出错误(例如IE中某些“接口”对象上的“prototype”),甚至在访问时本身会抛出错误。
你可能认为可以仅在规范中定义的对象之一中检查对象,如果不是,则将其视为主机。但这并不能真正帮助你;它只会给你非内置的对象。其中一些非标准对象仍然可能是本地的(这意味着它们会实现规范中描述的通常语义)。
你最好的选择是测试你的应用/脚本的特定行为,这可能会对主机对象敏感。这总是最安全的方式。你计划从对象中访问某些内容吗?删除对象中的某些内容?向对象添加内容?进行测试。看看它是否有效。如果没有——您可能正在处理主机对象。

嗨@kangax,虽然我同意您的大部分答案,但ECMAScript规范还声明:“任何不是本地的对象都是宿主对象。”,因此在检查它们不是原生对象(如规范中定义)时,理论上可以测试宿主对象。这里的诀窍是如何测试一个对象真正是本地的,因为环境可能会通过在宿主对象中复制许多这些行为来使其变得困难。 - Steven de Salas

4
这里是更新版本的isNative,它拒绝所有具有本地toString实现的对象,这解决了堆栈跟踪库的问题,但不能令人满意地回答此处发布的问题。对于后者来说,这种方法失败的地方在于它过滤掉了所有内置类型定义,例如ObjectDateStringMath等,它们本身不是主机对象。此外,此解决方案取决于环境如何输出本地/内置函数定义(必须包含“[native code]”才能使函数正常工作)。由于函数行为不同,因此已将其重命名为isUserObject
// USER OBJECT DETECTION

function isUserObject(obj) {

    // Should be an instance of an Object
    if (!(obj instanceof Object)) return false;

    // Should have a constructor that is an instance of Function
    if (typeof obj.constructor === 'undefined') return false;
    if (!(obj.constructor instanceof Function)) return false;

    // Avoid built-in functions and type definitions
    if (obj instanceof Function && 
      Function.prototype.toString.call(obj).indexOf('[native code]') > -1) 
          return false;

    return true;
}

// CHECK IF AN OBJECT IS USER-CREATED OR NOT

if (typeof myObject === 'object' || typeof myObject === 'function')
   alert(isUserObject(myObject) ? 'User Object' : 'Non-user Object'); 

这里有一个JsFiddle测试列表,可用于在各种浏览器中测试此功能。

// ASSERT HELPER FUNCTION

var n = 0;
function assert(condition, message) {
    n++;
    if (condition !== true) {
       document.write(n + '. FAILS: ' + (message || '(no message)') + '<br/>');
    } else {
       document.write(n + '. PASS: ' + (message || '(no message)') + '<br/>');
    }
}

// USER CREATED OBJECTS

assert(isUserObject({}), '{} -- Plain object');
assert(isUserObject(function() {}), 'function() {} -- Plain function');
assert(isUserObject([]), '[] -- Plain array');

assert(isUserObject(/regex/), '/regex/ - Native regex');
assert(isUserObject(new Date()), 'new Date() - Native date object through instantiation');

assert(isUserObject(new String('string')), 'new String("string") - Native string object through instantiation');
assert(isUserObject(new Number(1)), 'new Number(1) - Native number object through instantiation');
assert(isUserObject(new Boolean(true)), 'new Boolean(true) - Native boolean object through instantiation');
assert(isUserObject(new Array()), 'new Array() - Native array object through instantiation');
assert(isUserObject(new Object()), '{} -- new Object() - Native object through instantiation');
assert(isUserObject(new Function('alert(1)')), '{} -- Native function through instantiation');

// USER OBJECT INSTANTIATION AND INHERITANCE

var Animal = function() {};
var animal = new Animal();

var Dog = function() {};
Dog.prototype = animal;
var dog = new Dog();

assert(isUserObject(Animal), 'Animal -- User defined type');
assert(isUserObject(animal), 'animal -- Instance of User defined type');

assert(isUserObject(Dog), 'Dog -- User defined inherited type');
assert(isUserObject(dog), 'dog -- Instance of User defined inherited type');

// BUILT IN OBJECTS

assert(!isUserObject(Object), 'Object -- Built in');
assert(!isUserObject(Array), 'Array -- Built in');
assert(!isUserObject(Date), 'Date -- Built in');
assert(!isUserObject(Boolean), 'Boolean -- Built in');
assert(!isUserObject(String), 'String -- Built in');
assert(!isUserObject(Function), 'Function -- Built in');

// PRIMITIVE TYPES 

assert(!isUserObject('string'), '"string" - Primitive string');
assert(!isUserObject(1), '1 - Primitive number');
assert(!isUserObject(true), 'true - Primitive boolean');
assert(!isUserObject(null), 'null - Primitive null');
assert(!isUserObject(NaN), 'NaN - Primitive number NotANumber');
assert(!isUserObject(Infinity), 'Infinity - Primitive number Infinity');
assert(!isUserObject(undefined), 'undefined - Primitive value undefined');

// HOST OBJECTS

assert(!isUserObject(window), 'window -- Host object');
assert(!isUserObject(alert), 'alert -- Host function');
assert(!isUserObject(document), 'document -- Host object');
assert(!isUserObject(location), 'location -- Host object');
assert(!isUserObject(navigator), 'navigator -- Host object');
assert(!isUserObject(parent), 'parent -- Host object');
assert(!isUserObject(frames), 'frames -- Host object');​

所有这些测试都不可靠。尝试传递{__proto__:null}()=>'[native code]' - Hugh Allen

2

几乎解决

几乎成功让它工作了。

但是,这个解决方案有缺陷,因为主机对象有时与本地对象无法区分。当在Chrome上测试isNative(window.alert)时,以下代码会失败,因为webkit引擎定义了一个看起来与原生alert函数完全相同的alert函数。

它使用普通的ES3 JavaScript,并基于测试对象是否为本地(而不是Host对象)的方法。然而,根据ES3 Host对象的定义:"任何不是本地的对象都是host对象",这个函数可以用来检测host对象。

// ISNATIVE OBJECT DETECTION

function isNative(obj) {

    switch(typeof obj) {
        case 'number': case 'string': case 'boolean':
            // Primitive types are not native objects
            return false;
    }  

    // Should be an instance of an Object
    if (!(obj instanceof Object)) return false;

    // Should have a constructor that is an instance of Function
    if (typeof obj.constructor === 'undefined') return false;
    if (!(obj.constructor instanceof Function)) return false;

    return true;
}

// CHECK IF AN OBJECT IS HOST OR NATIVE

if (typeof myObject === 'object' || typeof myObject === 'function')
   alert(isNative(myObject) ? 'Native Object' : 'Host Object'); 

这里有一个JsFiddle测试列表,可以用于在IE / Firefox / Chrome中测试此内容。

由于代码非常基础,我没有测试非浏览器环境,但我认为不会有任何问题。

// ASSERT HELPER FUNCTION

var n = 0;
function assert(condition, message) {
    n++;
    if (condition !== true) {
       document.write(n + '. FAILS: ' + (message || '(no message)') + '<br/>');
    } else {
       document.write(n + '. PASS: ' + (message || '(no message)') + '<br/>');
    }
}

// USER CREATED OBJECTS

assert(isNative({}), '{} -- Plain object');
assert(isNative(function() {}), 'function() {} -- Plain function');
assert(isNative([]), '[] -- Plain array');

assert(isNative(/regex/), '/regex/ - Native regex');
assert(isNative(new Date()), 'new Date() - Native date object through instantiation');

assert(isNative(new String('string')), 'new String("string") - Native string object through instantiation');
assert(isNative(new Number(1)), 'new Number(1) - Native number object through instantiation');
assert(isNative(new Boolean(true)), 'new Boolean(true) - Native boolean object through instantiation');
assert(isNative(new Array()), 'new Array() - Native array object through instantiation');
assert(isNative(new Object()), '{} -- new Object() - Native object through instantiation');
assert(isNative(new Function('alert(1)')), '{} -- Native function through instantiation');

// USER OBJECT INSTANTIATION AND INHERITANCE

var Animal = function() {};
var animal = new Animal();

var Dog = function() {};
Dog.prototype = animal;
var dog = new Dog();

assert(isNative(Animal), 'Animal -- User defined type');
assert(isNative(animal), 'animal -- Instance of User defined type');

assert(isNative(Dog), 'Dog -- User defined inherited type');
assert(isNative(dog), 'dog -- Instance of User defined inherited type');

// BUILT IN OBJECTS

assert(isNative(Object), 'Object -- Built in');
assert(isNative(Array), 'Array -- Built in');
assert(isNative(Date), 'Date -- Built in');
assert(isNative(Boolean), 'Boolean -- Built in');
assert(isNative(String), 'String -- Built in');
assert(isNative(Function), 'Function -- Built in');

// PRIMITIVE TYPES 

assert(!isNative('string'), '"string" - Primitive string');
assert(!isNative(1), '1 - Primitive number');
assert(!isNative(true), 'true - Primitive boolean');
assert(!isNative(null), 'null - Primitive null');
assert(!isNative(NaN), 'NaN - Primitive number NotANumber');
assert(!isNative(Infinity), 'Infinity - Primitive number Infinity');
assert(!isNative(undefined), 'undefined - Primitive value undefined');

// HOST OBJECTS

assert(!isNative(window), 'window -- Host object');
assert(!isNative(alert), 'alert -- Host function'); // fails on chrome
assert(!isNative(document), 'document -- Host object');
assert(!isNative(location), 'location -- Host object');
assert(!isNative(navigator), 'navigator -- Host object');
assert(!isNative(parent), 'parent -- Host object');
assert(!isNative(frames), 'frames -- Host object');

process.binding('evals').NodeScript.constructor instanceof Function === true; 我相当确定 NodeScript 在 node.js 中是用 C++ 定义的,应该算作“宿主对象”。你的解决方案难以扩展到非浏览器环境。 - Raynos
我不确定这是否正确,但是根据isNativealert本身是一个本地函数。 - pimvdb
@Steven de Salas:显然,alertinstanceof {Object, Function},所以isNative中没有捕获。但我不知道是什么属性使alert能够被区分出来... - pimvdb
@Steven de Salas:虽然你可以测试 new alert,它会抛出一个特定的错误,你可以尝试捕获。但是再说一遍,这也变成了另一个丑陋的主机对象技巧列表,而你说你想避免这种情况。 - pimvdb
问题在于误报,因为环境有时会模拟宿主对象的行为,使其与本地对象无法区分。我想前进的唯一方法是测试更多的功能,看看是否在其中任何一个失败,但这永远不会完美,因为宿主对象允许模拟本地对象。 - Steven de Salas
显示剩余2条评论

0

我相信主机对象的本质意味着没有一种简单的、与环境无关的方法来检测它们。如果你感兴趣,可以参考这个SO上的讨论

正如你所指出的,jQuery项目也尝试检测主机对象并遇到了类似的问题。那个错误页面上的讨论非常有启示性。


jQuery只是jQuery,尽管它的代码在许多方面非常智能,但仍然受到范围的限制,并且特定于其特定目的(浏览器内DOM操作)。 - Steven de Salas
1
SO链接似乎已经失效。 - the8472

0

我有一个想法,可能不适用于所有情况。

确保你的脚本是第一个执行的,并将其包装在一个闭包中,就像JS框架一样。
然后,循环遍历全局范围内的所有对象(如果你在非浏览器环境下,则window将未定义;因此,在脚本开头执行window = this),并循环遍历其子级等等除了你的对象之外的所有对象都将是宿主对象! 然后你可以将其添加到本地数据库或甚至存储它并与运行环境关联以供将来使用。


尝试过了,不起作用,一些主机对象与它们自己不相等,因此在两个相同主机对象的引用之间进行等式比较的结果是不可预测的。 - Steven de Salas
太糟糕了。但是,这些是哪些?我很好奇。 - Camilo Martin
在IE中,window.frames是最糟糕的。请尝试使用console.log(frames === frames); - Steven de Salas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接