zoukankan      html  css  js  c++  java
  • 一些有趣的Javascript技巧

    整理一些刷题时学会的小技巧……

    目录:

    1. 即大于0又小于0的变量
    2. String.split() 与 正则表达式
    3. 缓存的几种方法
    4. 初始化一个数组

    即大于0又小于0的变量

    问题: 设计一个变量val,使得以下表达式返回true:

    val<0 && val>0;
    

    逻辑上来说一个数是不可能即大于0又小于0的。我们能做到的只是让一个变量一会大于0一会小于0.那么在这个表达式里是否存在先后顺序呢?答案是肯定的。我们先判断val是否大于0,然后才判断它是否小于0.不过对于普通的变量而言,这个顺序没有任何意义:普通变量的值不会因为你读取了它而发生变化。那么,什么样的变量会因为读取而发生改变呢?

    没错,就是对象的访问器属性。

    我们可以声明一个对象,并给其赋予一个数据属性,比如 _value = -3, 然后再给它设置一个访问器属性 val, gettar参数设置为:当我读取 val属性时,_value的值加2,并将 _value 的值返回。这样一来当我第一次判断 val<0 时,返回值为 -3+2=-1, 成立。进行第二次判断时,再一次读取了它,这次的返回值变成了 -1+2>0, 也成立,于是表达式为真。具体代码如下:

    var obj = {
        _value: -3
    };
    
    Object.defineProperty(obj, "_val", {
        get: function() {
            this._value += 2;
            return this._value;
        }
    );
    
    obj._val<0 && obj._val>0;
    // return true
    

     到这里基本完成了任务。不过这个方法还不够完美。为什么?看下这个问题的升级版:

    问题:设计一个变量,使得以下函数返回true:

    function foo(val) {
        return val<0&&val>0;
    }
    

    看上去没怎么变对不对?可是如果这时候把上面的变量放进去,像这样:

    foo(obj.val); // false
    

     是不行的。因为函数的参数是按值传递的。这样调用只会在传入参数时读取一次obj.val的值,随后的比较表达式里只会将这个值复制过去进行比较,而不会读取obj的属性,也就不会触发gettar函数。这种情况下,val的值最终一直都是-3+2=-1,所以无法通过测试。

    解决办法是传入一个对象而不是一个值。可是传入对象后,怎么比较对象和数值呢?答案是Object.toString()方法。

    当我们试图比较一个对象和一个数的大小时,会调用对象的toString()方法,并取返回值与数字比较。通常情况下对象的toString方法返回值并不是数字,所以无法比较。在这里,只要重写目标对象的ttoString方法,令它返回obj._value即可。当然,也需要用到选择器属性。

    Object.defineProperty(obj, 'toString', function(){
       get: function() {
           this._value += 2;
           return this._value;
    });   
    

     如此一来,当我们传入对象到函数参数时,并不会读取toString方法,_value保持原始值。直到比较数字和它的大小时,才调用toString并返回相应的值。大功告成。

    这个特性有什么用呢?暂时没想到……不过至少可以加深对于对象属性的理解。

    String.split()与正则表达式

    问题:将字符串“JavascriptIsSoInteresting”分割为["Javascript","Is","So","Interesting"]

    字符串的split方法大家都很熟悉,我们可以传入一个字符串参数以对目标字符串进行分割,比如:

    var str = "a=2&b=3&c=4";
    console.log(str.split('&'));
    // ['a=2','b=3','c=4']
    

     有过进一步了解的话,还可以知道它的参数可以是正则表达式,比如:

    var str="a=2&b=3#c=4";
    var reg = /[&#]/g;
    str.split(reg);
    // ['a=2','b=3','c=4']
    

     另外,split还可以接受第二个参数以决定分割的长度,这个比较简单就不说了。

    现在回到问题。要分割题目中的字符串,我们缺少一个分割符。那么一个很直观的解决方法就是,在每个大写字母前面插入一个分隔符,然后再调用split方法。

    var str="JavascriptIsSoInteresting";
    var reg = /([A-Z][a-z])/g;
    str.replace(reg, '&$1').split('&');
    // ["", "Javascript", "Is", "So", "Interesting"]
    

     有点瑕疵,前面多了一个空字符串,需要再处理下。不过基本的目标算是达成了。

    有没有更好的方法呢?

    在这之前我们先考虑一个问题:在用split分割字符串的时候,如果字符串仅仅由分割符组成,结果会是什么?比方说,对字符串“aaaaa”,进行split('a')的处理,会得到怎样一个数组?来看下:

    var str="aaa";
    str.split('a');     // ['', '', '', '']
    

     结果是由a之间的空字符串组成的新数组,也就是说当分隔符连续出现时,split会把“间隔”作为成员分配到数组中。

    用正则表达式试试看:

    var str="aaa";
    var reg = /a/g;
    str.split(reg);    // ['', '', '', '']

     结果是一样的。到目前为止一切都很正常。接下来的这个特性才是我们需要用到的。

    我们对上面的代码做一点小修改:

    var str="aaa";
    var reg = /(a)/g;
    str.split(reg);     // ["", "a", "", "a", "", "a", ""]
    

     很奇怪对不对?我只是将正则表达式用子表达式的括号括了起来,理论上没有使用子表达式的情况下应该和上面的没什么区别,但是当它跑到split里面时,奇迹就出现了:不仅原先的分割结果还在数组里,本该不存在的分隔符也回来了。

    经试验证明,当split的参数是正则表达式,并且正则表达式里包含了子表达式,那么子表达式内的分隔符将保留在结果数组中,而不是通常的忽略。

    将这个神奇的现象应用到题目中,我们把“一个大写字母加上若干个小写字母”作为分隔符,并给它加上括号:

    var str="JavascriptIsSoInteresting";
    var reg = /([A-Z][a-z]+)/g;
    str.split(reg);
    // ["","Javascript","", "Is", "", "So", "", "Interesting"]
    

     最后还需要去除空字符串,可以使用filter方法:

    str.split(reg).filter(function(val){return /S/.test(val);});;
    

     或者还可以更优雅一些:

    str.split(reg).filter(Boolean);
    

    这个特性的用处么,比如你想写一个代码解释器,对于 “1+20+x+y”这样的输入,可能需要将它分解成["1","+","20","+","x","y"]时,就可以这么办了。

    缓存的几种方法

    问题:令以下函数返回true:

    function foo() {
        return Math.random()*Math.random()*Math.random===0.5;
    }
    

    显然这个函数几乎不可能返回true。其实Math.random()只是个幌子,为了达到目的我们势必要重新Math.random()。问题就在于,怎么写。

    很简单地,只需要写一个函数,返回值是0.5的三次根式就可以了。不过这个返回值不太好求。所以我决定定义一个函数,第一次调用时返回0.5,之后每次调用返回都是1.

    var val = 0;
    function m() {
      if(val != 1) val += 0.5;
      return val;
    }
    

     当然,全局变量是魔鬼,所以需要把val封装在一个闭包里:

    function v() {
      var val = 0;
      return function() {
        if(val != 1) val += 0.5;
        return val;
      }
    }
    
    var m = v();
    m()*m()*m();
    // return 0.5

     这就是第一种方式。

    其实说道这里是不是觉得有点熟悉?没错这和我们的第一个问题其实很相似,同样可以用对象的访问器属性解决。

    var obj = {
      _value: 0
    };
    Object.defineProperty(obj,'val',{
      get:  function(){
          if(this._value != 1)
            this._value+= 0.5;
          return this._value;
      }
    });
                          
    var i = function() {
      return obj.val;
    }
    m()*m()*m();
    // return 0.5
    

    最后一种方法,我们可以不用全局变量,也不用闭包。把函数本身看成一个对象即可:

    function m() {
      if(this.val != 1) this.val+=0.5;
      return this.val;
    }
    
    m.val = 0;
    m()*m()*m()
    // return 1

    到目前为止这题貌似和缓存没有多大关系。其实只要把上面作为存储数据的value改成一个数组/对象,对value的操作改成为其添加一个元素,那么它就可以作为缓存使用了。

    初始化数组

    问题:声明一个长度为给定值的数组,并初始化所有元素为0.

    当然,这可以用for循环来做:

    var arr = [];
    for(var i=0; i<n; i++) {
        arr[i] = 0;
    }
    

    不过我们还可以做得更酷一点:

    var n = 4;
    arr = Array(n+1).join('0').split('').map(Number);
    // [0,0,0,0]
    

    像这样,用一行语句就初始化了一个全为0的数组。

    不过有个缺点:如果我想初始化的值不是个位数,比如说都是12呢?

    很简单,记得上面说过的split方法了么?可以这么做:

    var n = 4;
    arr = Array(n+1).join('12').split(/(12)/).map(Number).filter(Boolean);
    // [12,12,12,12]
    

     其中正则部分还可以稍微改一改,改成以长度划分,会更优雅些:

    var n = 4;
    arr = Array(n+1).join('1222').split(/(d{4})/).map(Number).filter(Boolean);
    // [1222,1222,1222,1222]
    

    上面的方法后来考虑了一下,感觉有点绕,为何我要把一个Array先分割成字符串然后再合并成数组呢?不能直接就用map么,像这样:

    Array(n+1).map(function(){return 0;});
    

     试了下发现不行,对于用Array(n)这种形式创建的数组,不管怎么用map每个元素都仍然是undefined。

    但是,

    这个思路还是有拓展的余地的,比如如果我们要声明一个4*4的二维数组,可以这么做:

    var n = 4;
    var arr = Array(n+1).join('0').split('').map(function(v){
      return Array(n+1).join('0').split('');
    });
    

     更多维数可以继续嵌套下去……

    最后要考虑的问题是:这样做虽然很酷,但是有没有必要呢?引用了一大堆方法感觉速度会很慢。

    试一试:

    var arr1 = [];
    var arr2 = [];
    var n = 1e6;
    
    console.time("for");
    for(var i=0; i<n; i++)
      arr1[i] = 0;
    console.timeEnd("for");
    
    console.time("Array");
    arr2 = Array(n+1).join('0').split('').map(Number);
    console.timeEnd("Array");
    

     输出是:

    for: 944.83ms
    Array: 170.69ms
    

     哎哟性能也不错的样子。上面是在Firebug下运行的数据。切换到本地node.js试试看:

    for: 22.448ms
    Array: 124.617ms
    

     ……

    所以在浏览器端运行代码时,放心大胆地用吧,浏览器对这些原生方法做的优化简直不要太厉害。

    而如果是在后端运行的,或者想用这些方法糊弄过leetcode的代码复杂度检测的(比如我),放弃这些想法吧……

    篇幅有点长了……就此打住……

  • 相关阅读:
    VBScript学习笔记
    C#调用C++库知识点
    .Net面试经验,从北京到杭州
    杭州.Net 相关大公司,希望对大家有帮助
    一起学习《C#高级编程》3--运算符重载
    一起学习《C#高级编程》2--比较对象的相等性
    一起学习《C#高级编程》1--类型的安全性
    博客园的第一天
    基于SpringCloud+Kubernetes 微服务的容器化持续交付实战
    第一节:Docker学习 — 安装
  • 原文地址:https://www.cnblogs.com/kindofblue/p/4999701.html
Copyright © 2011-2022 走看看