問題引發:最近在整理DOM系列的一些知識點,發現在DOM的某些介面API中,存在一些我想不通的現象。就隨便舉個例子吧:DOM文檔模型中的文本節點,可以通過nodeValue或data屬性訪問文本節點的文本內容,而且在更新data的時候nodeValue也即時更新,反之亦然。不光是data或nodeV ...
問題引發:
最近在整理DOM系列的一些知識點,發現在DOM的某些介面API中,存在一些我想不通的現象。就隨便舉個例子吧:DOM文檔模型中的文本節點,可以通過nodeValue或data屬性訪問文本節點的文本內容,而且在更新data的時候nodeValue也即時更新,反之亦然。不光是data或nodeVaulue有這種相互影響的關係,其他類型節點也有比如該改變document.title也會隨之改變網頁標題。當時想弄明白這在JS引擎中是怎麼實現的,於是乎進行了以下分析:
通過文本節點的原型鏈繼承關係 textNode.__proto__->Text.prototype->CharacterData.prototype->Node.prototype->EventTarget.prototype->Object.prototype。和原型上的存在的屬性和方法很好理解文本節點調用某個方法或屬性來自(繼承)哪裡
但是看下麵這段代碼
var text=document.createTextNode('文本'); text.hasOwnProperty('data');// false text.hasOwnProperty('nodeValue');// false text.data;// "文本" text.nodeValue;// "文本"
返回false很正常因為data是CharacterData.prototype上的屬性,nodeValue是Node.prototype上的屬性。text只是通過原型鏈繼承訪問這兩個屬性而已。當我改變內容,
text.data="新文本"; text.hasOwnProperty('data');// false text.nodeValue;// "新文本" text.data;// "新文本"
我奇怪為什麼已經給text對象添加了data屬性,但還是返回false??
疑惑一:按理說在對象自身添加和原型上相同名稱的屬性在訪問的時候只訪問到自身屬性上的data為止,就算JS引擎最後強制delete了text.data,在text.data試圖去訪問data的時候還是按原型鏈查找,在CharacterData.prototype的data上找到值為"新文本"。可是這樣又存在一個問題,因為JS引擎中CharacterData.prototype.data是共用的只有一個,那麼如果有多個text對象。text1.data,text2.data...它們都訪問原型上的這個data值顯然和實際情況每個文本節點對象有自己的文本衝突??
疑惑二:改變text.nodeValue的時候text.data值也會改變,反過來也一樣,一個的改變會引起另一個的改變。這個JS引擎中是怎麼實現的??
自己的思考:
如果把nodeValue和data當成指針而不是具體保存的某個特定值,當然nodeValue和data還是在原型屬性上的,當你訪問text1.nodeValue的時候原型上的nodeValue指向text1對應的文本值,訪問text2.nodeValue的時候原型上的nodeValue執行text2對應的文本....訪問data也是如此。在改變data的指向的時候nodeValue也要同時改變指向。
在知乎問了一圈DOM中為什麼文本節點的data屬性值的改變,文本節點的nodeValue值也會同步改變? 大神回答是Chrome在最近的版本中把DOM中的很多類實例變成了原型上的訪問器屬性https://groups.google.com/a/chromium.org/forum/m/#!topic/blink-dev/H0MGw0jkdn4。很遺憾我想的不完全正確,我能對這個東西產生疑問但思維卻沒跳出數據屬性的空間忘了居然還有訪問器屬性這個神奇的東西,於是我也不知道是第幾次再需要重溫《高程三》P141頁了,對屬性類型熟悉的同學可直接跳過相關知識點看分析~
相關知識點:
一張圖先說明屬性類型,數據屬性,訪問器屬性,四種特性之間的關係。
(1).數據屬性:包含一個數據值的位置,這個位置可以讀取或寫入值,數據屬性有4個描述其行為的特性。直接在對象上定義的屬性,它們的這四個特性值預設為true。通過Object.defineProperty(obj,'attr',{})設置某屬性,value為undefined,其餘特性為false
- [[Configurable]]:
a.表示能否通過detele刪除屬性從而重新定義屬性
b.能否修改屬性的特性
一旦把某屬性的configurable設置為false就再不能把其設置為可配置,此時再調用Object.defienProperty()修改除writable特性都會導致錯誤(註:value特性能不能通過Object.definedProperty()修改取決於writable,若writable之前就為true,那麼此時value可以更改,若writeable被更改為false,value則不能更改)。
c.能否把屬性修改為訪問器屬性 - [[Enumerable]]:
a.表示能否通過for-in迴圈返回屬性。輸出“new 1”是因為Chrome控制臺下基本會給每個語句都有返回值。。因為get函數里不是return 某個值,所以get函數返回undefiend。 - [[Writable]]:
a.表示能否修改屬性的值,示例參見上面。當某屬性的writable為false時,重新給該屬性賦值嚴格模式下會報錯,非嚴格模式會忽略。 - [[Value]]:
a.包含這個屬性的數據值,讀取屬性的時候從這個位置讀,寫入屬性值得時候把新值保存在這個位置。這個特性值預設為undefined。
註:IE8只能在DOM對象上使用Object.defineProperty()方法,而且只能創建訪問器屬性,由於實現不徹底建議還是不要再IE8中使用該方法。
(2).訪問器屬性:不包含數據值,它們包含一對getter和setter函數(不過不是必須的)。讀取訪問器屬性時候會調用getter函數,這個函數返回有效的值;寫入訪問器屬性時會調用setter函數並可以傳入新值,這個函數負責決定如何處理數據。對於直接在對象上定義的屬性,configurable,enumerable特性的預設值為true。
- [[Configurable]]:
a.能否通過detele刪除屬性從而重新定義屬性
b.能否修改屬性的特性
c.能否把屬性修改為數據屬性
註意不能同時設置數據屬性和訪問器屬性會報錯。 - [[Enumerable]]:
a.能否通過for-in迴圈返回屬性。 - [[Get]]:在讀取屬性時引擎自動調用的函數,非嚴格模式下不設置預設值為undefiend。
- [[Set]]:在寫入屬性時引擎自動調用的函數,預設值為undefiend。
訪問器屬性不能直接定義,必須使用Object.defineProperty()來定義。不一定非要同時指定getter和setter,只指定get意味著屬性不能寫,嘗試寫入屬性會被忽略,嚴格模式下報錯。只指定setter函數的屬性不能讀,嘗試讀屬性非嚴格模式下返回undefiend,經測試嚴格模式下好像並沒報錯
分析探討
其實這種關聯現象的出現還是有歷史原因的,需要FQ去閱讀知乎大神魯小夫推薦的這篇文章 Intent-to-Ship: Moving DOM attributes to prototype chains 。我簡單闡述一下我的理解吧(英語渣請看官見諒)~
追根溯源小科普:
13年左右,Chrome的JS引擎中DOM介面的這些屬性都還是被定義在DOM實例對象上的,以元素節點的id屬性為例,它是數據屬性而不是現在的訪問器屬性。Chromium團隊已經意識到這種表現不符合WebIDL規範,畢竟當時IE和火狐這些屬性早一年實現是在原型鏈上的,Chromium團隊覺得自己也應該把DOM屬性從實例上移到原型鏈上然後通過暴露getter/setter訪問這些屬性,既然是原型鏈上共用的屬性,數據屬性因為只有一個值不滿足多個實例對象訪問各自的id,所以訪問器屬性是個很好的選擇,因為它本質是函數,在函數裡面進行對應的處理就好。
屬性移到原型鏈上有什麼好處呢?
這樣能夠使開發人員掛鉤DOM屬性的getter/setter方法,這將提高DOM操作的可編程性;
因為是在原型上的getter/setter處理JavaScript邏輯,便於開發者操作JavaScript函數庫和覆寫預設的DOM屬性表現,開發者可覆寫提供的預設的getter/setter方法,實現自定義的邏輯;
從底層來說這也方便C++實現JS引擎提供的這種操作;
還能減少記憶體消耗(不然怎麼說V8引擎那麼快呢~~),提高性能,畢竟原型鏈上的屬性是所有實例共用的;
至於相容性方面,風險很低畢竟IE和FF已經實現了這種改變;
他們還問卷調查過哪些屬性建議移動,下圖是他們的目標
我的理解:
也就是說data和nodeValue現在是原型對象上的訪問器屬性,有一對setter和getter函數,可惜看不到getter/setter函數怎麼實現的處理邏輯,我就按自己的理解寫了下代碼思路後面再說。
嗯很好!這些屬性的特性值被設置為true,可配置可枚舉,給開發人員提供使用的靈活性,我可以重新定義自己的gettr/setter函數邏輯,讓id就添加在text身上,text.hasOwnProperty('id')返回true。
先從破壞data和nodeValue的關聯開始
//覆寫set/get函數,斷開預設set/get實現與nodeValue關聯的邏輯 Object.defineProperty(CharacterData.prototype,'data',{ configurable:true, enumerable:true, set:function(){}, get:function(){} });
var text=document.createTextNode('文本'); text.data;// undefined text.nodeValue;// "文本" /*創建text節點對象,訪問其data屬性返回undefiend,但是訪問nodeValue能返回"文本,因為我並沒覆寫nodeValue屬性的訪問器"*/
text.data='新文本'; text.hasOwnProperty('data');// false text.data;// undefined text.nodeValue;// "文本" /*還是返回false,看來處理delete text.data不在set函數中實現*/
delete CharacterData.prototype.data;// true CharacterData.prototype.hasOwnProperty('data');// false text.data='新新文本';// "新新文本" text.nodeValue;// "文本" text.hasOwnProperty('data');// true; /*從根源delete掉data屬性*/
預設的setter/getter函數怎麼實現關聯邏輯的,提供一下我的簡單思路,肯定沒預設的實現好,大家可以集思廣益一下互相探討~~
我猜想引擎中一直有個while(true)迴圈監聽著text.hasPrototype('data');當text.data="文本",可能執行如下操作
獲得開發者添加在text對象的data屬性值,改變CharacterData.prototype.data的值(預設調用data訪問器屬性的set方法)和Node.prototype.nodeValue的值(預設調用data訪問器屬性的set方法),delete掉本應該在text對象上的data屬性。
藉助於JS引擎預設實現的delete掉text實例上的data屬性,我用組合繼承模式實現了
(1)data的賦值操作在原型屬性上更新值而不會添加到自身屬性這一處理邏輯
代碼如下
//為了不和引擎中原生介面名衝突,命名採取相似僅為演示理解就好 //組合繼承 function Objectt(){ //... } function EventTargett(){ //... } //Node function Nodee(mydata){ if(this instanceof Textt){ CharacterDataa.prototype.data=mydata; EventTargett.call(this,mydata); //...需要給text添加屬性的話 } else{ Object.defineProperties(this,{ data:{ configurable:true, enumerable:true, set:function(protodata){ mydata=protodata; }, get:function(){ return mydata; } }, length:{ configurable:true, enumerable:true, set:undefined, get:function(){} }, previousElementSibling:{ configurable:true, enumerable:true, set:undefined, get:function(){} }, nextElementSibling:{ configurable:true, enumerable:true, set:undefined, get:function(){} } }); /*this.substringData=substringData; this.appendData=appendData; this.insertData=insertData; this.deleteData=deleteData; this.replaceData=replaceData; this.remove=remove;*/ } } Nodee.prototype=new EventTargett(); Nodee.prototype.constructor=Nodee; Nodee.prototype.__proto__=EventTargett.prototype; //CharacterData function CharacterDataa(mydata){ //給文本節點自身添加屬性 if(this instanceof Textt){ Nodee.call(this,mydata); //...需要給text添加屬性的話 } //原型上的屬性 else{ Object.defineProperties(this,{ wholeText:{ configurable:true, enumerable:true, set:undefined, get:function(){} } }); /*this.splitText=splitText; this.getDestinationInsertionPoints=getDestinationInsertionPoints;*/ } } CharacterDataa.prototype=new Nodee(); CharacterDataa.prototype.constructor=CharacterDataa; CharacterDataa.prototype.__proto__=Nodee.prototype; //Text function Textt(mydata){ CharacterDataa.call(this,mydata); //...如果需要給文本節點text自身添加屬性的話 } Textt.prototype=new CharacterDataa(); Textt.prototype.constructor=Textt; Textt.prototype.__proto__=CharacterDataa.prototype; //export介面 Document.prototype.createTextNode=function(mydata){ if(arguments.length==0){ return 'error'; } return new Textt(arguments[0].toString()); } //使用 var text1=document.createTextNode('hello'); text1.data;// "hello" text1.hasOwnProperty('data');// false; CharacterDataa.prototype.hasOwnProperty('data');// trueView Code
在實現過程中也遇到了些問題且存在一些局限性,如下
1.Character.prototype在初始化原型對象的時候需要執行一遍Node函數,new Text(arguments[0].toString())也會執行一遍Node函數,這導致Node函數中get雖然可以作為閉包來用作用域鏈中的mydata但卻不是真正想要的那個,即處在原型對象創建的作用域鏈中的get想要new Text(arguments[0].toString())創建的作用域鏈中mydata。後來我的解決方案就是在創建new Text(argumetns[0].toString())時候設置 CharacterData.prototype.data=mydata; 而訪問器屬性更好可以實現因為它是預設調用set()函數的,我在set函數中將mydata一保存這樣就更新Character.prototype初始化原型對象創建作用域鏈中的mydata值了!
2.如果不藉助JS引擎預設的delete操作,我只能事先在代碼中創建好text對象,暫時還沒有動態創建節點的代碼處理思路。
var text=document.createTextNode('文本'); while (true) { if(text.hasOwnProperty('data')){ CharacterData.prototype.data=text.data; delete text.data; } }
3.不能滿足多個text實例,set/get邏輯還需強化不然data真成共用的值了,真的要用到指針指向??
4.還未實現和nodeValue的關聯,DOM的API體系太龐大,我得慢慢研究啦,先就這麼多我把代碼放到GitHub,後續如果有興趣可以關註下啦~如果大家有什麼想法歡迎探討,我也想多學習學習!