14 个鲜为人知的 JavaScript 功能和语法

天魂
Autor 天魂 一个死磕 Angular 的逼粉

14 个鲜为人知的 JavaScript 功能和语法

用了多年 JavaScript,偶然会发现一些以前不知道的隐藏语法或技巧。

本文中列出了一些鲜为人知的 JavaScript 功能,其中一些功能在严格模式下无效,但这些代码仍然是完全有效的。为了避免引起你的队友生气,所以不建议在开发中使用文中提到的所以功能。

1.逗号运算符

JavaScript 中允许在一行中写多个用逗号分隔的表达式,并返回最后一个表达式的结果。

1
2
// syntax
let result = expression1, expression2,... expressionN;

上面代码示例中,对所有表达式进行求值,并为变量 result 赋值为 expressionN 返回的值。

你可能已经在 for 循环中使用了逗号运算符

1
for (var a = 0, b = 10; a <= 10; a++, b--)

逗号运算符有时在一行中写多个语句时会有所帮助:

1
2
3
function getNextValue() {
    return counter++, console.log(counter), counter;
}

或在写简短箭头函数是使用: const getSquare = x => (console.log (x), x * x);

2.Getters & Setters

在 JavaScript 中访问对象的属性时,如果对象中定义了这个属性,则返回正在访问的属性的值;否则,返回 undefined

如果不想这么简单的访问对象,就要用到 JavaScript 中对象的 Getter 和 Setter 了。可以自定义 Getter 函数以返回所需的任何内容,而不是直接返回属性的值。设置属性的值也一样。

这样的好处是在获取或设置字段的值时可以有很多骚操作,比如:虚拟字段、字段验证、副作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const human = {
    firstName: 'Nathan',
    lastName: 'Drake',
    _age: 0,

    /* fullName 是一个虚拟字段 */
    get fullName() {
        return `${this.firstName} ${this.lastName}`;
    },

    /* 在存储之前验证 age 字段 */
    set age(value) {
        if (isNaN(value)) {
            throw Error('Age has to be a number');
        }

        this._age = Number(value);
    },
    get age() {
        return this._age;
    }
}

console.log(human.fullName); // Nathan Drake

human.age = 'invalid text'; // Uncaught Error: Age has to be a number
ES5 中的 Getters 和 Setters

Getters 和 Setters 不是在 ES5 中新增的功能,它们一直在 JavaScript 中。ES5 只是在现有功能中添加了方便使用的语法,了解更多关于 gettersetter 的知识。

3. !! 运算符

从技术上讲,!! 并不是独立的 JavaScript 运算符,只是 ! (逻辑非)运算符使用了两次。

!! 的作用是将表达式强制转换为 布尔值(Boolean) 的骚操作。如果表达式是值,则会返回 true; 否则返回 false

1
2
3
4
5
6
7
8
9
10
!! null              // returns false
!! undefined         // returns false
!! false             // returns false
!! true              // returns true
!! ""                // returns false
!! "string"          // returns true
!! 0                 // returns false
!! 1                 // returns true
!! {}                // returns true
!! []                // returns true

4.带标签的模板字符串

你之前肯定听说过或者用过模板字面量,它是允许嵌入表达式的字符串字面量。你可以使用多行字符串和字符串插值功能。它们在 ES2015 之前的版本中被称为“模板字符串”。但是你知道高级形式的带标签的模板字符串吗?

1
2
3
4
5
/* 模板字符串 */
`Hello ${userName}`

/* 带标签的模板字符串 */
myTag`Hello ${userName}`

带有标签的模板字符串可以在模板字符串上添加自定义标签,以更好地控制将模板字符串解析为字符串。标签是一个解析器函数,第一个参数包含一个字符串值的数组,其余的参数与表达式相关。最后,函数可以返回处理好的字符串(或者返回完全不同的东西)。

在下面的示例中,自定义标签 highlight 用来解析模板字符串的值,并且将解析后的值与元素封装在结果字符串中以高亮显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function highlight(strings, ...values) {
    // 遍历 strings 数组
    let result = ``;
    strings.forEach((str, i) => {
        result += str;
        if (values[i]) {
            result += `<mark>${values[i]}</mark>`
        }
    });

    return result;
}

const author = `Henry Avery`;
const statement = `I am a man of fortune & I must seek my fortune`;
const quote = highlight`${author} once said, ${statement}`;

console.log(quote);
/* 输出:<mark>Henry Avery</mark> once said, <mark>I am a man of fortune & I must seek my fortune</mark> */

正如下面例子所展示的,标签函数并不一定需要返回一个字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function template(strings, ...keys) {
  return (function(...values) {
    const dict = values[values.length - 1] || {};
    const result = [strings[0]];
    keys.forEach(function(key, i) {
      const value = Number.isInteger(key) ? values[key] : dict[key];
      result.push(value, strings[i + 1]);
    });
    return result.join('');
  });
}

const t1Closure = template`${0}${1}${0}!`;
t1Closure('Y', 'A');  // "YAY!" 
const t2Closure = template`${0} ${'foo'}!`;
t2Closure('Hello', {foo: 'World'});  // "Hello World!"

5. ~ 运算符

现实中没有人会去关注按位运算符,那什么时候才能使用它呢?

有一个 TildeBitwise NOT 运算符的日常用例。当与数字一起使用时,Tilde 作用于 ~N => -(N+1)。只有当 N = -1 时,此表达式才计算为 0。利用这一点,将 ~ 放在 indexOf 函数前面,以判断 item 是否存在于字符串或数组中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const str = 'rawr';
const searchFor = 'a';

// 这是 if (-1*str.indexOf('a') <= 0) 条件判断的另一种方法
if (~str.indexOf(searchFor)) {
  // searchFor 包含在字符串中
} else {
  // searchFor 不包含在字符串中
}

// (~str.indexOf(searchFor))的返回值
// r == -1
// a == -2
// w == -3

注意:ES6 和 ES7 分别在字符串和数组中添加了一个新方法 .includes()。无疑,这是比 ~ 运算符更简洁的方法来判断数组或字符串中是否存在 item。

6. void 运算符

void 运算符 对给定的表达式进行求值,然后返回 undefined

经常看到它用作 void (0)void 0,使用 0 只是一个约定,不一定必须使用 0,它可以是任何有效的表达式,并且仍返回 undefined

1
2
3
4
5
6
7
void 0                  // returns undefined
void (0)                // returns undefined
void 'abc'              // returns undefined
void {}                 // returns undefined
void (1 === 1)          // returns undefined
void (1 !== 1)          // returns undefined
void anyFunction()      // returns undefined
与立即调用的函数表达式一起使用

在使用立即执行的函数表达式时,可以利用 void 运算符让 JavaScript 引擎把一个 function 关键字识别成函数表达式而不是函数声明(语句)。

1
2
3
4
5
6
7
8
9
10
11
12
void function iife() {
    const bar = function () {};
    const baz = function () {};
    const foo = function () {
        bar();
        baz();
     };
    const biz = function () {};

    foo();
    biz();
}();
与 JavaScript URIs 一起使用

当用户点击 javascript: URI 时,它会执行 URI 中的代码,然后用返回的值替换页面内容,除非返回的值是 undefinedvoid 运算符可用于返回 undefined。例如:

1
2
3
4
5
6
7
<a href="javascript:void(0);">
  Click here to do nothing
</a>

<a href="javascript:void(document.body.style.backgroundColor='green');">
  Click here for green background
</a>

注意:虽然这么做是可行的,但利用 javascript: 伪协议来执行 JavaScript 代码是不推荐的,推荐的做法是为链接元素绑定事件。

与箭头函数一起使用

箭头函数标准中,允许在函数体不使用括号来直接返回值。如果右侧调用了一个原本没有返回值的函数,其返回值改变后,则会导致非预期的副作用。安全起见,当函数返回值是一个不会被使用到的时候,应该使用 void 运算符,来确保返回 undefined(如下方示例),这样,当 API 改变时,并不会影响箭头函数的行为。

1
button.onclick = () => void doSomething();

确保了当 doSomething 的返回值从 undefined 变为 true 的时候,不会改变函数的行为。

7.构造函数 () 是可选的

在调用构造函数是会在类名后面添加 (),在不需要向构造函数传递任何参数时 () 完全是可选的。

下面示例中两种代码形式都是有效的 JavaScript 语法,执行后的结果也是完全相同的。

1
2
3
4
5
6
7
8
9
// 有 () 的构造函数
const date = new Date();
const month = new Date().getMonth();
const myInstance = new MyClass();

// 没有 () 的构造函数
const date = new Date;
const month = (new Date).getMonth();
const myInstance = new MyClass;

8.省略 IIFE (立即调用函数表达式)()

常见的 IIFE 都用 () 包裹整个函数,这样的语法多少有点奇怪。实际上这些额外的 () 只是给 JavaScript 解析器看的。但是,有很多方法可以省略这些多余的 () ,并且不会影响 IIFE 有效性。

1
2
3
4
5
6
7
8
9
// 没有返回值的 IIFE

(function() {
    console.log('Normal IIFE called');
})()

void function() {
    console.log('Cool  IIFE called');
}()

void 操作符告诉解析器代码是函数表达式。因此,可以省略包裹函数定义的 ()。除了 void 操作符,还可以使用任何一元运算符,如 void, +, !, -

如果IIFE有返回值,那么使用一元运算符会影响IIFE的返回值。但是,如果当需要将返回值赋值给变量存起来时完全可以省略多余的 ()

1
2
3
4
5
6
7
8
9
10
// 有返回值的 IIFE
const result = (function() {
    return 'Victor Sully';
})()
// result: 'Victor Sully'

const result = function() {
    return 'Nathan Drake';
}()
// result: 'Nathan Drake'

9. with 声明

JavaScript有一个 with 语句块(with 是 JavaScript 中的一个关键字)。

1
2
3
4
5
6
7
8
9
with (object)
   statement 

// 多个语句块
with (object) {
   statement
   statement
   ...
}

with 语句将某个对象添加到作用域链的顶部,如果在 statement 中有某个未使用命名空间的变量,跟作用域链中的某个属性同名,则这个变量将指向这个属性值。如果沒有同名的属性,则将拋出 ReferenceError 异常。

1
2
3
4
5
6
7
8
9
10
11
const human = {
    firstName: 'Nathan',
    lastName: 'Drake',
    age: 29
}

with (human) {
    console.log(`${firstName} ${lastName} is ${age} years old.`);
}

// 输出:Nathan Drake is 29 years old.

10.函数的构造函数

如果要声明一个函数,利用 function 关键字不是唯一途径,还可以使用 Function() 构造函数和 new 运算符动态地定义函数。

1
2
const multiply = new Function('x', 'y', 'return x*y;');
multiply(2, 3); // 6

函数构造函数的最后一个参数是函数的字符串形式函数定义(函数体),之前的其他参数为函数参数。

11.函数属性

函数是 JavaScript 中的”一等公民“,和其他对象一样可以在函数上添加自定义属性。

可配置的函数

假设有一个叫做 greet 的函数,它的功能是根据不同的语言环境打印不同的问候消息,这个语言环境也应该是可配置的。

实现方式有很多,下面的代码示例是用函数属性来实现这个 greet 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const greeting = {
    cn: '你好!',
    fr: 'Bonjour!',
    es: 'Hola!',
    en: 'Hello!'
};

function greet() {
    console.log(greeting[greet.locale] || 'Hello!');
}

greet(); // Hello!

greet.locale = 'cn';
greet(); // 你好!
带有静态变量的函数

假设要实现一个生成有序数字序列的数字生成器,通常的做法是使用带有静态计数器变量的 Class 或 IIFE 跟踪最后一个值。这样,既限制了对计数器的访问,还避免了用额外的变量污染全局空间。

如果想灵活地读取甚至修改计数器而又不污染全局空间怎么办?

常规情况是创建一个有计数器属性和额外方法的类来读取计数器。骚操作就是在函数上添加一个计数器属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function generateNumber() {
    if (!generateNumber.counter) {
        generateNumber.counter = 0;
    }

    return ++generateNumber.counter;
}

generateNumber();
// 1

generateNumber();
// 2

generateNumber.counter;
// 2

generateNumber.counter = 10;
generateNumber.counter;
// 10

generateNumber();
// 11

12.参数属性

众所周知,所有(非箭头)函数中都有一个可用的类似数组的 arguments 对象,可以使用 arguments 对象在函数中引用函数的参数列表。它还具有其他一些有趣的特性:

  • arguments.callee:引用当前调用的函数
  • arguments.callee.caller:引用调用当前函数的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const myFunction = function() {
    console.log('Current function: ', arguments.callee.name);
    console.log('Invoked by function: ', arguments.callee.caller.name);
}

void function main() {
    myFunction();
} ();

/* 
 *  输出:
 *  Current function:  myFunction
 *  Invoked by function:  main
 */

13. + 运算符

在字符串前面加上 + 运算符就能快速将字符串转换为数字。

+ 运算符还适用于负数,八进制,十六进制,指数值。而且,它甚至可以将 DateMoment.js 对象转换为时间戳!

1
2
3
4
5
6
7
8
9
10
11
12
// + 运算符
+'9.11'             // returns 9.11
+'-4'               // returns -4
+'0xFF'             // returns 255
+true               // returns 1
+'123e-5'           // returns 0.00123
+false              // returns 0
+null               // returns 0
+'Infinity'         // returns Infinity
+'1,234'            // returns NaN
+dateObject         // returns 1542975502981 (timestamp)
+momentObject       // returns 1542975502981 (timestamp)

14.标记语句

JavaScript 中 label 语句 可以和 break 或 continue 语句一起使用。标记就是在一条语句前面加个可以引用的标识符(identifier)。

标记语句在嵌套循环中使用特别方便,也可以使用它将代码简单地组织成块或构建易碎的块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
declarationBlock: {
    // 将逻辑代码块组合在一起
    let i, j;
}

forLoop1: // 第一个 for 语句被标记为 “forLoop1”
for (let i = 0; i < 3; i++) {
    forLoop2: // 第二个 for 语句被标记为 “forLoop2”
    for (let j = 0; j < 3; j++) {
        if (i === 1 && j === 1) {
            continue forLoop1;
        }

        console.log(`i = ${1}, j = ${j}`);
    }
}

loopBlock: {
    console.log('I will print');
    break loopBlock;
    console.log('I will not print');
}

需要注意的是,JavaScript 没有 goto 语句,标记只能和 breakcontinue 一起使用。

在严格模式中,你不能使用 “let” 作为标签名称。它会抛出一个 SyntaxError(因为 let 是一个保留的标识符)。

💡其他疑问可以在「web 前端骚操作」知识星球 一起探索
comments powered by Disqus