# [筆記] Javascript 中的 Plain Object

2020年3月9日 星期一 js

# 前言

前陣子在看 Vue 源碼時,看到了 Vue 的 utils 有一個 isPlainObject 的方法。

所以想說來一探 Javascript Plain Object,結果發現根本如泥沼一般啊 XD

# 快速導覽

  • JS Plain Object 定義
    • 使用 typeof 來最初步的判斷
    • 原形鍊判斷
    • Object.prototype.toString

# JS Plain Object 定義

到底 Plain Object 是什麼呢?

上網查詢後的得到的結論眾說紛紜,每個人給出的答案都不盡相同。

以下兩個是常見 isPlainObject 做法。

  • 使用 typeof 來最初步的判斷
  • 產生出來的 object 的 protype 要是 Object.prototype
  • 透過 Object.prototype.toString 得到 [object {tag}]

# 使用 typeof 來最初步的判斷

我們知道 JS 裡的一個眾所皆知的大笑話 typeof null === 'object'

但其實 typeof 還是滿有用的,能夠讓我們初步的判斷你的 Object 是不是真的長得像 Object XD。

// lodash isObjectLike
function isObjectLike(value) {
  return typeof value === 'object' && value !== null
}

但一直都沒有改掉這個,也許也永遠不會~

# 產生出來的 object 的原形要是 Object.prototype

最簡單的判斷 Plain Object 也就是只要是透過 object iterator 也就是 {},或是使用 new Object 來產生的 object 都先統稱為 Plain Object。

如果是透過 function 建立的 object 我們就不稱他為 Plain Object。

範例

// Plain Object
const foo = new Object
const bar = {}

// Not Plain Object
function Foobar () {}
const foobar = new Foobar

到這邊簡單我們整理出基本款 isPlainObject 的做法,看看兩者差異就可以了。

  • Plain Object
    • foo、bar 的 prototypeObject.prototype
    • foobar 的 prototypeFoobar.prototype

這邊我們透過 Object.getPrototype 來找到物件的原型。

// Plain Object
const foo = new Object
const bar = {}

console.log(Object.getPrototypeOf(foo) === Object.prototype)

// Not Plain Object
function Foobar () {}
const foobar = new Foobar

console.log(Object.getPrototypeOf(foobar) !== Object.prototype)

到這邊其實就是最簡單的 Plain Object 的判斷囉!

這也是 lodash 的實作。

但其實有一些小小得不同,這邊拿出來看看。

# Object.create(null)

透過 Object.create(null) 這個方法能夠做出一個原形為 null 也就是沒有原型的 object

常常被用在製作單純的 Key - Value Mapping。

這個方式產生了 object,照我們上面透過 Object.getPrototypeOf 是無法成為 plain object 的 (因為他的 prototype 不是 Object.prototype 嘛!)。

有趣的是像是 lodash、JQuery 這邊把它當成是 plain object。

但也有一些人寫出來的 isPlainObject 是把 Object.create(null) 判斷成 false

如:is-plain-object (opens new window)

若有用到可能要特別注意一下。

# 透過 Object.prototype.toString 得到 [object {tag}]

Object.prototype.toString 也就是 Vue.js 底層函式 isPlainObject 的用法。

若把 Plain Object 丟進 Object.prototype.toString 會回傳一個 [object Object]

function isPlainObject (obj) {
    return Object.prototype.toString.call(obj) === '[object Object]'
}
isPlainObject({}) // true
isPlainObject(new Object) // true
isPlainObject(Object.create(null)) // true
isPlainObject(true) // [object Boolean] false
isPlainObject('') // [object String] false
isPlainObject([]) // [object Array] false

到這邊看許多人都使用 Object.prototype.toString 來判斷。

那到底為什麼是 Object.prototype.toString 這個看起來完全和判斷型別八竿子打不著的函數!

到這邊其實直接找 ECMAScript spec 的是最快的

19.1.3.6Object.prototype.toString ( ) (opens new window)

簡單解釋一下 ecma 定義了什麼 一起看一下做了什麼事...

  1. undefined 回傳 [object Undefined]
  2. null 回傳 [object Null]
  3. 設 O 為 ToObject(this value) let O = ToObject(this value)
  4. 透過 isArray(O) 判斷是 array,使 builtinTag"Array"
  5. 如果 O 是 exotic object, 回傳 String ,使 builtinTag"String"
  6. 如果 O 有 [[ ParameterMap ]], ,使 builtinTag"Arguments"
  7. 如果 O 有 [[ Call ]], ,使 builtinTag"Function"
  8. 如果 O 有 [[ ErrorData ]], ,使 builtinTag"Error"
  9. 如果 O 有 [[ BooleanData ]], ,使 builtinTag"Boolean"
  10. 如果 O 有 [[ NumberData ]], ,使 builtinTag"Number"
  11. 如果 O 有 [[ DateValue ]], ,使 builtinTag"Date"
  12. 如果 O 有 [[ RegExpMatcher ]], ,使 builtinTag"RegExp"
  13. 如果都不是以上,使 builtinTag"Object"
  14. Get 找看看 O 有沒有 @@toStringTag 存到 tag 中
  15. 如果 Type(tag) 不是 String 則 builtinTag = tag
  16. 回傳 string-concatenation "[object tag]"

其實講了那麼多,Object.prototype.toString 透過 內部 的檢查,回傳 [object (型態)]。

其實這樣看下來,上面的 13. 以前都可以很好理解是在判斷內部的型態,但 14. 後又是在做什麼呢。

我們可以透過這個 table (opens new window) 找到其實上面寫的 @@toStringTag 就是 Symbol.toStringTag

代表我們可以透過 Symbol.toStringTag 來變更 [object Tag] 的結果!

const foo = {}
foo[Symbol.toStringTag] = 'foo'

console.log(Object.prototype.toString.call(foo)) // '[object foo]'

# 小結

  1. typeof(obj) === 'object' && typeof(obj) !== null 來找到 ObjectLike 的 Object
  2. Object.getPrototypeOf(obj) === Object.prototype 之類的方式判斷原型是否為 Object.prototype
  3. 透過 Object.prototype.toString 取得 Tag 來判斷 Object

這邊我們看完了三種基本的 isPlainObject,再來我們可以看看各個大框架的實作啦。

# 各種 Library 實作

# lodash (opens new window)

lodash 看起來就是 1. isObjectLike 2. 原形鍊判斷 3. getTag === [object Object]

# Vue (opens new window)

vue 的話就只有用到 3. Object.prototype.toString

# JQuery (opens new window)

JQuery 其實在 Document 有定義 Plain Object

但我們直接看原始碼 JQ 這邊前面是用了 2. 再來額外判斷 prototype 是否為 null (Object.create(null)) 情境

最後的 return Ctor === 'function' && fnToString.call(Ctor) === ObjectFunctionString,是透過判斷傳入的 object 的 constructor 是否和 Object 相圖,所以感覺像是變形的 2. 吧。

話外題: JQuery 的 ObjectFunctionString,可以去追蹤一下過程,感覺滿神奇的XD (默默踩到另外一個水坑)

# Redux (opens new window)

基本上是採取 1. + 2. 但這邊有一些有趣的討論可以看看~https://www.zhihu.com/question/299783862

# 結論

Javascript Plain Object 基本上來說就是透過 new Object 或是 {} 定義出來的物件。

但各個框架的實作可能略有差異,若被雷到請詳細閱讀原始碼 XD

# 參考資料

stackoverflow isplainobject-thing https://stackoverflow.com/questions/18531624/isplainobject-thing ECMAScript