Skip to content

JavaScript专题之类型判断(下) #30

Description

@mqyqingfeng

前言

在上篇《JavaScript专题之类型判断(上)》中,我们抄袭 jQuery 写了一个 type 函数,可以检测出常见的数据类型,然而在开发中还有更加复杂的判断,比如 plainObject、空对象、Window 对象等,这一篇就让我们接着抄袭 jQuery 去看一下这些类型的判断。

plainObject

plainObject 来自于 jQuery,可以翻译成纯粹的对象,所谓"纯粹的对象",就是该对象是通过 "{}" 或 "new Object" 创建的,该对象含有零个或者多个键值对。

之所以要判断是不是 plainObject,是为了跟其他的 JavaScript对象如 null,数组,宿主对象(documents)等作区分,因为这些用 typeof 都会返回object。

jQuery提供了 isPlainObject 方法进行判断,先让我们看看使用的效果:

function Person(name) {
    this.name = name;
}

console.log($.isPlainObject({})) // true

console.log($.isPlainObject(new Object)) // true

console.log($.isPlainObject(Object.create(null))); // true

console.log($.isPlainObject(Object.assign({a: 1}, {b: 2}))); // true

console.log($.isPlainObject(new Person('yayu'))); // false

console.log($.isPlainObject(Object.create({}))); // false

由此我们可以看到,除了 {} 和 new Object 创建的之外,jQuery 认为一个没有原型的对象也是一个纯粹的对象。

实际上随着 jQuery 版本的提升,isPlainObject 的实现也在变化,我们今天讲的是 3.0 版本下的 isPlainObject,我们直接看源码:

// 上节中写 type 函数时,用来存放 toString 映射结果的对象
var class2type = {};

// 相当于 Object.prototype.toString
var toString = class2type.toString;

// 相当于 Object.prototype.hasOwnProperty
var hasOwn = class2type.hasOwnProperty;

function isPlainObject(obj) {
    var proto, Ctor;

    // 排除掉明显不是obj的以及一些宿主对象如Window
    if (!obj || toString.call(obj) !== "[object Object]") {
        return false;
    }

    /**
     * getPrototypeOf es5 方法,获取 obj 的原型
     * 以 new Object 创建的对象为例的话
     * obj.__proto__ === Object.prototype
     */
    proto = Object.getPrototypeOf(obj);

    // 没有原型的对象是纯粹的,Object.create(null) 就在这里返回 true
    if (!proto) {
        return true;
    }

    /**
     * 以下判断通过 new Object 方式创建的对象
     * 判断 proto 是否有 constructor 属性,如果有就让 Ctor 的值为 proto.constructor
     * 如果是 Object 函数创建的对象,Ctor 在这里就等于 Object 构造函数
     */
    Ctor = hasOwn.call(proto, "constructor") && proto.constructor;

    // 在这里判断 Ctor 构造函数是不是 Object 构造函数,用于区分自定义构造函数和 Object 构造函数
    return typeof Ctor === "function" && hasOwn.toString.call(Ctor) === hasOwn.toString.call(Object);
}

注意:我们判断 Ctor 构造函数是不是 Object 构造函数,用的是 hasOwn.toString.call(Ctor),这个方法可不是 Object.prototype.toString,不信我们在函数里加上下面这两句话:

console.log(hasOwn.toString.call(Ctor)); // function Object() { [native code] }
console.log(Object.prototype.toString.call(Ctor)); // [object Function]

发现返回的值并不一样,这是因为 hasOwn.toString 调用的其实是 Function.prototype.toString,毕竟 hasOwnProperty 可是一个函数!

而且 Function 对象覆盖了从 Object 继承来的 Object.prototype.toString 方法。函数的 toString 方法会返回一个表示函数源代码的字符串。具体来说,包括 function关键字,形参列表,大括号,以及函数体中的内容。

EmptyObject

jQuery提供了 isEmptyObject 方法来判断是否是空对象,代码简单,我们直接看源码:

function isEmptyObject( obj ) {

        var name;

        for ( name in obj ) {
            return false;
        }

        return true;
}

其实所谓的 isEmptyObject 就是判断是否有属性,for 循环一旦执行,就说明有属性,有属性就会返回 false。

但是根据这个源码我们可以看出isEmptyObject实际上判断的并不仅仅是空对象。

举个栗子:

console.log(isEmptyObject({})); // true
console.log(isEmptyObject([])); // true
console.log(isEmptyObject(null)); // true
console.log(isEmptyObject(undefined)); // true
console.log(isEmptyObject(1)); // true
console.log(isEmptyObject('')); // true
console.log(isEmptyObject(true)); // true

以上都会返回 true。

但是既然 jQuery 是这样写,可能是因为考虑到实际开发中 isEmptyObject 用来判断 {} 和 {a: 1} 是足够的吧。如果真的是只判断 {},完全可以结合上篇写的 type 函数筛选掉不适合的情况。

Window对象

Window 对象作为客户端 JavaScript 的全局对象,它有一个 window 属性指向自身,这点在《JavaScript深入之变量对象》中讲到过。我们可以利用这个特性判断是否是 Window 对象。

function isWindow( obj ) {
    return obj != null && obj === obj.window;
}

isArrayLike

isArrayLike,看名字可能会让我们觉得这是判断类数组对象的,其实不仅仅是这样,jQuery 实现的 isArrayLike,数组和类数组都会返回 true。

因为源码比较简单,我们直接看源码:

function isArrayLike(obj) {

    // obj 必须有 length属性
    var length = !!obj && "length" in obj && obj.length;
    var typeRes = type(obj);

    // 排除掉函数和 Window 对象
    if (typeRes === "function" || isWindow(obj)) {
        return false;
    }

    return typeRes === "array" || length === 0 ||
        typeof length === "number" && length > 0 && (length - 1) in obj;
}

重点分析 return 这一行,使用了或语句,只要一个为 true,结果就返回 true。

所以如果 isArrayLike 返回true,至少要满足三个条件之一:

  1. 是数组
  2. 长度为 0
  3. lengths 属性是大于 0 的数字类型,并且obj[length - 1]必须存在

第一个就不说了,看第二个,为什么长度为 0 就可以直接判断为 true 呢?

那我们写个对象:

var obj = {a: 1, b: 2, length: 0}

isArrayLike 函数就会返回 true,那这个合理吗?

回答合不合理之前,我们先看一个例子:

function a(){
    console.log(isArrayLike(arguments))
}
a();

如果我们去掉length === 0 这个判断,就会打印 false,然而我们都知道 arguments 是一个类数组对象,这里是应该返回 true 的。

所以是不是为了放过空的 arguments 时也放过了一些存在争议的对象呢?

第三个条件:length 是数字,并且 length > 0 且最后一个元素存在。

为什么仅仅要求最后一个元素存在呢?

让我们先想下数组是不是可以这样写:

var arr = [,,3]

当我们写一个对应的类数组对象就是:

var arrLike = {
    2: 3,
    length: 3
}

也就是说当我们在数组中用逗号直接跳过的时候,我们认为该元素是不存在的,类数组对象中也就不用写这个元素,但是最后一个元素是一定要写的,要不然 length 的长度就不会是最后一个元素的 key 值加 1。比如数组可以这样写

var arr = [1,,];
console.log(arr.length) // 2

但是类数组对象就只能写成:

var arrLike = {
    0: 1,
    length: 1
}

所以符合条件的类数组对象是一定存在最后一个元素的!

这就是满足 isArrayLike 的三个条件,其实除了 jQuery 之外,很多库都有对 isArrayLike 的实现,比如 underscore:

var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;

var isArrayLike = function(collection) {
    var length = getLength(collection);
    return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
};

isElement

isElement 判断是不是 DOM 元素。

isElement = function(obj) {
    return !!(obj && obj.nodeType === 1);
};

结语

这一篇我们介绍了 jQuery 的 isPlainObject、isEmptyObject、isWindow、isArrayLike、以及 underscore 的 isElement 实现。我们可以看到,即使是 jQuery 这样优秀的库,一些方法的实现也并不是非常完美和严密的,但是最后为什么这么做,其实也是一种权衡,权衡所失与所得,正如玉伯在《从 JavaScript 数组去重谈性能优化》中讲到:

所有这些点,都必须脚踏实地在具体应用场景下去分析、去选择,要让场景说话。

专题系列

JavaScript专题系列目录地址:https://github.com/mqyqingfeng/Blog。

JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

Activity

  1. F-happy commented on Sep 12, 2017

    @F-happy

    这样是不是也可以判断是否是空对象呢?

    function isEmptyObject(obj) {
        return !!obj ? (Object.keys(obj).length === 0) : true;
    }
  2. mqyqingfeng commented on Sep 12, 2017

    @mqyqingfeng
    OwnerAuthor

    @F-happy 也可以呀,不过两者稍微有一点区别,文章中的方法用的是 for in ,而这种方法用的是 Object.keys 两者的主要区别在于 for in 还会遍历原型上的属性,这也就意味着:

    function Person(){}
    Person.prototype.name = "111"
    var person = new Person();
    console.log(isEmptyObject(person))

    如果用文中的方法就会是 false,用这种方法结果就会是 true,无所谓对错,看设计者想设计成什么样哈。

    除此之外, Object.keys 是一个 ES5 的方法哈~

  3. NewNewKing commented on Oct 26, 2017

    @NewNewKing

    image
    这里 应该是length 大于0的number类型的吧

  4. mqyqingfeng commented on Oct 26, 2017

    @mqyqingfeng
    OwnerAuthor

    @NewNewKing 非常感谢指出~ 确实是写错了

  5. foxpsd commented on Oct 26, 2017

    @foxpsd

    有一个地方有点疑惑,isWindow方法也会放过一个 window 属性指向自身的普通对象。用 toString 方法得到的[object Window]是不是更好呢?

  6. mqyqingfeng commented on Oct 26, 2017

    @mqyqingfeng
    OwnerAuthor

    @foxpsd 出于兼容性的考虑,使用 toString 打印的结果在各个浏览器还不一样:

    default

  7. foxpsd commented on Oct 26, 2017

    @foxpsd

    @mqyqingfeng 原来如此····
    顺便问一句,楼主这个结构是自己试的还是在什么资料站看到哒?

  8. mqyqingfeng commented on Oct 26, 2017

    @mqyqingfeng
    OwnerAuthor

    @foxpsd 出自 jQuery 的源码

  9. xiaobinwu commented on Nov 3, 2017

    @xiaobinwu

    看来要花些时间去多看源码,楼主对于看源码有没有心历过程呀?

  10. mqyqingfeng commented on Nov 3, 2017

    @mqyqingfeng
    OwnerAuthor

    @xiaobinwu 心历过程?先挑个简单的看,然后搜索看有没有人写过这个东西的源码解析或者精简版的实现,然后边看源码边看文章,不知道算不算……

  11. sarazhang123 commented on Nov 10, 2017

    @sarazhang123

    @mqyqingfeng 判断isPlainObject那个方法的最后一句

    return typeof Ctor === "function" && hasOwn.toString.call(Ctor) === hasOwn.toString.call(Object);
    

    是否可以写成

    return typeof Ctor === "function" && Ctor.toString() === class2type.constructor.toString();
    
  12. mqyqingfeng commented on Nov 15, 2017

    @mqyqingfeng
    OwnerAuthor

    @sarazhang123 可以的,如果非要说两种方法有什么区别的话,可能是前者速度会更快些……

  13. zhangruinian commented on Nov 17, 2017

    @zhangruinian

    isElement里面为什么多加一个判断obj是否存在呢? 直接obj.nodeType === 1不可以么?

  14. mqyqingfeng commented on Nov 17, 2017

    @mqyqingfeng
    OwnerAuthor

    @zhangruinian 举个例子:

    var isElement = function(obj) {
        return !!(obj.nodeType === 1);
    };
    
    console.log(isElement()) // 报错
    console.log(isElement(a)) // 报错
    console.log(isElement(undefined)) // 报错
  15. 17 remaining items

  16. sclchic commented on Mar 15, 2019

    @sclchic

    来两遍只能说看懂了代码想表达什么,要我自己写的话肯定会懵逼。
    还有~~为什么没有直接点下一篇的链接啊哈哈哈

    推荐chrome的插件 Octotree 😄

  17. happy-ling commented on Mar 22, 2019

    @happy-ling

    window.a = 1;
    function isWindow( obj ) {
    return obj != null && obj === obj.window;
    }
    isWindow(a) //false
    请问是window.a 还是说{} {}.window={} 这种赋值的window对象呢?

  18. Fangwenwen commented on Jul 10, 2019

    @Fangwenwen

    @mqyqingfeng 判断isPlainObject那个方法的最后一句

    return typeof Ctor === "function" && hasOwn.toString.call(Ctor) === hasOwn.toString.call(Object);
    

    是否可以写成

    return typeof Ctor === "function" && Ctor.toString() === class2type.constructor.toString();
    

    那最后一句是否可以写成

    return Ctor === Object;

  19. wangyiman commented on Jul 10, 2019

    @wangyiman

    @sarazhang123 可以的,如果非要说两种方法有什么区别的话,可能是前者速度会更快些……

    为什么前者Ctor是函数类型,本可以Ctor.toString,却非要调用call来调用?
    是不是也可以return typeof Ctor === "function" && Ctor.toString() === Object.toString();

  20. YaoXiang123 commented on Oct 28, 2019

    @YaoXiang123

    image
    这里应该是window.obj吧

  21. moyahuang commented on Apr 1, 2020

    @moyahuang

    image
    这里应该是window.obj吧

    当然只有obj等于window时才会返回true啦
    window.window === window //true

  22. raind33 commented on Apr 5, 2020

    @raind33

    空对象判断,isEmptyObject(100)也会返回true,这个合理吗

  23. wweggplant commented on Jul 2, 2020

    @wweggplant

    判断是否为window

    function isWindow( obj ) {
        return obj != null && obj === obj.window;
    }
    let fakeWindow = {}
    fakeWindow.window = fakeWindow
    isWindow(fakeWindow) // true

    为什么不用这种方法,是存在什么缺陷吗?

    function isWindow(obj) {
        return !!(window && obj === window)
    }

    @mqyqingfeng 请楼主解惑

    我觉得应该是在node环境下,window没有定义的话,会报错的

  24. Vuact commented on Dec 4, 2020

    @Vuact

    Array.isArray函数个人感觉这么写更好些:

    const MAX_ARRAY_INDEX = Math.pow(2, 53) - 1; //避免内存溢出
    
    function isArrayLike(obj) {
      if (isWindow(obj) || type(obj) === "function") {
        return false;
      }
    
      const length =
        !!obj &&
        typeof obj.length === "number" &&
        obj.length >= 0 &&
        obj.length <= MAX_ARRAY_INDEX &&
        obj.length;
    
      if (isElement(obj) && length) {
        return true;
      }
    
      return (
        Array.isArray(obj) || length === 0 || (length > 0 && length - 1 in obj)
      );
    }
  25. yinju123 commented on Apr 20, 2021

    @yinju123

    !obj || toString.call(obj) !== "[object Object]" 中!obj可以不用判断吧,toString 就能判断吧

  26. yinju123 commented on Apr 20, 2021

    @yinju123

    typeof Ctor === "function" && hasOwn.toString.call(Ctor) === hasOwn.toString.call(Object);可以直接判断构造函数和Object吗

    Ctor === Object
    
  27. tahr007 commented on Oct 20, 2021

    @tahr007

    这样是不是也可以判断是否是空对象呢?

    function isEmptyObject(obj) {
        return !!obj ? (Object.keys(obj).length === 0) : true;
    }

    Object.keys 拿不到原型链上的属性吧

  28. wittypeter commented on Mar 2, 2022

    @wittypeter

    typeof Ctor === "function" && hasOwn.toString.call(Ctor) === hasOwn.toString.call(Object);可以直接判断构造函数和Object吗

    Ctor === Object
    

    同问,直接判等是不是有没有什么说法呢

  29. zhangzhuoya commented on Mar 7, 2022

    @zhangzhuoya

    /**
    * 以下判断通过 new Object 方式创建的对象
    * 判断 proto 是否有 constructor 属性,如果有就让 Ctor 的值为 proto.constructor
    * 如果是 Object 函数创建的对象,Ctor 在这里就等于 Object 构造函数
    */
    Ctor = hasOwn.call(proto, "constructor") && proto.constructor;
    请问这里为什么要判断两次,只用proto.constructor或者只用hasOwn.call(proto, "constructor") 可以吗

  30. chd666233 commented on Apr 5, 2025

    @chd666233

    string也是伪数组, 所以isArrayLike('abc')会报错

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions