logo
标签编程技术

JavaScript小技巧

使用数组解构操作符交换变量

面试中,交换两个变量的问题很像一个面试官跟你打开话题的小游戏。传统的方法是使用一个临时变量来完成交换:

var tmp = a;
a = b;
b = tmp;

对于数字类型的变量,还有一些更为巧妙的方法,例如利用按位异或运算:

a = a ^ b;
b = a ^ b;
a = a ^ b;

不过,ES6引入的数组解构赋值更优雅:

[b, a] = [a, b];

这种方法首先创建一个包含变量 ab 的数组,然后通过解构赋值,将 b 赋值给 a,反之亦然。虽然性能上可能略逊一筹——因为涉及到创建数组——但绝对可以在日常开发中放心使用。

校验器

在处理用户输入或外部数据的时候,校验器非常有用。最简单的验证器是接受一个字符串输入并返回一个布尔值的函数。例如,下面是两个验证数字的不同方面的函数:

function isNumeric(n) {
    return !isNaN(parseFloat(n)) && isFinite(n);
}

function isPositive(n) {
    return parseInt(n) > 0;
}

console.log(isNumeric("5")); // true
console.log(isNumeric("abc")); // false
console.log(isNumeric("5g")); // false
console.log(isNumeric("0")); // true
console.log(isPositive("0")); // false
console.log(isPositive("abc")); // false
console.log(isPositive("5g")); // false

函数式编程思想中的“组合(combine)”非常适合创建多重校验器。如果我们想要创建一个函数来验证给定的字符串既是数字又是正数,我们可以使用函数 combine,只有当所有通过的验证器都返回 true 时,才会返回 true

const combine = (...fns) => (val) => fns.reduce((memo, fn) => memo && fn(val), true);

const combined = combine(isNumeric, isPositive);
console.log(combined("5")); // true
console.log(combined("0")); // false
console.log(combined("5g")); // false

因为验证器的类型都是相同的 (string) => boolean,所以可以随意组合:

function lessThan5(n) {
    return parseInt(n) < 5;
}

const evenMoreCombined = combine(combined, lessThan5);
console.log(evenMoreCombined("4")); // true
console.log(evenMoreCombined("6")); // false

通过这种方式,可以轻松地从简单的验证器中创建任意复杂的验证器。

<wbr/>来断句

使用 <wbr/> 标签可以在不影响语义的情况下插入断点,使长文本能够在适当的位置进行换行。这对于长URL或者没有自然断点的文本特别有用。例如:

Lorem_ipsum_dolor_sit_<wbr/>amet_consectetur_

这样,浏览器会在适当的位置进行换行,而不会破坏单词的连续性。这种方法比较灵活,可以根据需要在任何地方插入断点。在多国语言使用同一套样式的时候这个方法尤其有用。

如何取消Promise(如何解决请求竞争)

在原生 Promise 中,虽然没有提供内置的取消功能,但我们可以通过手动处理来模拟取消行为。例如,在一个场景中,当用户在一个 <select> 元素中进行选择时,会从远程服务器获取一些数据。但是如果用户快于数据获取的速度进行多次选择,之前的请求可能会缓慢返回时污染我们的界面,这就是“请求竞争”问题。

解决方案1:比较选中的元素值 最简单的方法是在每次数据获取时,检查当前选中的元素值是否与请求数据时的元素值相同。如果不同,就丢弃这次获取到的数据。

longFetch(value).then((res) => {
    if (selectElement.value === value) {
        // 显示结果
    }
});

但是,如果用户快速地选择了不同的选项,用户切换和结果到达的时间差很小,可能会导致多次请求并且页面会刷新,可能会出现闪烁的问题。

解决方案2:使用时间戳进行比较 一个更可靠的解决方案是在发起数据请求之前记录当前的时间戳,然后在获取数据后,检查时间戳是否与之前记录的时间戳相同。如果不同,则说明数据请求已经过期,应该丢弃结果。

let lastFetchTimestamp;
// ...
const currentTime = new Date().getTime();
lastFetchTimestamp = currentTime;
longFetch(value).then((res) => {
    if (lastFetchTimestamp === currentTime) {
        // 显示结果
    }
});

通过这种方法,我们可以确保只有最后一次有效的数据请求结果会被处理,而之前的结果会被丢弃。

检查变量是否为 undefined

检查变量是否为 undefined 挺常见的,也有许多方法可以实现。但是需要认真考究以下各种方法的异同。

比较好的方法:

  • 使用 === 运算符和 void 0 检查未定义:
let variable;
console.log(variable === void 0); // true

这种方法几乎不会出错。但是缺点也很明显,不包含明确的 undefined 字样,不够直观。

  • 使用 typeof 检查:
let variable;
console.log(typeof variable == "undefined"); // true

这种方法看似很好,但是有个陷阱,变量没有被声明也会返回 true

其他人可能会这么写:

  • 检查变量是否为假值:
if (variable) {
    // ...
}

这种方法通常用于检查变量是否为假值,但对于检查是否为 undefined 来说有一些不足,他混淆了很多值(如 0""nullNaN 等),有时候一些有意义值如 0 也会被挡住。

  • 使用 === 运算符严格检查:
let variable;
console.log(variable === undefined); // true

这种方法更严格了,排除了 0,但可能会受到全局作用域中 undefined 被修改的影响。

比较奇葩最好不要这样写的方法:

  • 声明一个变量但不初始化它:
let realUndefined;
let variable;
console.log(variable === realUndefined); // tru

这种方法有点奇怪,但是原理与 void 0 类似,这里的缺点是需要声明一个新的变量,我们肯定不会这样写。

顺序运行Promise

通常情况下,创建多个 Promise 的时候,它们会并行执行。但有时候我们可能更希望它们按照顺序执行,例如比如 一个Promise 依赖于另一个 Promise 的结果。

以下是如何严格按顺序运行 Promise 的方法:

const makePromise = (e) => {
    // 返回一个 Promise
    // 异步操作
};

const list = [/* 结果 */];

list.reduce((prev, e) => {
    return prev.then(() => makePromise(e));
}, Promise.resolve()).then(() => {
    // 当所有 Promise 解析时执行的操作
});

这里的 reduce 方法会依次处理列表中的每个元素,并在前一个 Promise 完成后创建下一个 Promise。这样,它们就会严格按顺序运行。

要收集结果,可以稍作修改:

list.reduce((prev, e) => {
    return prev.then((partial) => {
        return makePromise(e).then((result) => {
            // 在每次 Promise 解析时收集结果
            partial.push(result);
            return partial;
        });
    });
}, Promise.resolve([])).then((results) => {
    // 所有 Promise 都解析后执行的操作,results 是所有结果组成的数组
});