通过js高级程序设计学js 三、语言基础

378 阅读14分钟

简介

ECMA-262第5版(ES5)定义的ECMAScript,是目前为止浏览器支持最好的一个版本。EACA-262第6版(ES6)在浏览器中受支持程度次之,本篇内容主要基于ES6,并且文中所提到的ECMAScript可以理解并在脑中替换为JavaScript

语法

ECMAScript的语法很大程度上借鉴了C语言和其他类C语言,如Java和Perl。

区分大小写

ECMAScript中一切都区分大小写,无论是变量、函数名或操作符。比如:变量test和Test是两个不同的变量。同样的 typeof不能作为函数名或者其他变量名否则浏览器会报错,Unexpected token 'typeof',因为它是一个关键字,其他关键字也不可作为变量名(后面会提到)。但是Typeof是一个完全有效的函数名。

标识符

所谓标识符就是变量、函数、属性或函数参数的名称。标识符可以由一个或者多个下列字符组成(标识符的规则):

  1. 第一个字母必须是一个字母、下划线(_)或美元符号($)下划线和美元符号须切换英文输入法;
  2. 剩下的其他字符可以是字母、下划线、美元符号或数字;
let name = '张三'; // 正确
let _name = '张三'; // 正确
let $name = '张三'; // 正确
let 1name = '张三'; // 错误,标识符不可数字作为第一个字符

不只是数字不可以,规则一中已明确规定了第一个字符的规则。

标识符中的字母可以是扩展ASCII中的字母,也可以是Unicode中的字符。但不推荐使用(就不要用)。

按照惯例,ECMAScript中的标识符推荐使用驼峰大小写形式,即第一个单词字母小写,后面的每个单词字母大写。如:myName doSomethingImportant

这种写法不是强制的,但这种形式和ECMAScript内置函数和对象命名一致,所以算是最佳实践。

注意:关键字、保留字、true、false和null不能作为标识符,经过测试undefined是可以的

注释

ECMAScript采用C语言的注释风格。如:

// 单行注释,以两个斜杠字符开头

/*
    块注释也叫多行注释以一个斜杠字符和*字符开头,以反向组合结尾
 */

/*
 *   同上,这样写提高了注释的可读性
 *
 */

在我日常工作中,发现有其他的现象,是在js高级程序设计中本章节没有提到的。

/**
 * 这样的注释在我们日常开发中与上面的块注释并没有什么区别
 * 当你需要用JSDoc等工具生成API文档时,会需要这种写法的注释
 * 当然你要觉得这种的好看,这样用也无所谓
 */

还有一种错误的情况,注释没有结尾,会造成/*后面的所有代码都是注释。所以块注释一定要有结尾标识。

/* 
    let name = '张三';
    ...
    ...
    ...
    ...

严格模式

ECMAScript 5增加了严格模式的概念。严格模式是一种不同的JavaScript解析和执行模型。ECMAScript 3中的一些不规范的写法在这种模式下会被处理,对于不安全的活动会抛出错误,开启严格模式只需在脚本(js文件)开头加上一行"use strict"

这个看起来像个没有赋值给任何变量的字符串,实际上是一个预处理指令。任何支持JavaScript的浏览器看到它都会切换到严格模式。选择这种语法的目的是不破坏ECMAScript 3语法。

也可以单独指定一个函数在严格模式下执行,只要在函数的首行加入这个指令。如:

function getName() {
    "use strict"
    // 函数体
}

严格模式会影响JavaScript执行的很多方面,后面的文章用到它时会明确指出来。所有的现代浏览器都支持严格模式。

语句

ECMAScript语句以分号结尾。也可以省略分号,由JavaScript解析器自动补全(但不推荐)。如:

let name = '张三'; // 推荐
let sum = a + b // 不推荐

即使语句后加分号不是必需的,我们也应该加上。加分号可以避免输入内容不完整,也便于开发者通过删除空行来压缩代码(如果结尾没有分号,只删除空行,会导致语法错误)。加分号也有助于有些情况下提升性能,因为解析器会尝试在合适的位置补全分号以纠正语法错误。

书中没有具体提到不加分号在某些情况下会引起报错,我根据工作中遇到过的问题和查阅了一些网上的资料并实测了一遍,如下:

JavaScript解析器添加分号的规则

JavaScript只有在缺少分号无法解析代码的时候,才会自动补全分号;规则如下:

  1. 下行代码破坏上行代码时。
  2. 下行代码以 { 开头或 } 结束时。
  3. 当解析到源代码结尾时。
  4. 当前行return、break、throw、continue语句后面紧跟着换行时。

注意:一条语句如果以 ( [ / + - 这几个字符开头时,那么极有可能和上条语句合并解析

const name = '张三'
['a', 'f'].forEach(e => console.log(e));
// 这种情况浏览器会报错 Cannot read properties of undefined (reading 'forEach')
// 基于规则,上面的代码会被解析为
const name = '张三'['a', 'f'].forEach(e => console.log(e));

1 + 1
-1 + 1 === 0 ? console.log(0) : console.log(2);
// 这种情况下console的值为2,解析器会将上面的代码解析为
1 + 1 - 1 + 1 === 0 ? console.log(0) : console.log(2);

let name = '张三'
(function getName() {
  console.log(name);
})()
// 这种情况下浏览器会报错 "张三" is not a function
// 会被解析为 let name = '张三'(function getName() {...}())

let // 这里不会自动添加分号,因为下面的代码不会破坏当前行代码
name = '张三' // 这里会自动添加分号
let sex = '男';

function getName() {
  return
  { name: '张三' }
}
console.log(getName()); // 返回undefined,并没有返回这个对象,规则如上第4条。应该写成
function getName() {
  return { name: '张三' }
}

多条语句可以合并到一个C语言风格的代码块中。代码块由一个左花括号({)开始,右花括号(})结束:

if (test) {
    test = false;
    let name = "张三";
}

if之类的控制语句只有在执行多条语句时才要求必须有代码块。不过最佳实践是始终控制在代码块中,即使执行的语句只有一条,如下:

if (test) test = false; // 有效,但容易导致错误,应该避免

if (test) {
    test = false; // 推荐
}

// 这里有一个推荐,代码块后不需要添加分号。

关键字和保留字

ECMAScript-262描述了一组保留的关键字,这些关键字有特殊用途。比如表示控制语句的开始和结束,或者执行特定的操作。按照规定,保留的关键字不能用作标识符或属性名。ECMA-262第6版规定的所有关键字如下:

关键字
breakdointypeofcase
elseinstanceofvarcatchexport
newvoidclassextendsreturn
whileconstfinallysuperwith
continueforswitchdebuggerfunction
thisdefaultifthrowdelete
importtryyield

规范中也描述了一组未来的保留字,同样不能作为标识符或属性名。虽然保留字在语言中没有特定的用途,但它们是保留给未来关键字用的。

以下是ECMAScript-262第6版为将来保留的所有词汇。

始终保留:enum

严格模式下保留:

保留字
implementspackagepublicinterfaceprotected
staticletprivate

模块代码中保留:await

这些词汇不能用作标识符,但还可以用作对象的属性名,最好还是不要使用关键字或保留字作为对象属性名,以确保兼容过去和未来的ECMAScript版本。

变量

ECMAScript变量是松散类型的,变量可以保存任何类型的数据,每个变量只不过是用于保存任意值的命名占位符。有3个关键字可以声明变量var、let和const。其中var在ECMAScript的所有版本被支持,let和const只在ECMAScript 6和6之后的版本支持。

var关键字

var massage;

这行代码使用var关键字声明了一个变量massage,可以用它保存任何类型的数据,不初始化的情况下变量会保存一个特殊值undefined,也可以在声明的同时给变量提供一个初始化值var massage = 'hi'

这里,massage变量保存了一个字符串'hi',像这样初始化变量不会将它标识为字符串类型,只是一个简单的赋值。你可以任意的改变它的值和值的数据类型。

var massage = 'hi' // 初始化变量
massage = 100 // 合法,但不推荐
massage = true // 合法,但不推荐

这样的写法在ECMAScript中是完全有效的,但是不推荐改变变量的数据类型。
这点在学习typeScript时解决这个因开发人员所造成的问题。
平常我们也应该刻意的去注意这个问题。

var声明作用域

关键的问题在于使用var声明的变量,会在包含它的函数里成为局部变量,这就意味着在函数退出时会销毁该变量:

function getName() {
  var name = '张三';
}
getName();
console.log(name);
// 此时浏览器会报错 name is not defined;

这里变量name使用var在函数内部定义,是一个局部变量。调用函数会创建这个变量并赋值,调用之后会立即销毁,因此在函数外部console时浏览器会抛出错误。不过,在函数体内部定义变量的时候省略var关键字可以定义一个全局变量:

function getName() {
  name = '张三';
}
getName();
console.log(name);
// 此时浏览器会打印 '张三';

注意:虽然可以通过省略var定义全局变量,但不推荐这样做。会出现难以维护的问题,在严格模式下,像这样给未声明的变量赋值,会导致浏览器抛出错误。

定义多个变量,可以在一条语句中用逗号分隔每个变量(及可选的初始值): var message = 'hi', sex = '男', age = 18, name; 因为ECMAScript是松散类型的,所以使用不同数据类型初始化的变量可以用一天语句来声明。

在严格模式下,不能定义eval和arguments的变量,会导致语法错误。Unexpected eval or arguments in strict mode

var声明提升

使用var声明变量时,这个变量会自动提升到函数作用域的顶部:

function getName() {
  console.log(name); // 不会报错,打印undefined
  var name = '张三';
}
getName();

之所以不会报错,是因为ECMAScript运行时把它看作:

function getName() {
  var name;
  console.log(name);
  name = '张三';
}
getName();

要注意的点是,var关键字声明的变量可以重复声明,但不建议这样做:

function getName() {
  var name = '张三';
  var name = '李四';
  var name = '王麻子';
  console.log(name); // 王麻子'
}
getName();

还有一个概念在上面说的可能不是很明确,“声明提升”是指提升到各层作用域的顶端执行。,我们看个例子:

var name = '张三';
function getName() {
    console.log(name); // undefined
    var name = '李四';
    console.log(name); // 李四
}
getName();
console.log(name); // 张三

还有一点需要说一下,使用var在全局作用域下声明的name变量使用typeof检测该变量数据类型时返回的是stringname属性在全局中比较特殊,不管var name = 任何值,它最终都是字符串

let声明

let和var的作用差不多,但有着非常重要的区别。let声明的范围是块级作用域,var声明的范围是函数作用域。

if (true) {
    var name = 'matt';
    console.log(name); // matt
}
console.log(name); // matt
if (true) {
    let age = 18;
    console.log(age); // 18
}
console.log(age); // age is not defined 没有定义

在这里,age变量之所以不能在if块外部被引用,是因为它的作用域仅限于该块内部,块级作用域是函数作用域的子集,因此适用于var的作用域限制同样适用于let。

let也不允许重复声明变量。会导致错误:

var name;
var name;

let age;
let age; // 报错:标识符age已经声明过了

当然,JavaScript引擎会记录用于变量声明的标识符及其所在的块作用域,因此嵌套使用相同的标识符不会报错,这是因为在同一个块中没有重复的声明,这个问题在上面已经提到过,“声明提升”是指提升到各层作用域的顶端执行。

对于重复声明报错不会因混用let和var而受影响。这两个关键字声明的并不是不同类型的变量,只是指出变量在相关作用域如何存在:

var name;
let name; // Identifier 'name' has already been declared(标识符“name”已经声明)

let age;
var age; // Identifier 'age' has already been declared(标识符“age”已经声明)

暂时性死区

let和var的另一个重要区别,就是let声明的变量不会在作用域中被提升。

// name 会被提升
console.log(name) // undefined
var name = 'matt';

// age 不会被提升
console.log(age) // Cannot access 'age' before initialization(无法在初始化之前访问“age”)
let age = 18;

在解析JavaScript代码时,解析器也会发现块后面的let声明,只不过在此之前不能以任何方式来引用未声明的变量。在let声明之前的执行瞬间被称为 “暂时性死区”,在此之前引用任何后面声明的变量都会报错。

全局声明

与var关键字不同,使用let在全局作用域下声明的变量不会成为window对象的属性(var声明的则会),这个问题上面有提到过,就是那个使用var全局作用域下声明的name变量typeof检测始终为string,window对象自带一个name属性,我们在全局作用域下使用var声明的name会以字符串的方式替换掉window对象name的属性值:

var name = 'matt';
console.log(window.name); // matt

let age = 18;
console.log(window.age); // undefined

不过,let声明仍然是在全局作用域下发生的,相应的变量会在页面的生命周期内存续。因此,为了避免报错,必须保证页面不会重复声明同一个变量。

for循环中的let声明

在let出现之前,for循环定义的迭代变量会渗透到循环体外部:

for (var i = 0; i < 5; i++) {
    // ...
}
console.log(i); // 5

改用let后,这个问题消失了,因为迭代的变量作用域仅限于for循环内部。
for (let i = 0; i < 5; i++) {
    // ...
}
console.log(i) // i is not defined 没有定义

在使用var的时候,最常见的问题就是对迭代变量的奇特声明和修改:

for (var i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), 0)
}

// 你可能以为会输出 0、1、2、3、4
// 实际上会输出 5、5、5、5、5

之所以会这样,是因为在退出循环时,迭代变量保存的是导致循环退出的值:5.在这之后执行超时逻辑时,所有的i都是同一个变量,因而输出的都是同一个最终值。

而在使用let声明迭代变量时,JavaScript引擎会在后台为每个迭代的循环声明一个新的迭代变量,每个setTimeout引用的都是不同的变量实例。

for (let i = 0; i < 5; i++) {
    setTimeout(() => console.log(i), 0)
}
// 会输出 0、1、2、3、4

这种每次迭代声明一个独立变量的行为适用于所有风格的for循环,包括for-in、for-of循环。

const声明

const声明的行为和let基本相同,唯一一个重要的区别就是用它声明变量时必须初始化变量,且尝试修改const声明的变量的值会导致运行时错误。

const age = 18;
age = 28; // 报错:给常量赋值、

// const也不允许重复声明
const name = 'matt';
const name = 'liSa'; // 报错

// const 的作用域也是块
const name = 'matt';
if (true) {
    const name = 'liSa';
}
console.log(name) // matt

const 声明的限制只适用于它指向的变量的引用。换句话说,如果const变量引用的是一个对象,那么修改这个对象的属性值并不违反const的限制。
const person = {};
person.name = 'matt'; // ok

JavaScript引擎会为for循环中的let声明分别创建独立的变量实例,虽然const变量和let变量很相似,但是不能用const声明迭代变量(因为迭代变量会自增):

for (const n = 0; n < 5; ++n) {} // 报错:给常量赋值

关于const声明的变量变量名大写,有一种说法是常量的变量名应该用大写的方式,比如:NAME MY_NAME,还有一种是公共的变量的变量名使用大写,页面中自用的小写。我一般按后面的那种来。

声明风格及最佳实践

ECMAScript6增加let和const这两个关键字从客观上为这门语言更精准的声明作用域和语义提供了更好的支持。行为怪异的var所造成的各种问题随着这两个关键字的出现新的有助于提升代码质量的最佳实践也逐渐显现。

  1. 不使用var。
  2. const优先,let次之。

未完待续。。。第三章内容很多,分篇记载。