跳转至

\(JavaScript\) 语法糖及语法坑

作者:光火

邮箱:victor_b_zhang@163.com

  • 先简单谈一谈自己创作这篇文章的初衷吧。作为一名零基础的科班生,我步入大学后最先接触的编程语言是C/C++。尽管随后又陆陆续续学习了数不尽的语言和框架,但是理解最为深刻的还是C/C++。不过,由于项目所需和个人兴趣使然,我在大二暑期开始涉足 Web 前端、微信小程序等领域。在这里,JavaScript 是绝对王者。因此,本文中,笔者尝试从自身角度出发,简要地总结一下个人在学习与使用 JavaScript 过程中遇到的语法糖及语法坑,还望各位读者批评指正,各倾陆海。(持续更新,建议追踪)

语法糖

可选链

  • 有些时候,我们希望特定的调用在属性、方法不存在时返回undefined,而非报错,这时我们就可以考虑使用可选链?.
  • ?.会检查左侧是否为null/undefined,如果不是则继续运算,否则就返回undefined
    let player = {
        name: 'Alice',  
    }
    
    console.log(player?.birthday?.month); // undefined
    

类型转换

  • + 可以发挥 Number()的作用;
  • !!可以发挥 Boolean() 的作用;
    let a = '2';
    let b = '4';
    console.log(+a * +b);  // 8
    console.log(!! null);  // false
    console.log(!! a);     // true
    

数值提取

  • 尽管对于字符串相关的问题,正则表达式几乎是通杀,但是在此还是要特别介绍一下parseIntparseFloat函数。它们会从头开始在字符串中读取数字,倘若发生 error,则返回已收集到的数字。
    console.log(parseInt('100'));        // 100
    console.log(parseInt('100px'));      // 100
    console.log(parseFloat('12.5em'));   // 12.5
    
    console.log(parseInt('a12345'));     // NaN
    console.log(parseFloat('XYZ14.6'));  // NaN
    
  • 可见,parseIntparseFloat很擅长做一些去除单位,保留数值的工作;
  • 当然,parseInt还有第二个参数,即parseInt(str,base) ,它可以将字符串 str 解析为给定的 base 数字系统中的整数,其中2 ≤ base ≤ 36;(36即0-9 + A-Z)
    console.log(parseInt('100', 2));    // 4
    console.log(parseInt('ff', 16));    // 255
    console.log(parseInt('0xff', 16));  // 255
    

数组合并

let a = [1, 7, 4, 9, 6, 1];
let b = [2, 1];
a.push.apply(a, b);
- 选择较长的数组作为 a ,较短的数组作为 b,这样性能会比较好; - 经过上述操作后,a 就是合并后的数组,内容为 [1, 7, 4, 9, 6, 1, 2, 1]; 也可以根据具体需求,使用 ES6 的语法:
let a = [1, 7, 4, 9, 6, 1];
let b = [2, 1];
c = [...a, ...b]; // 生成一个新的数组
a.push(...b);     // 直接在原数组进行操作
- 经过上述操作后,ac 的内容均为 [1, 7, 4, 9, 6, 1, 2, 1]

清空数组

  • JavaScript中,数组的length属性是可写的,如果我们手动减少它,数组就会发生截断,且该过程不可逆。因此,倘若我们需要清空数组,可以简单使用arr.length = 0
    let base = [1, 2, 3, 4, 5, 6];
    
    base.length = 0;
    console.log(base);      // []
    
    base.length = 6;
    console.log(base[2]);   // undefined
    

splice方法

  • JavaScript中,splice方法几乎可以完成对数组的任意操作,如添加、删除、替换、插入元素,该方法会改变原始数组:
    let base = [1, 2, 3, 4, 5, 6];
    base.splice(1, 2); // 自索引 1 开始, 删除 2 个元素
    console.log(base); // [1, 4, 5, 6]
    
    base.splice(1, 0, 2, 3); // 自索引 1 开始, 删除 0 个元素, 插入元素 2 和 3
    console.log(base); // [1, 2, 3, 4, 5, 6]
    
    base.splice(-1, 0, 'hey'); // 支持负索引, -1 即尾端的前一位
    console.log(base); // [1, 2, 3, 4, 5, 'hey', 6]
    

嵌套函数

  • JavaScript支持在一个函数中创建另一个函数,而且还可以返回一个嵌套函数,用于变量的共享:倘若对此感兴趣,可以自行查阅“闭包”相关内容。
    function initCounter() {
        let count = 0;
    
        return function() {
            return count ++;
        }
    }
    
    
    let counter = initCounter();
    console.log(counter()); // 0
    console.log(counter()); // 1
    console.log(counter()); // 2
    

筛选对象数组

  • 对象数组在JavaScript中有着广泛应用,特别是在列表渲染以及前后端通信上,该格式最为常见。findfilter方法可以有效地作用于对象数组并完成筛选工作,两者的主要区别为find返回第一个匹配的元素,而filter可返回多个元素。
    let rtn_data = [
        {id: 1, name: 'Alice'},
        {id: 2, name: 'Bob'},
        {id: 3, name: 'Carol'},
        {id: 4, name: 'Dave'},
        {id: 5, name: 'Eve'},
    ]
    
    let top_1 = rtn_data.find(item => item.name === 'Alice');
    let top_3 = rtn_data.filter(item => item.id <= 3);
    
    
    console.log(top_1);
    /* { id: 1, name: 'Alice' } */
    
    console.log(top_3);
    /*
    [
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' },
      { id: 3, name: 'Carol' }
    ]
    */
    

跳出多重循环

outer:
for(let i = 0; i < 3; i ++) {
    for(let j = 0; j < 3; j ++) {
        let value = prompt('Input value', '');
        if(!value) break outer;
    }
}   
- break outer:当用户输入空串抑或是点击取消后,会直接跳出两层循环。

逻辑运算符的返回值

  • ||:可用于返回第一个真值,result = value1 || value2 || value3
  • &&:可用于返回第一个假值,result = value1 && value2 && value3
  • ??:可用于返回第一个已定义值,对于a ?? b,倘若a是已定义的,则结果为a,反之则为b

语法坑

浮点数

  • 浮点数误差是一个经典话题,利用有限的位数表示无限的数字显然会产生一些潜在问题,不过这也没有什么好的解决方法。在计算机组成原理中,针对浮点数有着专门的论述,感兴趣的读者可以阅读阅读\(CSAPP\)。说回\(JavaScript\),新手时常会犯如下错误:
    // 归并排序
    let m = (l + r) / 2;   
    
  • 在归并排序中,我们需要二分数组,但这里的m很可能会是一个浮点数,最终导致结果出现大量的undefined,正确的方法利用Math.floor函数。

typeof

  • typeof 可以用于确定变量的类型,它的返回值有 undefined、object、boolean、number、bigint string、symbol、function、object 八种,具体使用方式如下所示:
    console.log(typeof(null));         // object
    console.log(typeof(function(){}))  // function
    
  • 另外,如果要判断一个变量的类型是否为数组,应使用Array.isArray()函数,因为typeof会返回object,这在于数组是基于对象实现的。

值的比较

JavaScript 在比较不同类型的数据时,可能会产生难以预料的结果。再加上nullundefine以及NaN的存在,这个语法坑可谓常见且不友善,需要我们特别注意。

console.log('2' > 1);            // true
console.log('001' == 1);         // true
console.log('001' === 1);        // false
console.log(null == 0);          // false
console.log(false == 0);         // true
console.log(false === 0);        // false
console.log(null == undefined);  // true
console.log(null === undefined); // false


console.log('02' != 2);             // false
console.log('02' !== 2);            // true
console.log(2 > undefined);         // false
console.log('0x123' > null);        // true
console.log(NaN == NaN);            // false
console.log(NaN === NaN);           // false
console.log(Object.is(NaN, NaN));   // true

  • 以上这些千奇百怪的例子只是为了辅助说明,没有必要去专门背诵,我们记住一些基本的原则即可:
    • JavaScript对不同类型的值进行比较时,会将它们转化为number再判断大小;
    • 不要尝试使用> < >= <=去比较一个可能为null或者undefined的变量,这可能会导致结果失控;
    • 除非你确信自己的代码是无懈可击的,否则还是建议使用===!==,因为它们不会进行类型转换,整体效率也好一些;
    • 在大部分情形下,Object.is(a, b)a === b 效果相同,只是有两个特例,一是上文中所展示的Object.is(NaN, NaN) = true,另一个则是Object.is(0, -0) === false
  • 另外,如果你打算进行字符串的比较,请记住 (小写字母) 始终大于 (大写字母):
    console.log('apple' > 'Zpple');  // true
    

对象拷贝

  • JavaScript中,对象通过引用被赋值和拷贝,所有通过被拷贝的引用的操作,都将作用在同一对象上。倘若需要完成真正意义的拷贝(克隆),可以使用Object.assign或者深拷贝函数_.cloneDeep(Obj)
  • 由于数组也是基于对象实现的,因此数组也是通过引用来进行赋值的;
    let user = { name: 'John' };
    
    let admin = user;
    admin.name = 'Peter';
    console.log(user.name); // Peter
    
    let user = { name: "Alice" }
    let permissions1 = { can_view: true };
    let permissions2 = { can_edit: true };
    Object.assign(user, permissions1, permissions2);
    // user = { name: "John", can_view: true, can_edit: true }
    
  • 应当注意的是,正如下述代码所展示的那样,Object.assign无法直接处理对象嵌套的情况。但可以考虑构建一个循环来检查每user[key],倘若它是一个对象,则复制它的结构。当然,为了避免重复造轮子,我们也可以采用现有的实现,如lodash库中的_cloneDeep(obj)
    let user = {
        name: 'John',
        size: {
            height: 182,
            weight: 60
        }
    };
    
    let clone = Object.assign({}, user);
    
    console.log(user.size === clone.size) // true, 是同一个对象
    
    user.size.height ++;
    console.log(clone.size.height);      // 183, 即同样会被改变
    

字符串不可修改

  • JavaScript 中,字符串是不可更改的 (但可以拼接):
    let str = 'Hello World';
    str[6] = 'w';
    console.log(str);  // Hello World
    
    str += '!';
    console.log(str);  // Hello World!
    
  • str仍为原值,并没有发生变化。这个坑还蛮难受的,比如我们想要写一个将字符串首字母变为大写的函数,我们不能修改原字符串,只好创造一个新的:
    function toUppercase(str) {
        if(! str) return str;
        return str[0].toUpperCase() + str.slice(1);
    }
    
    console.log(toUppercase('aeiou'));  // Aeiou
    console.log(toUppercase('1234a'));  // 1234a
    

sort函数默认顺序

  • sort 函数默认将元素按照字符串进行比较,因此当我们试图排序一个整数数组时,有可能会出现以下现象:
    let base = [1, 2, 15];
    base.sort();
    console.log(base);  // [1, 15, 2]
    
  • 因此,我们需要自定义比较函数:
    base.sort((a, b) => {
        return a - b;
    })
    
    console.log(base);  // [1, 2, 15]
    

参考文献

下载链接

本站总访问量

评论