JavaScript补课(二):语法

139 阅读12分钟

0. 前言

两本书关于JavaScript语法的部分都写得十分详细且复杂枯燥,让人很难抓住关键的部分。因此,小编在看这些内容的时候就在想,这里面很多东西,我只要写几行代码就可以搞明白他是什么了,还要看这么多内容;如果没写过代码,看这些东西也不过是纸上谈兵罢了。于是,在这篇文章中,小编要做的事情就是把JavaScript语法的关键部分找出来。

这篇文章的分为三个部分:

  • 变量声明和作用域、内存
  • 数据类型和值
  • 操作符和语句

1. 变量声明和作用域、内存

1.1 作用域

关于JavaScript的语法部分,我们首先来看作用域相关的内容,而作用域这个概念和执行上下文有关。关于执行上下文,《JavaScript高级程序设计》是这样描述的:

执行上下文(以下简称“上下文”)这个概念在JavaScript中尤为重要。变量或函数的上下文决定了它们可以访问哪些数据,以及它们的行为。每个上下文都有一个关联的变量对象。而这个上下文中定义的所有变量和函数都存在于这个对象上。

上面罗里吧嗦一大堆,说白了执行上下文就是控制中心,控制某段代码的前面执行了哪些东西,后面又要执行哪些东西。 在一段代码中,最外层的上下文就是全局上下文,它是根据 ECMAScript 实现的宿主环境来决定的。在全局上下文之下,每个函数调用都有自己的上下文。当代码执行到这个函数时,函数的上下文就会被推入到一个上下文栈中。在函数执行完之后,上下文栈会弹出这个函数的上下文,将控制权交还给之前的执行上下文。

在上下文中的代码执行的时候,会创建变量对象的作用域链。我们那一个函数来举例子,当这个函数调用的时候,宿主环境会创建变量,并且根据不同的变量声明方式确定这个变量的可访问范围。根据ECMAScript的变量访问规则,当JavaScript需要查找某个变量的时候,他会从当前调用的函数作用域范围开始查找。如果存在这个变量,则返回;如果不存在,则向上层查找;如果在上层还没有找到,那么就继续向上查找,直到到了最上层(也就是全局作用域)。如果找到了需要查找的变量,那么就返回该变量;否则,抛出错误ReferenceError. 在某个变量的查找过程中,这种形成嵌套关系的,最外层是全局作用域的这种链式结构,就叫做作用域链。

关于这两个概念,需要注意的是:只有函数才有执行上下文,变量是没有执行上下文的;而对于作用域链而言,变量和函数都是有的。

1.2 变量声明

接下来,我们就来看看变量声明相关的内容。

在ES6之前,只能用var来声明某个变量的作用域是函数作用域;当我们在变量声明是不使用任何关键字,则表明这个变量的作用域是全局作用域。ES6新增了两种声明变量的方式,letconst, 它们都表示某个变量的作用域是块作用域,不同的是let声明普通变量,即在定义之后可以修改的变量;而const声明常数变量,即在定义之后不能修改的变量。关于变量声明,需要注意以下几点:

  • 使用 var 操作符定义的变量会成为包含它的函数的局部变量
  • var声明的变量有变量提升,所谓变量提升,就是把所有变量声明都拉到函数作用域的顶部
  • var关键字允许同一个函数内有冗余声明,即同一个名称的变量生命多次不会报错;而let关键字声明的变量不允许在同一个块作用域内出现冗余声明,即同一个变量重复声明会报错:SyntaxError
  • let声明的变量不会在作用域中被提升
  • 在解析代码时,JavaScript引擎也会注意出现在块后面的let声明,只不过在此之前不能以任何方式来引用未声明的变量。在let声明之前的执行瞬间被称为“暂时性死区”,在此阶段引用任何后面声明的变量都会抛出RefrenceError
  • 使用var在全局作用域声明的变量会成为window对象的属性,而使用let关键字声明的变量不会称为window对象的全局属性
  • 在进行变量声明的时候:const优先,let次之,不使用var

1.3 内存管理

2. 数据类型和值

2.1 数据类型

在了解JS的数据类型之前,我们先来关注一下typeof操作符,我们可以用它来检查某一个变量或者值的数据类型是怎样的。例如:

console.log(typeof null)    // "object"
console.log(typeof 'asf')   // "string"

和其他的强类型编程语言不同,JavaScript数据类型的分类并没有那么细,只有六类:

  • 布尔值(Boolean):只有两个值,用来表示真(true)或假(false)
  • 数字(Number):可以表示任意的数字,还有一些看起来不是数字的关键字也属于数字类型的
  • 字符串(String):可以表示任意字符串
  • undefined:表示变量声明了,但是未定义
  • null:表示一个空对象指针
  • 对象(Object):用大括号包裹起来的,由键值对组成的集合。

除此之外,ES6还定义了新的数据类型,名叫Symbol符号类型,其用途为确保对象属性使用唯一标识符,不会发生属性冲突的危险。

相信有过JS编程经验的小伙伴们都对上述数据类型并不陌生。因此,关于这些数据类型的很详细的例子,小编在这里就不赘述了。在这部分内容中,小编只对这些数据类型中的一些重点和难点进行补充。

  1. 对于未声明的变量,只能执行一个有用的操作,就是对它调用typeof, 调用执行的结果是undefined
  2. 在对为初始化的变量调用typeof时,返回的结果是undefined;对未声明的变量调用它时,返回的结果还是undefined.
  3. undefined是一个假值。
  4. 一般来讲,永远不用显式地给某个变量设置undefined值。字面值undefined主要用于比较,而且在EAMA-262第3版之前是不存在的。增加这个特殊值的目的就是为了正式明确空对象指针(null)和未初始化变量的区别。
  5. null类型同样是一个假值。逻辑上讲,null值表示一个空对象指针,对null执行typeof操作之后,会返回一个"object".
  6. 任何时候,只要变量要保存对象,而当时有没有那个对象可保存,就要用null来填充该变量。
  7. 强制类型转换之后,得到false的值:false""(空字符串)、0NaNnullundefined.俗称六个假值对象。
  8. NaN是一个特殊的number类型的值,它表示不是一个数字,用来标记进行数学运算之后失败的结果。
  9. ECMAScript 中的字符串是不可变的,意思是一旦创建,它们的值就不能变了。要修改某个变量中的字符串值,必须先销毁原始的字符串,然后将包含新值的另一个字符串保存到该变量。
  10. 使用模板字符串:
let untaggedResult = `${ a } + ${ b } = ${ a + b }`; // 其中, 用 "${}" 包裹起来的是JS的语句
  1. Symbol()函数不能与new关键字一起作为构造函数使用

上述就是一些关于JavaScript数据类型的一些关键知识点,除此之外比较重要的就是强制类型转换的内容了。强制类型转换,分为两种:显式强制类型转换和隐式强制类型转换。关于强制类型转换的内容,可以戳这里看小编之前的文章。由于篇幅原因,这里就不再赘述了。

2.2 值

ECMAScript变量可以包含两种不同类型的数据:原始值和引用值。原始值就是最简单的数据,引用值则是由多个值构成的对象。

原始值的数据类型有六种:Undefined、Null、Boolean、Number、String和Symbol;引用值的数据类型只有Object。

原始值和引用值在使用上的区别是保存原始值的变量时按值访问的,因为我们操作的就是存储在变量中的实际值;而在操作对象时,实际操作的是对该对象的引用而非实际的对象本身,因此,保存引用值的变量(也就是对象)是按引用访问的。那么,问题来了,按值访问和按引用访问有什么区别呢?我们来看下面的例子。

let num1 = 5; 
let num2 = num1;

在这个例子中,我们执行的操作是,将5这个数字值赋值给变量num1,然后再将num1这个变量赋值给num2。此时,我们得到num1num2的值都是5。此时,如果我们更改num1这个变量中的值,num2这个变量中的值不会改变。因为存储在num2这个变量中的5这个值和存储在num1这个变量中的值是完全独立的,let num2 = num1这行语句实际执行的操作是,将存储在num1这个变量中的值复制了一份,然后存储在num2这个变量中。因此,更改num1中的值,对于num2这个变量是没有影响的。这就是对于原始值的按值访问

对于保存了某个对象的变量,则不是这种情况。

let obj1 = new Object();
let obj2 = obj1;

obj1.name = "Nicholas";
console.log(obj2.name);     // "Nicholas"

这个例子已经很能说明问题了。在这段代码中,首先,初始化了一个空对象,并且把它赋值给obj1;然后,我在把obj1赋值给obj2;当我在obj1中添加了一个name属性的时候,去访问obj2.name得到的结果是我上一行添加给obj1name属性。这说明obj1obj2这两个变量都表示同一个对象。这就是引用值的按引用访问。

3. 操作符和语句

3.1 操作符

  • 一元操作符:++, --,执行自加或自减的操作
  • 位操作符:<<, >> 分别表示二进制位向左移动或向右移动
  • 布尔操作符:
    • 非(!):对强制类型转换成布尔类型的值进行取反
    • 与(&&):
      • 如果第一个操作数是对象,则返回第二个操作数
      • 如果第二个操作数是对象,则只有第一个操作数求值为true才返回该对象
      • 如果两个操作数都是对象,则返回第二个操作数
      • 如果第一个操作数为null,则返回null
      • 如果第一个操作数为NaN,则返回NaN
      • 如果第一个操作数为undefined,则返回undefined
    • 或(||):
      • 如果第一个操作数是对象, 则返回第一个操作数
      • 如果第一个操作数求值为false,则返回第二个操作数
      • 如果两个操作数都是对象,则返回第一个操作数
      • 如果两个操作数都是null,则返回null
      • 如果两个操作数都是NaN,则返回NaN
      • 如果两个操作数都是undefined,则返回undefined
  • 乘性操作符: *,表示两个数相乘;/,表示两个数相除
  • 指数操作符:**, 表示求指数运算
  • 加性操作符:+, 表示两个数相加,或者字符串拼接;-,表示两个数相减
  • 关系操作符:小于(<), 大于(>),小于等于(<=),大于等于(>=
  • 相等操作符: ==表示值相等,类型不一定相等;===表示值和类型都相等;!=,表示值不相等;!== 表示值和类型都不等
  • 条件操作符:三目运算符(expression ? statement1 : statement2)
  • 赋值操作符:=表示把右边的值赋值给左边的变量
  • 逗号操作符:用来表示一行进行多个变量声明或者数组或对象的元素之间的分隔符

3.2 语句

语句用法用途
breakbreak[ label ]退出最内层循环或者退出switch语句,又或者退出label执行的语句
casecase expression在switch语句中标记一条语句
continuecontinue[label]重新开始内层循环或重新开始label执行的循环
debuggerdebugger;断点器调试
defaultdefault;在switch中标记默认的语句
do/whiledo statement while(expression)while循环的一种替代形式
空语句;什么都不做
forfor(init; test; incr) statement一种简写的循环
for/infor(var in object) statement遍历对象的属性
functionfunction name(prarm){body}声明一个函数
if/elseif(expr) statement1 [else statement2]执行statement1 或statement2
labellabel: statement给statement执行一个名字:label
returnreturn [expression]返回一个函数值
switchswitch(expression) {statements}用case或者default:语句标记的多分支语句
throwthrow expression抛出异常
trytry {statement} [catch {handler statements}] [finally {cleanup statement}]捕获异常
use strict"use strict"对脚本和函数应用严格模式
varvar name [= expr] [, ...]声明并初始化一个或多个变量
whilewhile(expression){statement}基本的循环结构
withwith(object) statement 扩展作用域链(不赞成使用)