別真以為JavaScript中func.call/apply/bind是萬能的!


自從學會call/apply/bind這三個方法后我就各種場合各種使用各種得心應手至今還沒踩過什么坑,怎么用?說直白點就是我自己的對象沒有某個方法但別人有,我就可以通過call/apply/bind去調用執行別人家的方法,不太懂具體用法的同學可移至MDN學習一下Function.prototype.call() Function.prototype.apply() Function.prototype.bind() ,本文不講解使用,但是這三個方法並不是萬能的,並不一定會執行你想要的那個函數,因為可能要看函數中上下文對象的某些屬性特性值允許不允許改變,今天這個坑我就深深踩了一下,並探索了一番。之所以說並不一定是因為確實有部分類數組可以傳入call/apply/bind且成功調用,比如arguments(雖然它是[object Arguments]類型的,但__proto__卻指向Object.prototype),感興趣的可以試試。

事件起因
在學習HTML5與類相關的擴充其中classList屬性時候,我了解到所有元素有classList這個屬性,並且這個屬性是新集合DOMTokenList的實例,在DOMTokenList.prototype上有一個add(value)方法,用來將給定字符串值添加到DOMTokenList實例列表中,並且即時反應到文檔頁面。如果在列表中value值已存在,就不添加了。我手賤尋思着自己用JS代碼把這個add方法大概實現一下(肯定沒人家JS引擎實現的好,我就實現個思路),但就這一實現才發現的這個坑。
(1).先來看這個add方法JS引擎給它設置的屬性特性值如何,是可寫的:

(2).然后實現我們的add函數

Object.defineProperty(DOMTokenList.prototype, 'add', {
   configureable : true,
   enumerable : true,
   writable : true,
   value : function(value){ 
      if([].indexOf.call(this, value)>=0) return;
      //加到classList類數組中
      [].push.call(this, value);
  }
});

是不是感覺沒什么錯,我就是想把'classA'加入到DOMTokenList實例列表中,然而當測試后控制台會報錯


分析探討
這個報錯內容為"類型錯誤:不能設置改變[object Object]的那個特性值只有getter的length屬性"。意思就是[object Object]就是這里的DOMTokenList.prototype,你換成childNodes它也同樣會給你提示出錯。

這里push方法因為會改變數組的長度length,而且不止push,pop,unshift,shift的執行也會改變數組長度,經測試它們會報出同樣的錯。這里的DOMTokenList.prototype.length獲取它的特性值為:

length的訪問器屬性中set特性被引擎設置為undefiend了,怪不得不能將value加到document.body.classList類數組中,因為引擎沒有給你提供set的接口函數啊,只給你提供get特性函數返回length長度。而我們平常使用的這個不會報錯是因為length屬性的value值是可變的,注意這里其實是每個數組實例都有自己的length屬性。

不過好在DOMTokenList.prototype的length屬性configurable特性是true,意味着我可以自己寫length的set函數。現在整個過程就是:當實現我自己的add函數,因為add函數中調用到push操作,當執行push操作時會更新DOMTokenList.prototype.length(自動調用set函數),我可以在set函數中執行相關處理,先拿console測試一下是不是這個流程

Object.defineProperty(DOMTokenList.prototype, 'length', {
  set : function(){console.log('執行了')}
})


確實是的,而且現在不報錯了。但是我現在做的只是皮毛,'classA'還沒有加到document.body.classList中去呢,看看列表里還只是"document":

不知道push函數引擎是怎么實現的,這說明JS引擎好像就沒執行push函數,此方法行不通...不清楚它為什么不執行push操作??

那還是乖乖轉化為數組再處理吧。常見的是將類數組轉化為真正的數組再做相關處理,方法挺多比如Array.from(),Array.prototype.slice.call()等等,不過這會改變原來類數組的類型啊,我就想問怎么樣處理能給類數組添加項而不改變類數組的類型。數組實例的類型由它的__proto__決定,那就好辦了,不過得先設置classList屬性的一些特性項,JS引擎給的set是undefiend

最后重寫下來為:

Object.defineProperty(DOMTokenList.prototype, 'add', {
   configureable : true,
   enumerable : true,
   writable : true,
   value : function(value){ 
      if([].indexOf.call(this, value)>=0) return;
      //加到classList類數組中
      var newarr = Array.from(this);
      newarr.push(value);
      newarr.__proto__ = DOMTokenList.prototype;
      document.body.classList = newarr;
  }
});

Object.defineProperty(Element.prototype, 'classList', {
  set: function(value){
    console.log(value);
    //這里怎么處理不同元素的classList值是JS引擎的事,我實在是不會了
  }
})

不過這樣寫有點固定具體元素了繼續重寫為:

Object.defineProperty(DOMTokenList.prototype, 'add', {
   configureable : true,
   enumerable : true,
   writable : true,
   value : function(value, ele){ 
      if([].indexOf.call(this, value)>=0) return;
      //加到classList類數組中
      var newarr = Array.from(this);
      newarr.push(value);
      newarr.__proto__ = DOMTokenList.prototype;
      ele.classList = newarr;
  }
});

Object.defineProperty(Element.prototype, 'classList', {
  set: function(value){
    console.log(value);
    //這里怎么處理不同元素的classList值是JS引擎的事,我實在是不會了
  }
});

//使用
document.body.classList.add('classA', document.body);//["classA"]

 

總結
受限於JS引擎中元素實例的某些屬性是共享於其原型屬性的屬性值的set函數,雖然最后也沒有全部實現add函數的功能,我是實在不知道JS引擎中怎么實現的set,get函數從而保證不同實例共享原型上同一屬性而且還保證不同實例的該屬性值不一樣,這估計得看引擎源碼了,心累...有知道的同學可以說一下思路嗎??還有對於我上面的分析部分我有兩點疑問:知道的大神求科普!!
(1).JS引擎好像就沒執行push函數,此方法行不通...不清楚它為什么不執行push操作
(2).怎么樣處理能給類數組添加項而不改變類數組的類型


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM