一些有趣的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的代码复杂度检测的(比如我),放弃这些想法吧……

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

原文地址:https://www.cnblogs.com/kindofblue/p/4999701.html