揭秘DOM中data和nodeValue屬性同步改變那些事

来源:http://www.cnblogs.com/venoral/archive/2016/04/05/5353299.html
-Advertisement-
Play Games

問題引發:最近在整理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');// true
View 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,後續如果有興趣可以關註下啦~如果大家有什麼想法歡迎探討,我也想多學習學習!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 現在編程的主流語言基本上都是面向對象的。如C#,C++,JAVA。我們在使用時,已經構造了一個個的類。但是往往由於我們在類內部或外部的設計上存在種種問題,導致儘管是面向對象的語言,卻是面向過程的邏輯,甚至維護起來異常困難。每次增加或修改功能都要改動很多的代碼,如履薄冰。而面向對象的六大原則主要的目的 ...
  • 一、設計目的 從事.Net平臺開發系統已有8年多了,一直思考搭建.Net分散式系統架構。基於window平臺搭建的大型分散式系統不多,之前瞭解過myspace、stackoverflow等大型網站。搭建一個大型平臺需要綜合考慮很多方面,不單純是軟體架構,還包括網路和硬體設備等。由於現代大部分應用建設 ...
  • 本文介紹一個jquery的小技巧,能讓任意組件對象都能支持類似DOM的事件管理,也就是說除了派發事件,添加或刪除事件監聽器,還能支持事件冒泡,阻止事件預設行為等等。在jquery的幫助下,使用這個方法來管理普通對象的事件就跟管理DOM對象的事件一模一樣,雖然在最後當你看到這個小技巧的具體內容時,你可... ...
  • <select>: 1 2 3 4 5 6 7 8 9 10 奪得2008年歐洲杯冠軍的國家是: <select name="nation" id="nation"> <option value="">請選擇</option> <option value="Germany">德國</option> ...
  • 從事前端開發工作也有一定的時間了,在這段時間里,由一個基本的前端開發開始,做到前端經理;基本上算是走過了所有前端開發都走過的路,今天主要分享下我這一路走來,對前端的理解。 我開始接觸前端的時候,其實稱不上是所謂的開發,更多的是現在多數人對前端的理解-美工,當時負責的工作就是將PSD轉化為HTML頁面 ...
  • 通常對於無刷新提交表單,我們都是運用ajax實現的。前段時間跟著老大瞭解到另一種無刷新提交表單的方法,是利用iframe框架實現的。現在整理出來分享給大家。 第一種: (html頁面) <!DOCTYPE HTML> <html lang="en-US"> <head> <meta charset= ...
  • 本節教程將介紹如何用DeviceOne簡單而高效的完成一個新聞頁面。 導入項目 數據模板分離MVVM模型 自定義事件 展示新聞 九宮格展示 將要學習的demo效果圖如下所示 1. 導入完整項目 本節示例demo請參考下載地址,可以導入到設計器中學習。 為了方便大家理解頁面結構,請參考下圖圖中紅框所示 ...
  • 流程式控制制 順序、分支、迴圈 順序結構 代碼一行一行從上往下執行並解析 分支結構 if語句 switch語句 if語句 單分支 if(條件表達式){ //語句塊 } 含義:當條件表達式為真的時候就執行裡面的語句塊 示例: 雙分支: if(條件表達式){ //語句塊1 }else{ //語句塊2 } 含... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...