JavaScript 学习系列一:基本语法

语法概述

基本句法和变量

语句以分号结尾,一个分号就表示一个语句结束。

JavaScript允许省略var,直接对未声明的变量赋值。也就是说,var a = 1 与 a = 1,这两条语句的效果相同。但是由于这样的做法很容易不知不觉地创建全局变量(尤其是在函数内部),所以建议总是使用var命令声明变量。

JavaScript引擎的工作方式是,先解析代码,获取所有被声明的变量,然后再一行一行地运行。这造成的结果,就是所有的变量的声明语句(var 命令声明的变量),都会被提升到代码的头部,这就叫做变量提升(hoisting)。

JavaScript语言的标识符对大小写敏感。

JavaScript使用大括号将语句组成区块,但与大多数编程语言不一样,JavaScript的区块不构成单独的作用域(scope)。

else代码块总是跟随离自己最近的那个if语句。

switch结构,每个case代码块内部的break语句不能少,否则会接下去执行下一个case代码块,而不是跳出switch结构。

1
2
3
4
5
6
7
8
9
10
switch (fruit) {
case "banana":
// ...
break;
case "apple":
// ...
break;
default:
// ...
}

switch语句后面的表达式与case语句后面的表示式,在比较运行结果时,采用的是严格相等运算符(===),这意味着比较时不会发生类型转换。

switch结构不利于代码重用,往往可以用对象形式重写。

1
2
3
4
5
6
7
8
9
10
11
var o = {
banana: function (){ return },
apple: function (){ return },
default: function (){ return }
};

if (o[fruit]){
o[fruit]();
} else {
o['default']();
}

break语句用于跳出代码块或循环。continue语句用于立即终止本次循环,返回循环结构的头部,开始下一次循环。

数据类型

JavaScript的数据类型,共有六个类别和两个特殊值。六个类别的数据类型又可以分成两组:原始类型(primitive type)和合成类型(complex type)。原始类型包括三种数据类型:数值(number)、字符串(string)和布尔值(boolean),合成类型也包括三种数据类型:对象(object)、数组(array)和函数(function)。两个特殊值null和undefined。

JavaScript有三种方法,可以确定一个值到底是什么类型:

  • typeof运算符
  • instanceof运算符
  • Object.prototype.toString方法

typeof运算符可以返回一个值的数据类型,数值、字符串、布尔值分别返回number、string、boolean,函数返回function,undefined返回undefined,除此以外,都返回object(包括数组、对象、null、window)。要想区分数组(Array)和对象(Object),可以用 instanceof运算符(if ( arr instanceof Array ) )。

利用 typeof undefined 返回undefined这一点,typeof可以用来检查一个没有声明的变量,而不报错。

1
2
3
if (typeof v !== "undefined"){
// ...
}

null表示”没有对象”,即该处不应该有值(转为数值时为0)。典型用法是:

  • 作为函数的参数,表示该函数的参数不是对象。
  • 作为对象原型链的终点。

undefined表示”缺少值”,就是此处应该有一个值,但是还未定义(转为数值时为NaN)。典型用法是:

  • 变量被声明了,但没有赋值时,就等于undefined。
  • 调用函数时,应该提供的参数没有提供,该参数等于undefined。
  • 对象没有赋值的属性,该属性的值为undefined。
  • 函数没有返回值时,默认返回undefined。

JavaScript会将预期为布尔值的位置出现的值自动转为布尔值,下面六个值被转为false,其他值都视为true。需要注意的是,空数组([])和空对象({})对应的布尔值,都是true。

  • undefined
  • null
  • false
  • 0
  • NaN
  • “”(空字符串)

结尾的分号

分号表示一条语句的结尾。但是,有一些语法结构不需要在语句的结尾添加分号,主要是以下三种情况:

  • for和while循环
  • 分支语句:if, switch, try
  • 函数的声明语句(但是函数表达式仍然要使用分号)

一般来说,在没有分号结尾的情况下,如果下一行起首的是(、 [ 、+、-、/这五个字符中的一个,分号不会被自动添加。只有下一行的开始与本行的结尾,无法放在一起解释,JavaScript引擎才会自动添加分号。另外,如果一行的起首是“自增”(++)或“自减”(–)运算符,则它们的前面会自动添加分号。如果continue、break、return和throw这四个语句后面,直接跟换行符,会自动添加分号。

数值

概述

JavaScript内部,所有数字都是以64位浮点数形式储存,即使整数也是如此。由于浮点数不是精确的值,所以涉及小数的比较和运算要特别小心。

64位浮点数格式的64个二进制位中,第0位到第51位储存有效数字部分,第52到第62位储存指数部分,第63位是符号位,0表示正数,1表示负数。

特殊数值

NaN是JavaScript的特殊值,表示“非数字”(Not a Number),主要出现在将字符串解析成数字出错的场合。NaN不是一种独立的数据类型,而是一种特殊数值,它的数据类型依然属于Number。NaN不等于任何值,包括它本身。

使用isNaN之前,最好判断一下数据类型:

1
2
3
function myIsNaN(value) {
return typeof value === 'number' && isNaN(value);
}

判断NaN更可靠的方法是,利用NaN是JavaScript之中唯一不等于自身的值这个特点,进行判断:

1
2
3
function myIsNaN(value) {
return value !== value;
}

Infinity表示“无穷”。除了0除以0得到NaN,其他任意数除以0,得到Infinity。isFinite函数返回一个布尔值,检查某个值是否为正常值,而不是Infinity。

1
2
3
4
isFinite(Infinity) // false
isFinite(-1) // true
isFinite(true) // true
isFinite(NaN) // false

与数值有关的全局方法

parseInt方法:

  • parseInt方法可以将字符串或小数转化为整数。如果字符串头部有空格,空格会被自动去除。
  • 如果字符串包含不能转化为数字的字符,则不再进行转化,返回已经转好的部分。
  • 如果字符串的第一个字符不能转化为数字(正负号除外),返回NaN。
  • parseInt方法还可以接受第二个参数(2到36之间),表示被解析的值的进制(可以用parseInt方法进行进制的转换)。

parseFloat方法:parseFloat方法用于将一个字符串转为浮点数,其他规则同parseInt。

字符串

要在单引号字符串的内部使用单引号,可以在内部的单引号(或者双引号)前面加上反斜杠,用来转义。

如果长字符串必须分成多行,可以在每一行的尾部使用反斜杠。

字符串可以被视为字符数组,因此可以使用数组的方括号运算符,用来返回某个位置的字符(从0开始)。

字符串是不可变的,一旦被创建,永远无法改变。两个包含着完全相同的字符且字符顺序也相同的字符串被认为是相同的字符串。

1
2
'c' + 'a' + 't' === 'cat'
// true

JavaScript使用Unicode字符集,每个字符在JavaScript内部都是以16位(即2个字节)的UTF-16格式储存。也就是说,JavaScript的单位字符长度固定为2个字节。

1
2
var s = '\u00A9';
s // "©"

对象

概述

对象的生成方法,通常有三种方法:

1
2
3
var o1 = {};
var o2 = new Object();
var o3 = Object.create(null);

读取属性有使用点运算符和方括号运算符两种方法。如果使用方括号运算符,键名必须放在引号里面,否则会被当作变量处理。数字键可以不加引号。

1
2
3
4
5
6
7
8
var o = {
p: "Hello"
0.7: "World"
};

o.p // "Hello"
o["p"] // "Hello"
o[0.7] // "World"

检查变量是否声明时,不要使用 if(window.a) {...} 写法,因为如果a属性是一个空字符串(或其他对应的布尔值为false的情况),则无法起到检查变量是否声明的作用。正确的写法如下:

1
2
3
if('a' in window) {
...
}

查看一个对象本身的所有属性,可以使用 Object.keys 方法,如:Object.keys(o)

删除一个属性,需要使用 delete ,一旦使用delete命令删除某个属性,再读取该属性就会返回undefined,而且该属性不再包含在Object.keys的返回中。

如果不同的变量名指向同一个对象,那么它们都是这个对象的引用,也就是说指向同一个内存地址。修改其中一个变量,会影响到其他所有变量。

for...in 循环用来遍历一个对象的全部属性。如果只想遍历对象本身的属性,可以使用 hasOwnProperty 方法,在循环内部做一个判断。

1
2
3
4
5
for (var key in person) {
if (person.hasOwnProperty(key)) {
console.log(key);
}
}

类似数组的对象

一个对象,只要有 数字键 和 length属性 ,就是一个类似数组的对象(array-like object)。但无法使用数组特有的一些方法,比如pop和push方法。而且,length属性不会随着成员的变化而变化。

1
2
3
4
5
6
7
8
var a = {
0:'a',
1:'b'
length:2
};

a[1] // 'b'
a.length // 2

典型的类似数组的对象是函数的arguments对象,以及大多数DOM元素集(比如 document.getElementsByTagName 的返回值),还有字符串。

通过函数的call方法,可以用slice方法将类似数组的对象,变成真正的数组。

1
var arr = Array.prototype.slice.call(a);

遍历类似数组的对象,可以采用for循环,也可以采用数组的forEach方法。

数组

本质上,数组也属于对象,是字典结构(dictionary)的一个变种。所以typeof运算符返回数组的类型是object。

数组的length属性是可写的。如果人为设置一个小于当前成员个数的值,该数组的成员会自动减少到length设置的值。如果设置length大于当前元素个数,则数组的成员数量会增加到这个值,新增的位置填入空元素(undefined)。将数组清空的一个有效方法,就是将length属性设为0。

使用delete命令删除一个值,会形成空位,不影响length属性。因为这个键还在,只是值变为了undefined。

遍历数组的两种方法,一是使用for-in循环,二是用for循环或者while循环结合length属性。

函数

JavaScript的函数与其他数据类型处于同等地位,可以使用其他数据类型的地方就能使用函数。可以把函数赋值给变量和对象的属性,也可以当作参数传入其他函数,或者作为函数的结果返回。

JavaScript引擎将函数名视同变量名,所以采用function命令声明函数时,整个函数会被提升到代码头部,即条件无效。要达到在条件语句中定义函数的目的,只有使用函数表达式。

1
2
3
if (false){
var f = function (){};
}

函数的name属性返回紧跟在function关键字之后的那个函数名。函数的toString方法返回函数的源码。

JavaScript只有两种作用域:一种是全局作用域,变量在整个程序中一直存在;另一种是函数作用域,变量只在函数内部存在。

JavaScript的函数参数传递方式是传值传递(passes by value),这意味着,在函数体内修改参数值,不会影响到函数外部。但是对于复合类型的变量来说,属性值是传址传递(pass by reference),也就是说,属性值是通过地址读取的。所以在函数体内修改复合类型变量的属性值,会影响到函数外部。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 修改原始类型的参数值
var p = 1;
function f(p){
p = 2;
}
f(p);
p // 1

// 修改对象的属性值
var o = { p:1 };
function f(obj){
obj.p = 2;
}
f(o);
o.p // 2

arguments对象包含了函数运行时的所有参数,这个对象只有在函数体内部才可以使用。

闭包(closure)就是定义在函数体内部的函数。其特点在于,在函数外部可以读取函数的内部变量。

1
2
3
4
5
6
7
8
9
function f() {
var v = 1;
var c = function (){
return v;
};
return c;
}
var o = f();
o(); // 1

有时,需要在定义函数之后,立即调用该函数。然而不能在函数的定义之后加上圆括号,因为Javascript引擎看到function关键字之后,认为后面跟的是函数定义语句,不应该以圆括号结尾。解决方法就是让引擎知道,圆括号前面的部分不是函数定义语句,而是一个表达式,可以对此进行运算。

1
2
3
4
5
// 写法1
(function(){ /* code */ }());

// 写法2
(function(){ /* code */ })();

通常情况下,只对匿名函数使用这种“立即执行的函数表达式”。它的目的有两个:一是不必为函数命名,避免了污染全局变量;二是IIFE内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。

运算符

加法运算符(+)需要注意的地方是,它除了用于数值的相加,还能用于字符串的连接(在运算子之中有字符串的情况下)。这种由于参数不同,而改变自身行为的现象,叫做“重载”(overload)。

加法运算符以外的其他算术运算符,都不会发生重载。它们的规则是:所有运算子一律转为数值,再进行相应的数学运算。

余数运算符(%)返回前一个运算子被后一个运算子除,所得的余数。运算结果的正负号由第一个运算子的正负号决定。为了得到正确的负数的余数值,需要先使用绝对值函数。

严格相等运算符(===)会比较值的类型和引用是否相同,相等运算符(==)在比较不同类型的数据时,会先将数据进行类型转换。

取反运算符(!)有转换数据类型的作用,对于非布尔值的数据,取反运算符会自动将其转为布尔值。如果对一个值连续做两次取反运算,等于将其转为对应的布尔值,与Boolean函数的作用相同。这是一种常用的类型转换的写法。

1
2
3
4
// 当x非null,非undefined,非""时,执行条件语句中代码
if(!!x){
//do something!
}

且运算符的运算规则是:如果第一个运算子的布尔值为true,则返回第二个运算子的值(注意是值,不是布尔值);如果第一个运算子的布尔值为false,则直接返回第一个运算子的值,且不再对第二个运算子求值。这种机制即“短路”。

尝试从undefined的成员属性中取值将会产生TypeError异常,这时可通过 && 运算符来避免错误。

1
2
3
4
a.b  // undefined
a.b.c // throw "TypeError"
// 可改为
a.b && a.b.c

或运算符的运算规则是:如果第一个运算子的布尔值为true,则返回第一个运算子的值,且不再对第二个运算子求值;如果第一个运算子的布尔值为false,则返回第二个运算子的值。或运算符常用于为一个变量设置默认值。

1
2
3
4
5
// 函数调用时,没有提供参数,则该参数默认设置为空字符串。
function saveText(text) {
text = text || '';
// ...
}

三元条件运算符用问号(?)和冒号(:),分隔三个表达式。如果第一个表达式的布尔值为true,则返回第二个表达式的值,否则返回第三个表达式的值。通常来说,三元条件表达式与 if...else 语句具有同样表达效果,区别是后者没有返回值。

“异或运算”在两个二进制位不同时返回1,相同时返回0。连续对两个数a和b进行三次异或运算,aˆ=b, bˆ=a, aˆ=b,可以互换它们的值而不引入临时变量(详见 维基百科)。

左移运算符表示将一个数的二进制形式向前移动,尾部补0。左移运算符用于二进制数值非常方便。

右移运算符表示将一个数的二进制形式向右移动,头部补上最左位的值,即整数补0,负数补1。右移运算可以模拟2的整除运算。

在JavaScript中,圆括号是一种运算符,它有两种用法:如果把表达式放在圆括号之中,作用是求值;如果跟在函数的后面,作用是调用函数。

void运算符的作用是执行一个表达式,然后返回undefined。这个运算符主要是用于书签工具(bookmarklet)或者用于在超级链接中插入代码,目的是返回undefined可以防止网页跳转。

1
2
3
4
// 写法1
javascript:void window.open("http://example.com/")
// 写法2
<a href="javascript:void(0)" onclick="f();">文字</a>

数据类型转换

Number函数将字符串转为数值,要比parseInt函数严格很多。基本上,只要有一个字符无法转成数值,整个字符串就会被转为NaN。

JavaScript自动转换有很大的不确定性,而且不易除错,建议在预期为布尔值、数值、字符串的地方,全部使用Boolean、Number和String方法进行显式转换。

四个特殊表达式(具体可参考 此文):

  • 空数组 + 空数组

    1
    2
    [] + []
    // ""
  • 空数组 + 空对象

    1
    2
    [] + {}
    // "[object Object]"
  • 空对象 + 空数组

    1
    2
    {} + []
    // 0
  • 空对象 + 空对象

    1
    2
    {} + {}
    // NaN

错误处理机制

Error对象

Error对象的实例有三个最基本的属性:

  • name:错误名称
  • message:错误提示信息
  • stack:错误的堆栈(非标准属性,但是大多数平台支持)

JavaScript的原生错误类型

Error对象是最一般的错误类型,在它的基础上,JavaScript还定义了其他6种错误,也就是说,存在Error的6个派生对象。

  • SyntaxError是解析代码时发生的语法错误。
  • ReferenceError是引用一个不存在的变量时发生的错误。
  • RangeError是当一个值超出有效范围时发生的错误。主要有几种情况,一是数组长度为负数,二是Number对象的方法参数超出范围,以及函数堆栈超过最大值。
  • TypeError是变量或参数不是预期类型时发生的错误。
  • URIError是URI相关函数的参数不正确时抛出的错误。
  • eval函数没有被正确执行时,会抛出EvalError错误。

自定义错误

通过继承Error对象,可以自定义一个错误对象

1
2
3
4
5
6
7
8
9
function UserError(message) {
this.message = message || "默认信息";
this.name = "UserError";
}

UserError.prototype = new Error();
UserError.prototype.constructor = UserError;

new UserError("这是自定义的错误!");

try…catch…finally

throw语句的作用是中断程序执行,抛出一个意外或错误。它接受一个表达式作为参数。

为了对错误进行处理,需要使用try…catch结构。为了捕捉不同类型的错误,catch代码块之中可以加入判断语句。

1
2
3
if (e instanceof RangeError) {
// code
}

try…catch结构允许在最后添加一个finally代码块,表示不管是否出现错误,都必需在最后运行的语句。而且即使有return语句在前,finally代码块依然会得到执行,且在其执行完毕后,才会显示return语句的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
function idle(x) {
try {
console.log(x);
return 'result';
} finally {
console.log("FINALLY");
}
}

idle('hello')
// hello
// FINALLY
// "result"

编程风格

语法标记的风格

表示区块起首的大括号,不要另起一行。

1
2
3
block {
  ...
}

圆括号在JavaScript中有两种作用,一种表示函数的调用,另一种表示表达式的组合。可以用空格,区分这两种不同的括号。表示函数调用和函数定义时,函数名与左括号之间没有空格。其他情况时,前面位置的语法元素与左括号之间,都有一个空格。

虽然在循环和判断的代码体只有一行时,JavaScript允许该区块(block)省略大括号。但最好总是使用大括号表示区块。

语法命令的风格

避免使用全局变量;如果不得不使用,用大写字母表示变量名,比如UPPER_CASE。

JavaScript使用new命令,从构造函数生成一个新对象。这种做法的问题是,一旦你忘了加上new,myObject()内部的this关键字就会指向全局对象。因此,尽量使用Object.create()命令,替代new命令。如果不得不使用new,构造函数的函数名,采用首字母大写。

所有变量声明都放在函数的头部,所有函数都在使用之前定义。

不要使用”相等”(==)运算符,只使用”严格相等”(===)运算符。

不要将不同目的的语句,合并成一行。

不要使用自增(++)和自减(–)运算符,用+=和-=代替。

避免使用switch…case结构,用对象结构代替。

eval函数的作用是将一段字符串当作语句执行。问题是eval不提供单独的作用域,而是直接在当前作用域运行。因此,避免使用eval函数。

参考教程

JavaScript 标准参考教程:基本语法

坚持原创技术分享,您的支持将鼓励我继续创作!