精妙短小的JavaScript代码片段和题目

概述

在网上看到的几个精妙短小的JavaScript代码片段,总结分享一下。

判断是否是闰年

知乎上看到的一个例子

1
2
3
4
function isLeapYear(year) {
return new Date(year, 1, 29).getDate() === 29;
}
console.log(isLeapYear(2016)); // true

这里用到了一个小技巧,即以某年2月29号为年月日参数构建一个Date对象时,若该年不存在29号,则构建的日期会往后延一天到3月1号,然后通过getDate()方法判断当前是20号还是1号,就可以判断是否为闰年了。

同样我们可以进行类似的扩展。

1
2
3
4
function daysLength(month, year) {
return new Date((+ new Date(year, month + 1, 1, 0, 0, 0)) - 1000).getDate();
};
console.log(daysLength(1, 2016)); // 29

上面的代码用来求某年某月有多少天,我们构建一个某年某月的下个月的一号凌晨零点整往前延一秒的日期,即本月最后一天的最后一秒的日期,调用getDate()方法即可得到本月的最后一天。

var和let的区别

之前在写ES6总结的时候就写过,let与var最大的不同就是,let是存在块级作用域的,且变量不会被提升(Hoisting)。

StackExchange上看到的例子,略加修改,数行代码完美诠释块级作用域和函数作用域的区别,很精美。

1
2
3
4
5
6
7
8
9
const foo = [];
(function() {
'use strict';
for(let i = 0; i < 5; i ++) {
foo.push(() => i);
}
}())
foo.map(bar => bar()); // [0, 1, 2, 3, 4]
// 如果将for循环中的let改为var,则结果为[5, 5, 5, 5, 5]

这里涉及的知识点有,IIFE(立即执行函数表达式),闭包,箭头函数,以及块级作用域和函数作用域,深入一点的话还有作用域链。

首先我们注意到,IIFE中有一个循环,循环变量使用let定义,这表示每个let都有一个独立的块级作用域,然后注意这里往数组中添加的是一个函数,该函数返回当前的循环变量。

在代码的最后一行中,调用了数组的map()方法,对数组中每个函数都进行了调用。实际上当返回循环变量的箭头函数一次一次地被推入数组时,每次都创建了一个闭包,这个闭包的作用域就是for循环的那个块级作用域,每一个数组元素都保留了这个块级作用域,每个块级作用域的循环变量的当前状态都在闭包中被保存了下来。

因此输出的结果是[0, 1, 2, 3, 4]。

而当使用var定义循环变量时,不存在块级作用域,闭包保留的作用域是每次循环都共享的匿名函数的函数作用域,等循环结束,循环变量已经是5了,而且被五个闭包所共享。

事实上根据作用域链的知识,这里在使用let时,块级作用域之上才是匿名函数的函数作用域。

数组map方法的一个主意事项

1
2
['1','2','3'].map(parseInt);
// [1, NaN, NaN]

上述代码的执行结果是[1, NaN, NaN],当然大多数人会以为是[1, 2, 3]。

这个题是对map()方法和parseInt()函数的一个误用,实际上parseInt()函数可以接收两个参数,第一个表示待转换的数字字符串,第二个表示字符串表示的进制。无独有偶,map()方法接收的作为参数的函数第一参数是当前数组元素的值,第二个参数是当前数组元素的下标,第三个参数是整个数组(这里没有被用到)。

因此实际调用就变成了如下所示。

1
2
3
parseInt('1', 0, ['1', '2', '3']); // 将'1'转换为十进制,结果为1
parseInt('2', 1, ['1', '2', '3']); // 第二个参数不能为1,结果为NaN
parseInt('3', 2, ['1', '2', '3']); // 将'3'转换为二进制,结果为NaN

根据文档,parseInt()函数的第二个参数应该在2到36之间(含2和36),实际上当该参数为0时,会被当做十进制处理。除此之外的进制数被传入时都会返回NaN。

所以这实际上是一个小坑,这行代码忽略了map()方法传入的第二个参数和parseInt()函数接收的第二个参数。

将一个N维数字数组降维到1维

1
2
3
const arrN = [1, [2, 3], [4, 5, [6, 7, [8]]], [9], 10];
const arr1 = arrN.toString().split(',').map(val => + val);
console.log(arr1);

这里利用到了对象的toString()方法会递归应用到子对象的小技巧。

使用科学计数法处理比较大的数字

1
2
3
4
5
6
7
8
let i = 0;
const interval = setInterval(() => {
if (i++ < 10) {
console.log(`${i}: Hello World`);
} else {
clearInterval(interval);
}
}, 1e2);

非常实用,现在这种情况我基本都用科学计数法了,比数一堆零舒服多了。

void 0和undefined

1
let foo = void 0; // undefined

曾几何时一些老浏览器是可以直接给undefined赋值的,如下所示:

1
var undefined = 1;

虽然现代浏览器不存在这个问题,不过参考6还给出了一个令人惊讶莫名的理由:void 0比undefined更短,在超大工程中,能节省一点点空间,如果是前端代码,还能节省带宽。这种想法太极端,仁者见仁智者见智吧。

参考

  1. 怎样判断闰年?怎样获取月份相应天数(包括平年和闰年)? - 回答作者: 方应杭 - 知乎
  2. Is there any reason to use the “var” keyword in ES6? - StackExchange
  3. what is [‘1’,’2’,’3’].map(parseInt) result - StackOverflow
  4. 有哪些短小却令人惊叹的 JavaScript 代码? - 回答作者: 许小言 - 知乎
  5. 有哪些短小却令人惊叹的 JavaScript 代码? - 回答作者: 沈嵘 - 知乎
  6. What does ‘void 0’ mean? - StackOverflow