isPlainObject
/**
* @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 = obj
while (Object.getPrototypeOf(proto) !== null) {
proto = Object.getPrototypeOf(proto)
}
// 拿obj最初的__proto__跟obj次顶层的__proto__做对比
return Object.getPrototypeOf(obj) === proto
}
依然是那么的简洁,while循环就是不对往obj的原型链往上查找,直到次顶层,一般来说就是Object.prototype,最后一行代码就是判断obj的__proto__
跟次顶层的Object.prototype
是否相等。那么这么看下来只有对象字面量跟new Object()创建的对象才能满足这个条件。该方法在redux中用来校验action是否为一个原生的JavaScript对象,这点跟lodash的isPlainObject有差别。可以添加测试用了,测试看看。
翻看了redux官网对isPlainObject.spec.js的测试用例
import expect from 'expect'
import isPlainObject from '../../src/utils/isPlainObject'
import vm from 'vm'
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)
})
})
其中并没有测试到Object.create的情况,我做了补充isPlainObject.spec.js,如下
import expect from 'expect'; // jest 的断言库
import isPlainObject from './isPlainObject';
import vm from 'vm';
describe('isPlainObject', () => {
it('returns true only if plain object', () => {
function Test() {
this.prop = 1;
}
const sandbox = { fromAnotherRealm: false };
vm.runInNewContext('fromAnotherRealm = {}', sandbox);
expect(sandbox).toEqual({ fromAnotherRealm: {} });
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(new Object())).toBe(true);
expect(isPlainObject(Object.create(null))).toBe(false);
expect(isPlainObject(Object.create({}))).toBe(false);
});
});
其中expect(isPlainObject(sandbox.fromAnotherRealm)).toBe(true);
很难理解,为什么要这么考虑,vm.runInNewContext('fromAnotherRealm = {}', sandbox);
这句话又是什么意思?而且经过上面的分析代码可以简化成这样
export default function isPlainObject(obj) {
if (typeof obj !== 'object' || obj === null) return false
return Object.getPrototypeOf(obj) === Object.prototype;
}
为什么redux源码不这么写?
为什么 Redux 判断 PlainObject 的写法这么复杂?跟我相同的疑问,大神在下面做出了详细的解释。粗略的理解下大神的意思就是js执行有不同的上下文环境,比如页面嵌套iframe就是不同的context环境,如果dispatch的action是iframe传过来的,那么用Object.getPrototypeOf(obj) === Object.prototype
返回的是false,其实这个action是个字面量对象或者new Object对象,只要是这种情况的对象就是被容许的,所以redux的考虑还是比较仔细的。
还有人提出其他疑问为什么redux不使用Object.prototype.toString判断Plain Object?从答案看出Object.prototype.toString判断对象的类型是不严谨的,这是我第一次看到。
Symbol.toStringTag可以修改toString的输出
import getTag from './.internal/getTag.js'
import isObjectLike from './isObjectLike.js'
/**
* Checks if `value` is a plain object, that is, an object created by the
* `Object` constructor or one with a `[[Prototype]]` of `null`.
*
* @since 0.8.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a plain object, else `false`.
* @example
*
* function Foo() {
* this.a = 1
* }
*
* isPlainObject(new Foo)
* // => false
*
* isPlainObject([1, 2, 3])
* // => false
*
* isPlainObject({ 'x': 0, 'y': 0 })
* // => true
*
* isPlainObject(Object.create(null))
* // => true
*/
function isPlainObject(value) {
if (!isObjectLike(value) || getTag(value) != '[object Object]') {
return false
}
if (Object.getPrototypeOf(value) === null) {
return true
}
let proto = value
while (Object.getPrototypeOf(proto) !== null) {
proto = Object.getPrototypeOf(proto)
}
return Object.getPrototypeOf(value) === proto
}
export default isPlainObject