别真以为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).怎么样处理能给类数组添加项而不改变类数组的类型

posted @ 2016-05-06 20:11  venoral  阅读(1090)  评论(6编辑  收藏  举报