Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
mqyqingfeng opened this issue Jul 5, 2017 · 45 comments
Open

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

mqyqingfeng opened this issue Jul 5, 2017 · 45 comments

Comments

@mqyqingfeng
Copy link
Owner

mqyqingfeng commented Jul 5, 2017

前言

在上篇《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,对作者也是一种鼓励。

@F-happy
Copy link

F-happy commented Sep 12, 2017

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

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

@mqyqingfeng
Copy link
Owner Author

@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 的方法哈~

@NewNewKing
Copy link

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

@mqyqingfeng
Copy link
Owner Author

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

@foxpsd
Copy link

foxpsd commented Oct 26, 2017

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

@mqyqingfeng
Copy link
Owner Author

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

default

@foxpsd
Copy link

foxpsd commented Oct 26, 2017

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

@mqyqingfeng
Copy link
Owner Author

@foxpsd 出自 jQuery 的源码

@xiaobinwu
Copy link

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

@mqyqingfeng
Copy link
Owner Author

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

@sarazhang123
Copy link

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

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

是否可以写成

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

@mqyqingfeng
Copy link
Owner Author

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

@zhangruinian
Copy link

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

@mqyqingfeng
Copy link
Owner Author

@zhangruinian 举个例子:

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

console.log(isElement()) // 报错
console.log(isElement(a)) // 报错
console.log(isElement(undefined)) // 报错

@MillionQW
Copy link

你好我想问问isWindow的判断原理是判断传入的对象是否有window属性,如果我创造一个对象,
给对象一个window属性指向window,这样isWindow不是也会返回true吗。

@mqyqingfeng
Copy link
Owner Author

@MillionQW

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

应该是创造一个对象,将这个对象的 window 属性指向自己

如果要刻意创造这样一个对象,这也没有办法呀~

@magentaqin
Copy link

isElement里!!(obj && obj.nodeType === 1)为什么不能直接写成obj && obj.nodeType === 1

@mqyqingfeng
Copy link
Owner Author

@magentaqin isElement 函数用来判断元素是否是 Element,结果只有两种,true 或者 false。

举个例子:

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

如果 obj 不存在的话,直接写就变成了返回 undefined

@MillionQW
Copy link

因为 || 和 && 的返回值实际上不是布尔值,而是返回比较中的两个值中的一个,用 !! 是做强制类型转换,将值转换为布尔值。
一点愚见,还是要请博主回答。(^○^)

@zjp6049
Copy link

zjp6049 commented Dec 12, 2017

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

@magentaqin
Copy link

magentaqin commented Dec 14, 2017 via email

@ClarenceC
Copy link

数组最后一个元素要有值这个好像吐槽了好久的一个梗.

@mqyqingfeng
Copy link
Owner Author

@ClarenceC 这个梗不知道哎~😂

@xunan007
Copy link

其实我觉得这段代码是有问题的:

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;
};

typeof length == 'number'这一个判断中,比如说typeof 2.3 == 'number'也是成立的,有点不严谨的地方就是length不应该是小数。JavaScript权威指南中判断的方法我觉得会更好一些:

function isArrayLike(o) {
    if (o && // o is not null, undefined, etc
        // o is an object
        typeof o === "object" &&
        // o.length is a finite number
        isFinite(o.length) &&
        // o.length is non-negative
        o.length >= 0 &&
        // o.length is an integer
        o.length === Math.floor(o.length) &&
        // o.length < 2^32
        o.length < 4294967296) //数组的上限值
      	return true;
    else 
      	return false;
}

@tr2v
Copy link

tr2v commented May 29, 2018

意思是这种就不算类数组对象吗。
var arrLike = { 0: 1, length: 3 }
好难理解啊,类数组的定义要求的吗

@mqyqingfeng
Copy link
Owner Author

@tr2v 没有要求,看 API 设计者想设计成什么样

@mqyqingfeng
Copy link
Owner Author

@tr2v 你要是写一个 API,将这种类型也算为类数组对象,也可以的,重点还是说在实际开发中对于类数组对象的处理是否符合业务需求,比如写业务中就是会出现这种对象,而我又需要跟数组一样处理,那它就是被允许的

@xwcp
Copy link

xwcp commented Oct 29, 2018

// obj 必须有 length属性
var length = !!obj && "length" in obj && obj.length; !!obj为什么要这样判断

@inottn
Copy link

inottn commented Dec 10, 2018

@xwcp

首先要理解 a || ba && b 返回的值不一定是布尔值,而是 a 和 b 其中一个值。

所以假定 obj === 0 时:

var length = !!obj // 此时 length === false
var length = obj   // 此时 length === 0

@rutingjin
Copy link

判断是否为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 请楼主解惑

@liiiku
Copy link

liiiku commented Mar 12, 2019

针对isArrayLike函数会将var obj = {length: 0, b: 1}这样的对象也判断为数组的问题,给这个函数的开始加一个这样的判断感觉可以解决问题,不知道会不会有其他的影响:

function isArrayLike(obj) {
    if (Object.keys(obj).indexOf('length') > -1) return false
   // 后面的代码省略
}

因为如果这里忽略这个问题,我看了后面的underscore的文章中,假如我要遍历一个,这样的对象:

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

_.each(obj, callback)是没有效果的

@sclchic
Copy link

sclchic commented Mar 15, 2019

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

推荐chrome的插件 Octotree 😄

@yanghuiqi
Copy link

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

@Fangwenwen
Copy link

Fangwenwen commented Jul 10, 2019

@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;

@wangyiman
Copy link

wangyiman commented Jul 10, 2019

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

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

@YaoXiang123
Copy link

YaoXiang123 commented Oct 28, 2019

image
这里应该是window.obj吧

@moyahuang
Copy link

image
这里应该是window.obj吧

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

@raind33
Copy link

raind33 commented Apr 5, 2020

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

@wweggplant
Copy link

判断是否为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没有定义的话,会报错的

@Vuact
Copy link

Vuact commented Dec 4, 2020

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)
  );
}

@yinju123
Copy link

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

@yinju123
Copy link

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

Ctor === Object

@tahr007
Copy link

tahr007 commented Oct 20, 2021

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

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

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

@wittypeter
Copy link

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

Ctor === Object

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

@zhangzhuoya
Copy link

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

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

No branches or pull requests