前言
在英语中,语法的要求非常严谨,主谓宾定状补各有规定,到了编程语言里也是如此。在 ES6 之前,ECMAScript 规范包含的语法相对不多,而且还出现了一些看起来很奇怪的语法 (比如 with 语句),尤其是 “严格模式 (use strict)” 的存在,让本就容易产生混乱的 JavaScript 又多了很多让人难以理解的特性。
1. 基础语法
在 一、JavaScript 变量与值 这篇文章里,已经介绍了在 JavaScript 里如何定义变量,以及变量的值类型。本节将介绍 ES5 及之前的语法特性,不会介绍已经被废弃或者不推荐的语法,比如 with 语句。
1.1 基本运算符
JavaScript 里的运算符,绝大多数都和 C/C++/Java 里的一样,详细可以参阅文档 表达式与运算符。
需要特别注意的运算符是 ===/!== 和 ==/!=,这两组比较运算符是用来判定两侧的值是否相等的,返回的值是 boolean 类型。
===/!==这一组比较运算符称为全等于/全不等于,它们首先会判定两侧的值类型是否相同,如果值类型不相同,则直接返回对应的结果 (false/true),只有值类型相同时,才会去比较具体的值是否相等,然后返回对应的比较结果 (true/false)。==/!=这一组比较运算符称为等于/不等于,它们也是先判定两侧的值类型是否相同,如果值类型不相同,它们 不会 先返回比较结果,而是会先进行类型转换,转换成相同类型的值后,再比较两个值是否相等,然后返回对应的比较结果。
正因为
==/!=这对比较运算符有一个隐式类型转换的过程,所以我在日常开发中不会用它们来比较,而是用===/!==这一组,我觉得这样能避免一些边界值问题。
console.log(null == undefined); // true
console.log(null === undefined); // false
1.2 流程控制语句
几乎所有编程语言中都有条件语句、循环语句两种流程控制语句,JavaScript 也不例外。
JavaScript 中的条件语句和 C/C++/Java 一样,也是 if...else 语句 和 switch...case 语句,详细示例可以看 这里。
JavaScript 里的循环语句除了 C/C++/Java 里的 white、do...while 和 for 循环之外,还有 for...of 循环和 for...in 循环。关于这两个循环的作用和区别,可以看文档 MDN 循环与迭代。
总结一下这两个循环:
for...of循环用于遍历 可遍历对象 的属性值 (value),for...in循环用于遍历 可遍历对象及其原型链上 的可枚举属性名 (key)。for...of循环是在 ES6 中纳入规范的,for...in是在 ES5 之前纳入规范的。for...of循环会消费@iterator,而for...in循环不消费@iterator。- 只要是实现了
@Iterator接口的数据结构,都称为可迭代对象,都可以用for...of来遍历 - 只要是对象,都可以用
for...in来遍历 Object没有实现@Iterator接口,所以字面量对象不能用for...of循环遍历
- 只要是实现了
注意:
for...in遍历对象时,得到的key除了自身的以外,还有可能是其原型链上其他对象的属性,且属性名顺序 与运行环境有关。虽然 ECMAScript 对遍历出的属性顺序有约定,但部分运行环境实现时这个语句时,ECMAScript 对应的规范还没出来,所以没能按规范去实现,这是一个历史遗留问题。
1.3 函数的特性
函数是 JavaScript 中非常重要的一个部分,它有很多的特性,也正是因为这么多的特性,才筑成了 JavaScript 丰富多彩的世界。
特性一:函数是对象
查阅 MDN Function 文档可以发现,Function 是继承自 Object,也就是说,每一个函数都是一个对象。
我们知道,对象是引用值类型,被保存在堆里,程序运行栈中仅保存了对象的引用,而函数继承自对象,因此函数也是引用值类型,函数体被保存到了堆里,函数引用才是保存到栈里。如何获取一个函数的引用?通过函数的签名,也就是函数名,或者函数表达式的变量名。
特性二:函数可以作为另一个函数的参数
我们知道,函数的参数可以是原始值,也可以是引用值 (对象)。函数继承自对象,也就是说,一个函数可以作为另一个函数的参数,像这样以参数形式传递给另一个函数的函数,被称为 回调函数。
function f1(callback) {
callback("来自函数 f1 的调用"); // 调用回调函数
}
function f2(content) {
console.log(content);
}
f1(f2); // 调用函数 f1,并将函数 f2 的签名作为参数传递给函数 f1
回调函数 这个概念也许很难理解,但它是 JavaScript 中最关键的特性之一,JavaScript 里的很多功能都是建立在这个特性上的,比如数组的很多方法 (forEach, map, sort, reduce 等)。
JavaScript 本身是单线程的,且最开始不支持异步。随着业务的发展,越来越多的工作需要异步进行,因为 回调函数 的存在,使得很多需要等待的、异步完成的工作,都可以通过回调函数的方式去调用执行。
由于前端的业务复杂度越来越高,异步执行的工作越来越多,就出现了
回调地狱这个问题。ECMAScript 为了提供异步支持,于是提出了Promise规范,这个规范参考了很多当时社区里成熟的Promise方案,并将这个规范纳入了 ES6。Promise不仅一定程度上解决了回调地狱的问题,也为 JavaScript 提供了真正意义上的异步。
特性三:function 函数可以作为构造函数
这个特性经常被使用,因为如果你想以 OOP 的方式开发,就必须要这样做,原因至少有 2:
- ES6 之前的 JavaScript 里没有
class一说,而 ES6 之后的class还是有很多 OOP 的特性没有支持 - 函数可以用于构建一个新的对象
注意:箭头函数表达式不可以作为构造函数!只有通过
function关键字定义的函数表达式或者函数声明,才可以作为构造函数。
function Person(name) {
this.name = name;
}
const sherry = new Person("Sherry");
consolelog(sherry.name); // "Sherry"
Typescript 中有完整的 OOP 支持,使用 Typescript 开发时,就可以完全以 OOP 的形式来开发了,相信不久之后,JavaScript 里的这个特性也将逐渐不再是开发者所必须使用的了。
特性四:函数可以立即执行
在 JavaScript 里有一个很重要的概念:立即执行函数,也叫 立即调用函数表达式 (Immediately Invoked Function Expression, IIFE)/自执行匿名函数。
在 ES6 之前,由于没有 块级作用域、模块作用域,只有 全局作用域 和 函数作用域,为了保证一些作用域的独立性,以及不污染外层作用域,因而用 立即执行函数 的方式来创建一个独立的作用域块。
(function() {
console.log("这是来自立即执行函数里的打印");
})()
运行上面这一段代码你会发现,这个函数不需要手动调用就会执行,这也是 “立即执行函数” 这个名字的由来。
立即执行函数可以是具名的,也可以是匿名的。通常来说,建议把立即函数执行定义为具名的,因为这样便于定位控制台的报错信息。
2. 扩展语法
ES5 之前提供的语法特性比较少,它们是 JavaScript 的基石。ES6 以后,ECMAScript 纳入了非常多的新语法规范,且受到了广泛的支持。本节介绍了 ES6 以后的一些新的语法和特性。
2.1 解构赋值
支持解构的值有:字符串、对象、数组。
当然,数字、布尔值也可以解构,但通常不会那样用。
const sherry = { count: 0, name: "Sherry" };
const names = ["Nenny", "Taco", "Anny"];
const hello = "Hello";
const [h, e] = hello; // 字符串解构,每个变量拿到的是对应 index 的字符
const [nenny, taco] = names; // 数组解构,每个变量拿到的是对应 index 的值
const { count, name } = sherry; // 对象解构,每个变量拿到的是对应属性的值
// 解构并重命名
const { count: sherryCount, name: sherryName } = sherry; // 对象解构,并将指定属性的值赋值给对应的变量
凡是使用到这些值的地方都可以解构,包括函数的参数。
2.2 运算符 "..."
这个运算符既可以用作 展开运算符,也可以用作 剩余参数收集运算符。
展开运算符
“展开”,就是把聚在一起的元素拆开成独立的个体,展开运算符 做的就是这个工作。
- 这个运算符也被称为
扩展运算符
ES2015 先约定了可以展开 实现了 @Iterator 接口的数据结构 (即可迭代数据结构),语法为 const arr2 = [...arr1];,后来的 ES2018 约定对象也可以展开,语法与可迭代数据结构的结构语法相同,但展开的结果是一个对象。
ES2015 约定的是将可迭代数据结构展开为数组
所以
Set、Map、类数组 (如arguments) 都可以使用这个运算符来将元素展开到一个数组里ES2018 新增约定对象也可以展开,但展开的结果为对象
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4]; // 数组解构
const m1 = new Map();
m1.set(1, 1);
m1,set(2, 2);
const arrForM1 = [...m1]; // 一个二维数组,每个元素都是包含2个元素的数组,其中第0个元素为 map 的 key,第1个元素为 map 的 value
const o1 = { count: 0 };
const o2 = { ...o1, name: "Sherry" }; // 对象解构
剩余语法
“剩余参数收集”,就是将操作的剩余的参数收集到一起的操作。
这个操作最常用的三个地方是:对象、数组、函数参数。
const o1 = { count: 0, age: 12, name: "Sherry" };
const { count, ...sherry } = o1; // 将 o1 除 count 外的属性都收集到 sherry 这个对象里
const arr = [1, 2, 3, 4];
const [first, arr2] = arr;; // 将 arr 第0个元素保存到 first 变量里,第1到3个元素收集到 arr2 这个数组里
// 将函数的第一个参数取出来,后面所有的参数,都存放到 sources 这个数组里
function merge(target, ...sources) {
// merge 的具体实现
}
剩余语法,顾名思义,它只能收集剩下的,所以这个操作符不能放到数组、对象、函数参数解构的中间,只能放到最后,像上面示例那样
2.3 赋值运算符
在 ES5 之前,+, -, *, /, %, <<, >>, >>>, &, |, ^ 这些运算符可以和赋值运算符 = 联合在一起,作为复合的赋值运算符,这些复合的赋值运算符的含义是:用操作符左边变量的值,执行对应的操作,然后把结果赋值给左边的变量。
const a= 5;
a += 1; // 等价于 a = a + 1;
a -= 1; // 等价于 a = a - 1;
a *= 1; // 等价于 a = a * 1;
a /= 1; // 等价于 a = a / 1;
a %= 1; // 等价于 a = a % 1;
a <<= 1; // 等价于 a = a << 1;
a >>= 1; // 等价于 a = a >> 1;
a >>>= 1; // 等价于 a = a >>> 1;
a &= 1; // 等价于 a = a & 1;
a |= 1; // 等价于 a = a | 1;
a ^= 1; // 等价于 a = a ^ 1;
ES6 之后,额外增加了几类联合赋值运算符:
**=&&=||=??=
const a = 2;
a **= 3; // 等价于 a= a ** 3,计算 a 的 3 次幂
const b = 0;
b &&= 2; // 等价于 b = b && 2,如果 b 为假值时,就将 b 赋值为 2
const c = 1;
c ||= 3; // 等价于 c = c || 3,如果 c 为真值时,就将 c 赋值为 3
const d = null;
d ??= { count: 0 }; // 等价于 d = d ?? { count: 0 },如果 d 为 null/undefined,就将 d 的值赋值为 { count: 0 }
2.4 其他运算符
可选链操作符
可选链操作符是 ?.,它可以用于属性访问、方法调用、函数调用。
const person = {
age: 15,
}
const age = person?.age; // 对象的属性访问
const result = person.introduction?.(); // 对象方法调用
someFunction?.(); // 函数调用
空值合并操作符
空值合并操作符是 ??,它连接两个操作值,只有当左侧操作操作值为 null 或 undefined 时,才会取右侧操作数。
let a = 0;
let b = 1;
let c = a ?? b; // a 为 0,所以 c 的值等于 a
console.log("c =", c); // c = 0
a = undefined;
let c = a ?? b; // a 为 undefined,所以 c 的值等于 b
console.log("c =", c); // c = 1
a = null;
let c = a ?? b; // a 为 null,所以 c 的值等于 b
console.log("c =", c); // c = 1
总结
本文介绍了 JavaScript 一些基本的运算符,以及 ES6 中扩展的一些运算符,用一些简单的代码介绍了这些运算符的使用方式和特性。除了这些运算符外,JavaScript 也提供了其他的一些运算符,这些运算符相对比较简单,或者在日常开发中使用不多,所以本文没有介绍,感兴趣的小伙伴可以查阅文档 MDN 表达式和运算符 来查看完整的 JavaScript 表达式和运算符。
本文如有描述错误的地方,欢迎指正~