14 个鲜为人知的 JavaScript 功能和语法
阅读需要 8 分钟
用了多年 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 只是在现有功能中添加了方便使用的语法,了解更多关于 getter&setter 的知识。
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. ~
运算符
现实中没有人会去关注按位运算符,那什么时候才能使用它呢?
有一个 Tilde
或 Bitwise 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 中的代码,然后用返回的值替换页面内容,除非返回的值是 undefined
。void
运算符可用于返回 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. +
运算符
在字符串前面加上 +
运算符就能快速将字符串转换为数字。
+
运算符还适用于负数,八进制,十六进制,指数值。而且,它甚至可以将 Date 或 Moment.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
语句,标记只能和break
或continue
一起使用。在严格模式中,你不能使用 “let” 作为标签名称。它会抛出一个
SyntaxError
(因为let
是一个保留的标识符)。