JS语言基础
目录
语法
关键字与保留字
变量
var关键字
let声明
暂时性死区
全局变量
for循环中的let声明
条件声明
const声明
语法
1. 区分大小写
无论是变量、函数名还是操作符,都区分大小写。
2. 标识符
所谓标识符,就是变量、函数、属性或函数参数的名称。
标识符可以由一或多个下列字符组成:第一个字符必须是一个字母、下划线(_)或美元符号($);剩下的其他字符可以是字母、下划线、美元符号或数字。
按照惯例,ECMAScript 标识符使用驼峰大小写形式,即第一个单词的首字母小写,后面每个单词的首字母大写:
3. 注释
ECMAScript 采用C 语言风格的注释,包括单行注释和块注释。单行注释以两个斜杠字符开头,如:
// 单行注释
块注释以一个斜杠和一个星号(/*
)开头,以它们的反向组合(*/
)结尾,如:
/* 这是多行
注释 */
4. 严格模式
ECMAScript 5 增加了严格模式(strict mode)的概念。
严格模式是一种不同的JavaScript 解析和执行模型,ECMAScript 3 的一些不规范写法在这种模式下会被处理,对于不安全的活动将抛出错误。
要对整个脚本启用严格模式,在脚本开头加上这一行:
"use strict";
任何支持的JavaScript引擎看到它都会切换到严格模式。选择这种语法形式的目的是不破坏ECMAScript 3 语法。也可以单独指定一个函数在严格模式下执行,只要把这个预处理指令放到函数体开头即可:
function doSomething() {
"use strict";
// 函数体
}
语句
ECMAScript 中的语句以分号结尾。省略分号意味着由解析器确定语句在哪里结尾,如下面的例子所示:
let sum = a + b // 没有分号也有效,但不推荐
let diff = a - b; // 加分号有效,推荐
多条语句可以合并到一个C 语言风格的代码块中。代码块由一个左花括号 {
标识开始,一个右花括号}
标识结束:
if (test) {
test = false;
console.log(test);
}
关键字与保留字
ECMA-262 描述了一组保留的关键字,关键字有特殊用途,比如表示控制语句的开始和结束,或者执行特定的操作。按照规定,保留的关键字不能用作标识符或属性名。
规范中也描述了一组未来的保留字,同样不能用作标识符或属性名。虽然保留字在语言中没有特定用途,但它们是保留给将来做关键字用的。
break do in typeof
case else instanceof var
catch export new void
class extends return while
const finally super with
continue for switch yield
debugger function this
default if throw
delete import try
始终保留:
enum
严格模式下保留:
implements package public
interface protected static
let private
模块代码中保留:
await
变量
Window 对象描述
Window 对象表示一个浏览器窗口或一个框架。在客户端 JavaScript 中,Window 对象是全局对象,所有的表达式都在当前的环境中计算。也就是说,要引用当前窗口根本不需要特殊的语法,可以把那个窗口的属性作为全局变量来使用。例如,可以只写 document,而不必写 window.document。
同样,可以把当前窗口对象的方法当作函数来使用,如只写 alert(),而不必写 Window.alert()。
除了上面列出的属性和方法,Window 对象还实现了核心 JavaScript 所定义的所有全局属性和方法。
变量是程序在内存中申请的一块用来存放数据的空间。即存放数据的一个容器。通过变量名获取数据并修改。变量值是存入变量空间内的值。
ECMAScript 变量是松散类型的,意思是变量可以用于保存任何类型的数据。每个变量只不过是一个用于保存任意值的命名占位符。
有3 个关键字可以声明变量:var、const 和 let。其中,var 在ECMAScript 的所有版本中都可以使用,而 const 和 let 只能在ECMAScript 6 及更晚的版本中使用。
var关键字
var 操作符(注意var 是一个关键字),后跟变量名(即标识符,如前所述):
这行代码定义了一个名为message 的变量,可以用它保存任何类型的值。可以同时定义变量并设置它的值:
var message = "hi";
var声明作用域
使用var 操作符定义的变量会成为包含它的函数的局部变量。比如,使用var在一个函数内部定义一个变量,就意味着该变量将在函数退出时被销毁:
function test() {
var message = "hi"; // 局部变量
}
test();
console.log(message); // 出错!
而在函数内定义变量时省略var 操作符,可以创建一个全局变量:
function test() {
message = "hi"; // 全局变量
}
test();
console.log(message); // "hi"
去掉之前的var 操作符之后,message 就变成了全局变量。只要调用一次函数test(),就会定义这个变量,并且可以在函数外部访问到。
注意
虽然可以通过省略var 操作符定义全局变量,但不推荐这么做。在局部作用域中定义的全局变量很难维护,也会造成困惑。这是因为不能一下子断定省略var 是不是有意而为之。在严格模式下,如果像这样给未声明的变量赋值,则会导致抛出ReferenceError。
如果需要定义多个变量,可以在一条语句中用逗号分隔每个变量(及可选的初始化):
var message = "hi",
found = false,
age = 29;
为ECMAScript 是松散类型的,所以使用不同数据类型初始化的变量可以用一条语句来声明。插入换行和空格缩进并不是必需的,但这样有利于阅读理解。
在严格模式下,不能定义名为eval 和arguments 的变量,否则会导致语法错误。
用 var 声明的变量的作用域是它当前的执行上下文,它可以是嵌套的函数,或者对于声明在任何函数外的变量来说是全局。
如果你重新声明一个JavaScript 变量,它将不会丢失其值。 当赋值给未声明的变量, 则执行赋值后, 该变量会被隐式地创建为全局变量(它将成为全局对象的属性)。但在函数内部对已经var声明过的变量赋值操作,不会将它变为全局变量。
var声明提升
使用var 时,下面的代码不会报错。这是因为使用这个关键字声明的变量会自动提升到函数作用域顶部:
function foo() {
console.log(age);
var age = 26;
}
foo(); // undefined
之所以不会报错,是因为ECMAScript 运行时把它看成等价于如下代码:
function foo() {
var age;
console.log(age);
age = 26;
}
foo(); // undefined
这就是所谓的“提升”(hoist),也就是把所有变量声明都拉到函数作用域的顶部。
用var声明的变量会被提升到其作用域的顶部,并使用 undefined 值对其进行初始化。
此外,反复多次使用var 声明同一个变量也没有问题:
function foo() {
var age = 16;
var age = 26;
var age = 36;
console.log(age);
}
foo(); // 36
let声明
let 跟var 的作用差不多,但有着非常重要的区别。
最明显的区别是,let 声明的范围是块作用域,而var 声明的范围是函数作用域。
例子:
var声明
if (true) {
var name = 'Matt';
console.log(name); // Matt
}
console.log(name); // Matt
let声明
if (true) {
let age = 26;
console.log(age); // 26
}
console.log(age); // ReferenceError: age 没有定义
age 变量之所以不能在if 块外部被引用,是因为它的作用域仅限于该块内部。块作用域是函数作用域的子集,因此适用于var 的作用域限制同样也适用于let。
let也不允许同一个块作用域中出现冗余声明。这样会导致报错:
var name;
var name;
let age;
let age; // SyntaxError;标识符age 已经声明过了
但不在同一个块中时,不会报错
let age = 30;
console.log(age); // 30
if (true) {
let age = 26;
console.log(age); // 26
}
对声明冗余报错不会因混用let 和var 而受影响。这两个关键字声明的并不是不同类型的变量,它们只是指出变量在相关作用域如何存在。
var name;
let name; // SyntaxError
let age;
var age; // SyntaxError
即声明冗余会报错,不同的声明只是表示了它的作用域的不同。
暂时性死区
与var声明的另一个区别是let声明的变量在作用域中不会被提升
// age 不会被提升
console.log(age); // ReferenceError:age 没有定义
let age = 26;
在解析代码时,JavaScript 引擎也会注意出现在块后面的let 声明,只不过在此之前不能以任何方式来引用未声明的变量。在let声明之前的执行瞬间被称为“暂时性死区”(temporal dead zone),在此阶段引用任何后面才声明的变量都会抛出ReferenceError。
全局变量
使用let 在全局作用域中声明的变量不会成为window 对象的属性(var 声明的变量则会)。
Window 对象表示浏览器中打开的窗口。
var name = 'Matt';
console.log(window.name); // 'Matt'
let age = 26;
console.log(window.age); // undefined
不过,let 声明仍然是在全局作用域中发生的,相应变量会在页面的生命周期内存续。因此,为了避免SyntaxError,必须确保页面不会重复声明同一个变量。
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); // ReferenceError: i 没有定义
在使用let 声明迭代变量时,JavaScript 引擎在后台会为每个迭代循环声明一个新的迭代变量。每个setTimeout 引用的都是不同的变量实例,所以console.log 输出的是我们期望的值,也就是循环执行过程中每个迭代变量的值。
for (let i = 0; i < 5; ++i) {
setTimeout(() => console.log(i), 0)
}
// 会输出0、1、2、3、4
这种每次迭代声明一个独立变量实例的行为适用于所有风格的for 循环,包括for-in 和for-of循环。
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 的作用域是块,所以不可能检查前面是否已经使用let 声明过同名变量,同时也就不可能在没有声明的情况下声明它(即使检查到没有,使用let声明也只是在检查块作用域中)。
try {
console.log(age); // 如果age 没有声明过,则会报错
}
catch(error) {
let age;
}
// age 被限制在catch {}块的作用域内
比如上面例子声明的age会被限制在catch块中,在下面这个外部调用时会报错。
因为let声明局限于块中,外部的修改也只是相当于重新声明了一个变量。
if (typeof name === 'undefined') {
let name;
}
// name 被限制在if {} 块的作用域内
// 因此这个赋值形同全局赋值
name = 'Matt';
const声明
const 的行为与let 基本相同,唯一一个重要的区别是用它声明变量时必须同时初始化变量,且尝试修改const 声明的变量会导致运行时错误。
const age = 26;
age = 36; // TypeError: 给常量赋值
// const 也不允许重复声明
const name = 'Matt';
const name = 'Nicholas'; // SyntaxError
// const 声明的作用域也是块
const name = 'Matt';
if (true) {
const name = 'Nicholas';
}
console.log(name); // Matt
const 声明的限制只适用于它指向的变量的引用。换句话说,如果const 变量引用的是一个对象,那么修改这个对象内部的属性并不违反const 的限制。
const person = {};
person.name = 'Matt'; // ok
JavaScript 引擎会为for 循环中的let 声明分别创建独立的变量实例,虽然const 变量跟let 变量很相似,但是不能用const 来声明迭代变量(因为迭代变量会自增):
for (const i = 0; i < 10; ++i) {} // TypeError:给常量赋值
不过,如果你只想用const 声明一个不会被修改的for 循环变量,那也是可以的。也就是说,每次迭代只是创建一个新变量。这对for-of 和for-in 循环特别有意义
let i = 0;
for (const j = 7; i < 5; ++i) {
console.log(j);
}
// 7, 7, 7, 7, 7
for (const key in {a: 1, b: 2}) {
console.log(key);
}
// a, b
for (const value of [1,2,3,4,5]) {
console.log(value);
}
// 1, 2, 3, 4, 5