二、JavaScript 基础语法

117 阅读11分钟

前言

在英语中,语法的要求非常严谨,主谓宾定状补各有规定,到了编程语言里也是如此。在 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 里的 whitedo...whilefor 循环之外,还有 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 约定的是将可迭代数据结构展开为数组

所以 SetMap、类数组 (如 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?.(); // 函数调用

空值合并操作符

空值合并操作符是 ??,它连接两个操作值,只有当左侧操作操作值为 nullundefined 时,才会取右侧操作数。

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 表达式和运算符。

本文如有描述错误的地方,欢迎指正~