基本语法
ECMAScript
JavaScript 在 1995 年由 Netscape 公司(网景公司)的 Brendan Eich 在网景导航者浏览器上首次设计实现。1996 年 11 月,Netscape 公司希望能将这门语言制定成国际标准,于是将 JavaScript 提交至国际标准化组织 ECMA(Ecma International)。次年,ECMA 发布了ECMA-262号标准文件的第一版,规定了 JavaScript 的标准化规范,并将这门标准化的脚本语言称为 ECMAScript。
既然 ECMAScript 从始至终就是针对 JavaScript 语言制定的,那为什么不直接叫做 JavaScript 呢?考虑以下两点原因:
- 版权原因:Java 是 Sun 公司的商标,根据授权协议,只有 Netscape 公司可以合法地使用 JavaScript 这个名字,而且 JavaScript 早已被 Netscape 公司注册为商标。
- ECMA 也希望体现这门语言的规范是由 ECMA 制定,而不是 Netscape,这样也能更好地保证这门语言的开放性和中立性。
因此,ECMAScript 是 JavaScript 的规范,而 JavaScript 是 ECMAScript 的其中一种实现。
保留字
ECMAScript 2015 (ES6) 规定的关键字如下:
as const export get null target void
async continue extends if of this while
await debugger false import return throw with
break default finally in set true yield
case delete for instanceof static try
catch do from let super typeof
class else function new switch var
JavaScript 还预留或限制使用某些关键字,目前没有使用,但可能会在未来的版本中使用:
enum implements interface package private protected public
由于历史原因,
arguments
和eval
在某些情况下不允许作为标识符,最好完全避免使用。
标识符命名规则
JavaScript 是一种大小写敏感的语言。JavaScript 标识符可以由数字、大小写字母、下划线、美元符号组成,但不能以数字开头。
注意,一般应避免使用以下划线开头的标识符(这和 C 语言一样)。
实际上,JavaScript 程序是使用 Unicode 字符集编写的,该语言允许在标识符中使用 Unicode 字母、数字和表意文字(但不允许使用表情符号)。为了便于移植和编辑,通常在标识符中只使用 ASCII 字母和数字,但这只是一种编程约定。例如,我们可以使用如下数学符号和单词作为常量和变量:
const π = 3.14;
const sí = true;
要注意的是:相同的 Unicode 字符可能会有多种不同的 Unicode 编码方式,例如字符 é
的 Unicode 序列可以为 \u00E9
,也可以是 e\u0301
。这两种编码在文本编辑器中显示时看起来是完全一样的,但是它们有不同的二进制编码,这意味着它们被 JavaScript 认为是不同的,这可能会导致非常混乱的程序:
const café = 1; // This constant is named "caf\u{e9}"
const café = 2; // This constant is different: "cafe\u{301}"
café // => 1: this constant has one value
café // => 2: this indistinguishable constant has a different value
Unicode 标准定义了所有字符的首选编码,并指定了一个标准化过程将文本转换为适合于比较的规范形式。JavaScript 假设它要解释的源代码已经被规范化了,它自己不做任何规范化。如果您计划在 JavaScript 程序中使用 Unicode 字符,那么应该确保您的编辑器或其他工具对源代码执行 Unicode 规范化,以防止出现不同但视觉上无法区分的标识符。
注释
多行注释中不能嵌套多行注释。
// 单行注释
/* 多行注释 */
语句分隔符和语句终止符
JavaScript 以半角分号 ;
作为语句分隔符和语句终止符。值得注意的是,虽然空行不是 JavaScript 语法的一部分,但 换行符是 JavaScript 语法的一部分 。 JavaScript 识别换行符、回车符和回车/换行序列作为语句终止符,因此在 JavaScript 代码中,有时候可以省略分隔符 ;
:
- 如果两个语句写在不同的行上,通常可以省略这两个语句之间的分号。
- 若语句被花括号
{}
括起来,可以省略右花括号}
后面的分号。
请注意, JavaScript 并不把每个换行符都当作分号来处理:它通常只在不添加隐式分号而无法解析代码时才把断行符当作分号来处理。 更正式的说法是,如果下一个非空格字符不能解释为当前语句的延续,JavaScript将换行符视为分号。考虑以下代码:
let a
a
=
3
console.log(a)
JavaScript 这样解释这段代码:
let a; a = 3; console.log(a);
这些语句终止规则会导致一些奇怪的情况。这段代码看起来像用换行符分隔的两个单独的语句:
let y = x + f
(a+b).toString()
但 JavaScript 是这样解释的:
let y = x + f(a+b).toString();
很可能,这不是代码作者想要的解释。为了作为两个独立的语句工作,在这种情况下需要显式的分号。
当 JavaScript 无法将第二行解析为第一行语句的延续时,JavaScript 将换行符解释为分号,但这个规则有三种例外。
第一种情况涉及 return
、throw
、yield
、break
和 continue
语句,这些语句通常是单独的,但有时候,它们后面会跟一个标识符或表达式。JavaScript 总是将它们后面出现的第一个换行符解释为分号。例如,如果你写:
return
true;
JavaScript 假设您的意思是:
return; true;
不过,你的意思可能是:
return true;
这意味着不能在 return
、break
或 continue
和关键字后面的表达式之间插入换行符。如果您确实插入了一个换行符,那么您的代码很可能会陷入一种难以调试且不明显的错误中。
有些操作符可以作为前缀操作符,也可以作为后缀操作符,例如++
和--
。如果要让这些操作符中作为后缀操作符,则它们必须与应用它们的表达式出现在同一行。
第三种例外涉及“箭头函数”,=>
箭头本身必须与参数列表出现在同一行。
空格
空格也不是 JavaScript 语法的一部分,JavaScript 会忽略所有空格。除了常规的空格字符(\u0020
)之外,JavaScript 还将制表符、各种 ASCII 控制字符和各种 Unicode 空格字符识别为空格。
字符转义
和 C、Python 一致,以反斜杠 \
作为转义符。转义符用于处理一些计算机硬件和软件不能显示、输入或正确处理完整的 Unicode 字符集。
为了支持使用较旧技术的程序员和系统,JavaScript 定义了转义序列,允许我们仅使用 ASCII 字符编写 Unicode 字符。这些 Unicode 转义以字符 \u
开始,后跟四个 16 进制数字(使用大写或小写字母A-F),或者用大括号括起来的一到六个 16 进制数字。这些 Unicode 转义可能以 JavaScript 字符串、正则表达式和标识符的形式出现(但不会以语言关键字的形式出现)。
例如,Unicode 字符 “é,
” 的转义是\u00E9
;这里有三种不同的方式来写一个变量名,包括这个字符:
let café = 1; // Define a variable using a Unicode character
caf\u00e9 // => 1; access the variable using an escape sequence
caf\u{E9} // => 1; another form of the same escape sequence
早期版本的 JavaScript 只支持四位数转义序列。带花括号的版本是在 ES6 中引入的,目的是为了更好地支持需要超过16位元的 Unicode 码点,比如表情符号:
console.log("\u{1F600}"); // Prints a smiley face emoji 😀
声明和初始化
在现代 JavaScript (ES6及以后的版本) 中,变量是用 let
关键字声明的,就像这样:
let i;
let sum;
你也可以在一个 let
语句中声明多个变量:
let i, sum;
在声明变量时,可以同时给它们赋一个初始值:
let message = "hello";
let i = 0, j = 0, k = 0;
let x = 2, y = x*x; // Initializers can use previously declared variables
如果仅声明变量,而不赋初始值,则在代码为其赋值之前,变量的值是未定义的。
使用 const
关键字声明常量。const
的工作方式与 let
类似,只是在声明常量时必须初始化它:
const H0 = 74; // Hubble constant (km/s/Mpc)
const C = 299792.458; // Speed of light in a vacuum (km/s)
const AU = 1.496E8; // Astronomical Unit: distance to the sun (km)
顾名思义,常量不能改变它们的值,任何赋值的尝试都会引发 TypeError
。
try {
const a = 1;
a = 2
} catch (error) {
console.log(error);
}
TypeError: Assignment to constant variable.
通常约定是使用全大写字母的名称来声明常量,以将它们与变量区分开来。
你可能会觉得奇怪,但是你也可以使用 const
来声明 for/in
和 for/of
循环的循环“变量”,只要循环体不重新赋值。在这种情况下,const
声明只是表示该值在循环迭代的持续时间内为常量:
data = [1, 2, 3];
for (const datum of data) {
console.log(datum);
}
1
2
3
在 ES6 之前的 JavaScript 版本中,声明变量的唯一方法是使用 var
关键字,且不支持声明常量。var
的语法就像 let
的语法一样,但它们有一些本质的区别:
-
用
var
声明的变量没有块作用域,因此如果你在函数体之外使用var
,它会声明一个全局变量。 -
用
var
声明的全局变量与用let
声明的全局变量在一个重要的方面不同:用var
声明的全局变量是作为全局对象的属性实现的。全局对象可以引用为globalThis
。例如,如果你通过var x = 2
初始化变量x
,就像你通过globalThis.x = 2;
初始化变量x
(这个类比并不完美)。用var
声明的属性不能用delete
操作符删除。用let
和const
声明的全局变量和常量不是全局对象的属性。 -
用
var
多次声明同一个变量是合法的,而用let
不能重复声明变量。JavaScript 编译器在解析代码时,遇到var
声明的变量,先在当前作用域判断该变量是否已经存在,若不存在,则在当前作用域声明一个新变量,否则忽略var
继续往下编译。var
的重声明实际上是很常见的。变量i
经常被用来表示循环变量,在有多个for
循环的函数中,每个循环都可以为for(var i = 0;...
,因为var
没有将这些变量作用域限定在循环体中,所以每个循环都 (无害地) 重新声明并初始化同一个变量。 -
var
声明最不寻常的特性之一是 hoisting(变量提升):当用var
声明一个变量时,这个声明会被提升到当前作用域的顶部,但变量的初始化保持在原来的地方。例如:
console.log(a); // undefined
var a
console.log(a); // undefined
a = 'hoisting';
var
声明的变量可以在声明的位置之前使用,而不会出错。如果初始化代码还没有运行,那么变量的值会是undefined
。这可能是bug的来源,也是 let
纠正的重要错误特性之一:如果你用 let
声明了一个变量,但试图在 let
语句运行之前使用它,程序将抛出错误,而不仅仅是得到一个未定义的值。
复合声明和赋值*
ES6 实现了一种复合声明和赋值语法,称为解构赋值。在解构赋值中,等号右边的值是一个数组或对象(“结构化的”值),左边使用模仿数组和对象字面量语法的语法指定一个或多个变量名。当发生解构赋值时,将从右边的值中提取一个或多个值(“解构”),并存储到左边命名的变量中。解构赋值可能常用来初始化const
、let
或 var
的声明,但它也可以用在正则赋值表达式中(使用已经声明的变量)。而且,我们将在 §8.3.5 中看到的,解构也可以用于定义函数的形参。
let [x,y] = [1,2]; // Same as let x=1, y=2
[x,y] = [x+1,y+1]; // Same as x = x + 1, y = y + 1
[x,y] = [y,x]; // Swap the value of the two variables
[x,y] // => [3,2]: the incremented and swapped values
命名空间和作用域
flowchart LR
B[Global Scope] & C[Local Scope]
C[Local Scope] --> D[Function Scope] & E[Block Scope]
全局作用域(Global Scope)
JavaScript 文件中只有一个全局范围。所有函数之外的区域被认为是全局范围,在全局范围内定义的变量可以在任何范围内访问和更改。
局部作用域(Local Scope)
局部作用域是相对于全局作用域的概念,局部作用域包括函数作用域和块作用域。各个局部作用域是独立的,相同的变量名可以在不同的局部作用域中使用,而不会相互影响。
函数作用域(Function Scope)
在函数中声明的变量仅在函数内可用,您无法在函数之外访问它。
块作用域(Block Scope)
块作用域是 if
、switch
、for
和 while
代码块的区域。一般来说,只要你看到花括号{}
,它就是一个块。ES6 引入了块作用域的概念,且提供了新关键字 const
和 let
让开发人员在块作用域内声明变量,这意味着这些变量只存在于相应的代码块中。
function foo() {
if (true) {
var fruit1 = 'apple'; //exist in function scope
const fruit2 = 'banana'; //exist in block scope
let fruit3 = 'strawberry'; //exist in block scope
}
console.log(fruit1);
console.log(fruit2); // ReferenceError: fruit2 is not defined
console.log(fruit3); // ReferenceError: fruit3 is not defined
}
console.log(fruit1); // ReferenceError: fruit1 is not defined
{
var txt1 = "defined in global scope";
let txt2 = "defined in block scope";
}
console.log(txt1);
console.log(txt2); // ReferenceError: txt is not defined
从以上代码可以看出,虽然函数代码块也是用花括号括起来的,但函数作用域和块作用域是不一样的,函数内定义的变量只能在函数内访问,而通过var
关键字在块作用域内定义的变量能够在全局作用域访问,因为var
声明的变量是没有块作用域的。
虽然 Python 和 JavaScript 都是动态检查的语言,但 Python 没有提供关键字用来声明变量。因此,如果我们想在局部作用域修改全局作用域变量的值,这两种语言会有不同的行为。Python 默认不能在局部作用域修改外部作用域的变量,除非使用 global
或 nonlocal
关键字来声明变量来自全局作用域或 enclosing 作用域。
JavaScript 则默认能够在局部作用域修改外部作用域的变量,除非在局部作用域重新声明变量,让 JavaScript 解析器知道该变量来自局部作用域。
var fruit = 'apple';
let txt = 'JavaScript Language';
function foo() {
fruit = 'banana';
let txt = 'TypeScript Language';
}
foo();
console.log(fruit);
console.log(txt);
banana
JavaScript Language
相等操作
JavaScript 允许隐式转换变量的类型,这些值类型转换规则会影响相等的定义,==
相等操作符允许隐式类型转换。
if ("1" == 1){
console.log("equal")
}
equal
在实践中,==
相等操作符已弃用,而使用严格的相等操作符 ===
,它不进行类型转换。
if ("1" === 1){
console.log("equal")
}
else{
console.log("not equal")
}
not equal
内置原子类型(基本类型)
JavaScript 的基本类型包括:
- 数字(整型和浮点型)
- 字符串
- 布尔值
null
undefined
null
和 undefined
是特殊的 JavaScript 值,但不是数字、字符串或布尔值。
内置复合类型(对象类型)
不是内置原子类型的内置类型都是对象。
对象(即类型对象的成员)是属性的集合,其中每个属性都有一个名称和一个值(原始值或另一个对象)。
普通的 JavaScript 对象是命名值的无序集合。该语言还定义了一种特殊类型的对象,称为数组,它表示编号值的有序集合。
函数
定义函数的语法格式:
function function_name(parameter1, parameter2){
statements
return object;
}