react-redux源码精读之isPlainObject

1,389 阅读2分钟

what is plainObject

  • 通过{}或者new Object()方式创建的对象是纯粹对象,Object.create(null) 创建的也是纯粹对象
  • 通过jquey.isPlain的源码来看纯粹对象是原型链最简单的一类对象, 对象的原型只能是null或者Object.prototype
  • isPlainObject函数的功能的判断依据与对象使用什么方式创建无关,而与的函数原型是否是Object.prototype有关系

下面是react-redux6.0.0里面的源码 isPlainObject.js


/**
 * @param {any} obj The object to inspect.
 * @returns {boolean} True if the argument appears to be a plain object.
 */
export default function isPlainObject(obj) {
  if (typeof obj !== 'object' || obj === null) return false

  let proto = Object.getPrototypeOf(obj)
  // 1. edge case Object.create(null)
  if (proto === null) return true
  let baseProto = proto
  
  while (Object.getPrototypeOf(baseProto) !== null) {
    baseProto = Object.getPrototypeOf(baseProto)
  }
  // 2. 原型链第一个和最后一个比较
  return proto === baseProto
}

代码理解也不难,

  1. 通过Object.create(null)生成的对象proto的值是:null, 处理edge case
    Object.create(null) 
    输出: {}
    
  2. 通过一个while循环拿到参数对象原型链的最后一个值,然后和对象原型链上的第一个值比较
  3. 因为有前置判断typeof,任意对象while循环结束后proto的值都是:Object.prototype,只有普通对象plainObject 原型链上的第一个值是Object.prototype 由此判断相等,
  4. 问题来了,但是为什么不直接进行下面的判断呢?
return Object.getPrototypeOf(obj) === Object.prototype || Object.getPrototypeOf(obj) === null
  1. redux是为了防止跨iframe访问变量时的类型判断错误, 比如说在一个frame里面调用父窗口的函数:
window.parent.someFunction(["hello", "world"])

在父窗口中有someFunction的定义

function someFunction(arg) {
  if (arg instanceof Array) {
    // ... operate on the array
  }
}

这样调用并不会执行if语句的代码,因为两段代码所处的javascript执行环境是不一样的,每个frame都有自己的执行环境,他们也不会共享原型链,也就是说两个执行环境中的Array Object构造函数都是不等的,那么if语句的判断就为false,这个数组并不是继承的父窗口执行环境里的Array

在看看单元测试源码

describe('isPlainObject', () => {
  it('returns true only if plain object', () => {
    function Test() {
      this.prop = 1
    }

    const sandbox = { fromAnotherRealm: false }
    vm.runInNewContext('fromAnotherRealm = {}', sandbox)

    expect(isPlainObject(sandbox.fromAnotherRealm)).toBe(true)
    expect(isPlainObject(new Test())).toBe(false)
    expect(isPlainObject(new Date())).toBe(false)
    expect(isPlainObject([1, 2, 3])).toBe(false)
    expect(isPlainObject(null)).toBe(false)
    expect(isPlainObject()).toBe(false)
    expect(isPlainObject({ x: 1, y: 2 })).toBe(true)
    expect(isPlainObject(Object.create(null))).toBe(true)
  })
})

可以看到源码并没有加上跨iframe的单元测试,大概是因为很难制造两个不同js执行环境

启发

  • 跨iframe的类型检测是个坑,很可能你依赖的一些底层的库就没有处理,比如JQ,所以遇到的跨iframe调用需要小心
  • ts的类型有没有设计这种针对iframe的edge case呢

参考文章