JavaScript程序

第2章 基本语法

2.1 概述

基本句法和变量

语句

JavaScript程序的执行单位为行(line),也就是一行一行地执行。一般情况下,每一行就是一个语句。

语句(statement)是为了完成某种任务而进行的操作,比如下面就是一行赋值语句:

vara =1+3;

这条语句先用var命令,声明了变量a,然后将1 + 3的运算结果赋值给变量a。

1 + 3叫做表达式(expression),指一个为了得到返回值的计算式。语句和表达式的区别在于,前者主要为了进行某种操作,一般情况下不需要返回值;后者则是为了得到返回值,一定会返回一个值。

凡是JavaScript语言中预期为值的地方,都可以使用表达式。比如,赋值语句的等号右边,预期是一个值,因此可以放置各种表达式。一条语句可以包含多个表达式。

语句以分号结尾,一个分号就表示一个语句结束。多个语句可以写在一行内。

vara =1+3;varb ='abc';

分号前面可以没有任何内容,JavaScript引擎将其视为空语句。

;;;

上面的代码就表示3个空语句。(关于分号的更多介绍,请看后文《代码风格》一节。)

表达式不需要分号结尾。一旦在表达式后面添加分号,则JavaScript引擎就将表达式视为语句,这样会产生一些没有任何意义的语句。

1+3;'abc';

上面两行语句有返回值,但是没有任何意义,因为只是返回一个单纯的值,没有任何其他操作。

变量

变量是对“值”的引用,使用变量等同于引用一个值。每一个变量都有一个变量名。

vara =1;

上面的代码先声明变量a,然后在变量a与数值1之间建立引用关系,也称为将数值1“赋值”给变量a。以后,引用变量a就会得到数值1。最前面的var,是变量声明命令。它表示通知解释引擎,要创建一个变量a。

变量的声明和赋值,是分开的两个步骤,上面的代码将它们合在了一起,实际的步骤是下面这样。

vara;a =1;

如果只是声明变量而没有赋值,则该变量的值是不存在的,JavaScript使用undefined表示这种情况。

vara;a// undefined

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

严格地说,var a = 1与a = 1,这两条语句的效果不完全一样,主要体现在delete命令无法删除前者。不过,绝大多数情况下,这种差异是可以忽略的。

如果一个变量没有声明就直接使用,JavaScript会报错,告诉你变量未定义。

x// ReferenceError: x is not defined

上面代码直接使用变量x,系统就报错,告诉你变量x没有声明。

可以在同一条var命令中声明多个变量。

vara, b;

JavaScirpt是一种动态类型语言,也就是说,变量的类型没有限制,可以赋予各种类型的值。

vara =1;a ='hello';

上面代码中,变量a起先被赋值为一个数值,后来又被重新赋值为一个字符串。第二次赋值的时候,因为变量a已经存在,所以不需要使用var命令。

如果使用var重新声明一个已经存在的变量,是无效的。

varx =1;varx;x// 1

上面代码中,变量x声明了两次,第二次声明是无效的。

但是,如果第二次声明的同时还赋值了,则会覆盖掉前面的值。

varx =1;varx =2;// 等同于varx =1;varx;x =2;

变量提升

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

console.log(a);vara =1;

上面代码首先使用console.log方法,在控制台(console)显示变量a的值。这时变量a还没有声明和赋值,所以这是一种错误的做法,但是实际上不会报错。因为存在变量提升,真正运行的是下面的代码。

vara;console.log(a);a =1;

最后的结果是显示undefined,表示变量a已声明,但还未赋值。

请注意,变量提升只对var命令声明的变量有效,如果一个变量不是用var命令声明的,就不会发生变量提升。

console.log(b);b =1;

上面的语句将会报错,提示“ReferenceError: b is not defined”,即变量b未声明,这是因为b不是用var命令声明的,JavaScript引擎不会将其提升,而只是视为对顶层对象的b属性的赋值。

标识符

标识符(identifier)是用来识别具体对象的一个名称。最常见的标识符就是变量名,以及后面要提到的函数名。JavaScript语言的标识符对大小写敏感,所以a和A是两个不同的标识符。

标识符有一套命名规则,不符合规则的就是非法标识符。JavaScript引擎遇到非法标识符,就会报错。

简单说,标识符命名规则如下:

第一个字符,可以是任意Unicode字母(包括英文字母和其他语言的字母),以及美元符号($)和下划线(_)。

第二个字符及后面的字符,除了Unicode字母、美元符号和下划线,还可以用数字0-9。

下面这些都是合法的标识符。

arg0

_tmp

$elem

π

下面这些则是不合法的标识符。

1a// 第一个字符不能是数字23// 同上***// 标识符不能包含星号a+b// 标识符不能包含加号-d// 标识符不能包含减号或连词线

中文是合法的标识符,可以用作变量名。

var临时变量 =1;

JavaScript有一些保留字,不能用作标识符:arguments、break、case、catch、class、const、continue、debugger、default、delete、do、else、enum、eval、export、extends、false、finally、for、function、if、implements、import、in、instanceof、interface、let、new、null、package、private、protected、public、return、static、super、switch、this、throw、true、try、typeof、var、void、while、with、yield。

另外,还有三个词虽然不是保留字,但是因为具有特别含义,也不应该用作标识符:Infinity、NaN、undefined。

注释

源码中被JavaScript引擎忽略的部分就叫做注释,它的作用是对代码进行解释。Javascript提供两种注释:一种是单行注释,用//起头;另一种是多行注释,放在/* 和 */之间。

// 这是单行注释/*

这是

多行

注释

*/

此外,由于历史上JavaScript兼容HTML代码的注释,所以也被视为单行注释。

x =1;

-->x = 3;

上面代码中,只有x = 1会执行,其他的部分都被注释掉了。

需要注意的是,-->只有在行首,才会被当成单行注释,否则就是一个运算符。

functioncountdown(n){while(n -->0)console.log(n);}countdown(3)// 2// 1// 0

上面代码中,n --> 0实际上会当作n-- > 0,因此输出2、1、0。

区块

JavaScript使用大括号,将多个相关的语句组合在一起,称为“区块”(block)。

与大多数编程语言不一样,JavaScript的区块不构成单独的作用域(scope)。也就是说,区块中的变量与区块外的变量,属于同一个作用域。

{vara =1;}a// 1

上面代码在区块内部,声明并赋值了变量a,然后在区块外部,变量a依然有效,这说明区块不构成单独的作用域,与不使用区块的情况没有任何区别。所以,单独使用的区块在JavaScript中意义不大,很少出现。区块往往用来构成其他更复杂的语法结构,比如for、if、while、function等。

条件语句

条件语句提供一种语法构造,只有满足某个条件,才会执行相应的语句。JavaScript提供if结构和switch结构,完成条件判断。

if 结构

if结构先判断一个表达式的布尔值,然后根据布尔值的真伪,执行不同的语句。

if(expression)  statement;// 或者if(expression) statement;

上面是if结构的基本形式。需要注意的是,expression(表达式)必须放在圆括号中,表示对表达式求值。如果结果为true,就执行紧跟在后面的语句(statement);如果结果为false,则跳过statement的部分。

if(m ===3)  m +=1;

上面代码表示,只有在m等于3时,才会将其值加上1。

这种写法要求条件表达式后面只能有一个语句。如果想执行多个语句,必须在if的条件判断之后,加上大括号,表示代码块。

if(m ===3) {  m +=1;}

建议总是在if语句中使用大括号,因为这样方便插入语句。

注意,if后面的表达式,不要混淆“赋值表达式”(=)与“严格相等运算符”(===)或“相等运算符”(==)。因为,“赋值表达式”不具有比较作用。

varx =1;vary =2;if(x = y) {console.log(x);}// "2"

上面代码的原意是,当x等于y的时候,才执行相关语句。但是,不小心将“严格相等运算符”写成“赋值表达式”,结果变成了将y赋值给x,然后条件就变成了,变量x的值(等于2)自动转为布尔值以后,判断其是否为true。

这种错误可以正常生成一个布尔值,因而不会报错。为了避免这种情况,有些开发者习惯将常量写在运算符的左边,这样的话,一旦不小心将相等运算符写成赋值运算符,就会报错,因为常量不能被赋值。

if(x =2) {// 不报错if(2= x) {// 报错

至于为什么优先采用“严格相等运算符”(===),而不是“相等运算符”(==),请参考《运算符》一节。

if...else结构

if代码块后面,还可以跟一个else代码块,表示不满足条件时,所要执行的代码。

if(m ===3) {// then}else{// else}

上面代码判断变量m是否等于3,如果等于就执行if代码块,否则执行else代码块。

对同一个变量进行多次判断时,多个if...else语句可以连写在一起。

if(m ===0) {// ...}elseif(m ===1) {// ...}elseif(m ===2) {// ...}else{// ...}

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

varm =1;varn =2;if(m !==1)if(n ===2)console.log('hello');elseconsole.log('world');

上面代码不会有任何输出,else代码块不会得到执行,因为它跟着的是最近的那个if语句,相当于下面这样。

if(m !==1) {if(n ===2) {console.log('hello');    }else{console.log('world');  }}

如果想让else代码块跟随最上面的那个if语句,就要改变大括号的位置。

if(m !==1) {if(n ===2) {console.log('hello');    }}else{console.log('world');}// world

switch结构

多个if...else连在一起使用的时候,可以转为使用更方便的switch结构。

switch(fruit) {case"banana":// ...break;case"apple":// ...break;default:// ...}

上面代码根据变量fruit的值,选择执行相应的case。如果所有case都不符合,则执行最后的default部分。需要注意的是,每个case代码块内部的break语句不能少,否则会接下去执行下一个case代码块,而不是跳出switch结构。

varx =1;switch(x) {case1:console.log('x 于1');case2:console.log('x 等于2');default:console.log('x 等于其他值');}// x等于1// x等于2// x等于其他值

上面代码中,case代码块之中没有break语句,导致不会跳出switch结构,而会一直执行下去。

switch语句部分和case语句部分,都可以使用表达式。

switch(1+3) {case2+2:    f();break;default:    neverhappens();}

上面代码的default部分,是永远不会执行到的。

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

varx =1;switch(x) {casetrue:console.log('x发生类型转换');default:console.log('x没有发生类型转换');}// x没有发生类型转换

上面代码中,由于变量x没有发生类型转换,所以不会执行case true的情况。这表明,switch语句内部采用的是“严格相等运算符”,详细解释请参考《运算符》一节。

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

functiongetItemPricing(customer, item){switch(customer.type) {case'VIP':returnitem.price * item.quantity *0.50;case'Preferred':returnitem.price * item.quantity *0.75;case'Regular':casedefault:returnitem.price * item.quantity;  }}

上面代码根据不同用户,返回不同的价格。你可以发现,switch语句包含的三种情况,内部逻辑都是相同的,不同只是折扣率。这启发我们可以用对象属性,重写这个判断。

varpricing = {'VIP':0.50,'Preferred':0.75,'Regular':1.0};functiongetItemPricing(customer, item){if(pricing[customer.type])returnitem.price * item.quantity * pricing[customer.type];elsereturnitem.price * item.quantity * pricing.Regular;}

如果价格档次再多一些,对象属性写法的简洁优势就更明显了。

三元运算符 ?:

JavaScript还有一个三元运算符(即该运算符需要三个运算子)?:,也可以用于逻辑判断。

(contidion) ? expression1 : expression2

上面代码中,如果contidion为true,则返回expression1的值,否则返回expression2的值。

vareven = (n %2===0) ?true:false;

上面代码中,如果n可以被2整除,则even等于true,否则等于false。它等同于下面的形式。

vareven;if(n %2===0) {  even =true;}else{  even =false;}

这个三元运算符可以被视为if...else...的简写形式,因此可以用于多种场合。

varmyVar;console.log( myVar  ?'myVar has a value':'myVar do not has a value')// myVar do not has a value

上面代码利用三元运算符,输出相应的提示。

varmsg ='The number '+ n  +' is '+ ((n %2===0) ?'even':'odd');

上面代码利用三元运算符,在字符串之中插入不同的值。

循环语句

循环语句用于重复执行某个操作,它有多种形式。

while循环

While语句包括一个循环条件和一段代码块,只要条件为真,就不断循环执行代码块。

while(expression)  statement;// 或者while(expression) statement;

while语句的循环条件是一个表达式(express),必须放在圆括号中。代码块部分,如果只有一条语句(statement),可以省略大括号,否则就必须加上大括号。

while(expression) {  statement;}

下面是while语句的一个例子。

vari =0;while(i <100) {console.log('i当前为:'+ i);  i +=1;}

上面的代码将循环100次,直到i等于100为止。

下面的例子是一个无限循环,因为条件总是为真。

while(true) {console.log("Hello, world");}

for循环

for语句是循环命令的另一种形式。

for(initialize; test; increment)  statement// 或者for(initialize; test; increment) {  statement}

for语句后面的括号里面,有三个表达式。

初始化表达式(initialize):确定循环的初始值,只在循环开始时执行一次。

测试表达式(test):检查循环条件,只要为真就进行后续操作。

递增表达式(increment):完成后续操作,然后返回上一步,再一次检查循环条件。

下面是一个例子。

varx =3;for(vari =0; i < x; i++) {console.log(i);}// 0// 1// 2

上面代码中,初始化表达式是var i = 0,即初始化一个变量i;测试表达式是i < x,即只要i小于x,就会执行循环;递增表达式是i++,即每次循环结束后,i增大1。

所有for循环,都可以改写成while循环。上面的例子改为while循环,代码如下。

varx =3;vari =0;while(i < x) {console.log(i);  i++;}

for语句的三个部分(initialize,test,increment),可以省略任何一个,也可以全部省略。

for( ; ; ){console.log('Hello World');}

上面代码省略了for语句表达式的三个部分,结果就导致了一个无限循环。

do...while循环

do...while循环与while循环类似,唯一的区别就是先运行一次循环体,然后判断循环条件。

dostatementwhile(expression);// 或者do{  statement}while(expression);

不管条件是否为真,do..while循环至少运行一次,这是这种结构最大的特点。另外,while语句后面的分号不能省略。

下面是一个例子。

varx =3;vari =0;do{console.log(i);  i++;}while(i < x);

break语句和continue语句

break语句和continue语句都具有跳转作用,可以让代码不按既有的顺序执行。

break语句用于跳出代码块或循环。

vari =0;while(i <100) {console.log('i当前为:'+ i);  i++;if(i ===10)break;}

上面代码只会执行10次循环,一旦i等于10,就会跳出循环。

for循环也可以使用break语句跳出循环。

for(vari =0; i <5; i++) {console.log(i);if(i ===3)break;}// 0// 1// 2// 3

上面代码执行到i等于3,就会跳出循环。

continue语句用于立即终止本轮循环,返回循环结构的头部,开始下一轮循环。

vari =0;while(i <100){  i++;if(i%2===0)continue;console.log('i当前为:'+ i);}

上面代码只有在i为奇数时,才会输出i的值。如果i为偶数,则直接进入下一轮循环。

如果存在多重循环,不带参数的break语句和continue语句都只针对最内层循环。

标签(label)

JavaScript语言允许,语句的前面有标签(label),相当于定位符,用于跳转到程序的任意位置,标签的格式如下。

label:

  statement

标签可以是任意的标识符,但是不能是保留字,语句部分可以是任意语句。

标签通常与break语句和continue语句配合使用,跳出特定的循环。

top:for(vari =0; i <3; i++){for(varj =0; j <3; j++){if(i ===1&& j ===1)breaktop;console.log('i='+ i +', j='+ j);    }  }// i=0, j=0// i=0, j=1// i=0, j=2// i=1, j=0

上面代码为一个双重循环区块,break命令后面加上了top标签(注意,top不用加引号),满足条件时,直接跳出双层循环。如果break语句后面不使用标签,则只能跳出内层循环,进入下一次的外层循环。

continue语句也可以与标签配合使用。

top:for(vari =0; i <3; i++){for(varj =0; j <3; j++){if(i ===1&& j ===1)continuetop;console.log('i='+ i +', j='+ j);    }  }// i=0, j=0// i=0, j=1// i=0, j=2// i=1, j=0// i=2, j=0// i=2, j=1// i=2, j=2

上面代码中,continue命令后面有一个标签名,满足条件时,会跳过当前循环,直接进入下一轮外层循环。如果continue语句后面不使用标签,则只能进入下一轮的内层循环。

数据类型

概述

JavaScript语言的每一个值,都属于某一种数据类型。JavaScript的数据类型,共有六种。(ES6又新增了第七种Symbol类型的值,本教程不涉及。)

数值(number):整数和小数(比如1和3.14)

字符串(string):字符组成的文本(比如"Hello World")

布尔值(boolean):true(真)和false(假)两个特定值

undefined:表示“未定义”或不存在,即此处目前没有任何值

null:表示空缺,即此处应该有一个值,但目前为空

对象(object):各种值组成的集合

通常,我们将数值、字符串、布尔值称为原始类型(primitive type)的值,即它们是最基本的数据类型,不能再细分了。而将对象称为合成类型(complex type)的值,因为一个对象往往是多个原始类型的值的合成,可以看作是一个存放各种值的容器。至于undefined和null,一般将它们看成两个特殊值。

对象又可以分成三个子类型。

狭义的对象(object)

数组(array)

函数(function)

狭义的对象和数组是两种不同的数据组合方式,而函数其实是处理数据的方法。JavaScript把函数当成一种数据类型,可以像其他类型的数据一样,进行赋值和传递,这为编程带来了很大的灵活性,体现了JavaScript作为“函数式语言”的本质。

这里需要明确的是,JavaScript的所有数据,都可以视为广义的对象。不仅数组和函数属于对象,就连原始类型的数据(数值、字符串、布尔值)也可以用对象方式调用。为了避免混淆,此后除非特别声明,本教程的”对象“都特指狭义的对象。

本教程将详细介绍所有的数据类型。undefined和null两个特殊值和布尔类型Boolean比较简单,将在本节介绍,其他类型将各自有单独的一节。

typeof运算符

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

typeof运算符

instanceof运算符

Object.prototype.toString方法

instanceof运算符和Object.prototype.toString方法,将在后文相关章节介绍。这里着重介绍typeof运算符。

typeof运算符可以返回一个值的数据类型,可能有以下结果。

(1)原始类型

数值、字符串、布尔值分别返回number、string、boolean。

typeof123// "number"typeof'123'// "string"typeoffalse// "boolean"

(2)函数

函数返回function。

functionf(){}typeoff// "function"

(3)undefined

undefined返回undefined。

typeofundefined// "undefined"

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

v// ReferenceError: v is not definedtypeofv// "undefined"

上面代码中,变量v没有用var命令声明,直接使用就会报错。但是,放在typeof后面,就不报错了,而是返回undefined。

实际编程中,这个特点通常用在判断语句。

// 错误的写法if(v) {// ...}// ReferenceError: v is not defined// 正确的写法if(typeofv ==="undefined") {// ...}

(4)其他

除此以外,其他情况都返回object。

typeofwindow// "object"typeof{}// "object"typeof[]// "object"typeofnull// "object"

从上面代码可以看到,空数组([])的类型也是object,这表示在JavaScript内部,数组本质上只是一种特殊的对象。

另外,null的类型也是object,这是由于历史原因造成的。1995年JavaScript语言的第一版,所有值都设计成32位,其中最低的3位用来表述数据类型,object对应的值是000。当时,只设计了五种数据类型(对象、整数、浮点数、字符串和布尔值),完全没考虑null,只把它当作object的一种特殊值,32位全部为0。这是typeof null返回object的根本原因。

为了兼容以前的代码,后来就没法修改了。这并不是说null就属于对象,本质上null是一个类似于undefined的特殊值。

既然typeof对数组(array)和对象(object)的显示结果都是object,那么怎么区分它们呢?instanceof运算符可以做到。

varo = {};vara = [];oinstanceofArray// falseainstanceofArray// true

instanceof运算符的详细解释,请见《面向对象编程》一章。

null和undefined

概述

null与undefined都可以表示“没有”,含义非常相似。将一个变量赋值为undefined或null,老实说,语法效果几乎没区别。

vara =undefined;// 或者vara =null;

上面代码中,a变量分别被赋值为undefined和null,这两种写法的效果几乎等价。

在if语句中,它们都会被自动转为false,相等运算符(==)甚至直接报告两者相等。

if(!undefined) {console.log('undefined is false');}// undefined is falseif(!null) {console.log('null is false');}// null is falseundefined==null// true

上面代码说明,两者的行为是何等相似!Google公司开发的JavaScript语言的替代品Dart语言,就明确规定只有null,没有undefined!

既然含义与用法都差不多,为什么要同时设置两个这样的值,这不是无端增加复杂度,令初学者困扰吗?这与历史原因有关。

1995年JavaScript诞生时,最初像Java一样,只设置了null作为表示"无"的值。根据C语言的传统,null被设计成可以自动转为0。

Number(null)// 05+null// 5

但是,JavaScript的设计者Brendan Eich,觉得这样做还不够,有两个原因。首先,null像在Java里一样,被当成一个对象。但是,JavaScript的值分成原始类型和合成类型两大类,Brendan Eich觉得表示"无"的值最好不是对象。其次,JavaScript的最初版本没有包括错误处理机制,发生数据类型不匹配时,往往是自动转换类型或者默默地失败。Brendan Eich觉得,如果null自动转为0,很不容易发现错误。

因此,Brendan Eich又设计了一个undefined。他是这样区分的:null是一个表示"无"的对象,转为数值时为0;undefined是一个表示"无"的原始值,转为数值时为NaN。

Number(undefined)// NaN5+undefined// NaN

但是,这样的区分在实践中很快就被证明不可行。目前null和undefined基本是同义的,只有一些细微的差别。

null的特殊之处在于,JavaScript把它包含在对象类型(object)之中。

typeofnull// "object"

上面代码表示,查询null的类型,JavaScript返回object(对象)。

这并不是说null的数据类型就是对象,而是JavaScript早期部署中的一个约定俗成,其实不完全正确,后来再想改已经太晚了,会破坏现存代码,所以一直保留至今。

注意,JavaScript的标识名区分大小写,所以undefined和null不同于Undefined和Null(或者其他仅仅大小写不同的词形),后者只是普通的变量名。

用法和含义

对于null和undefined,可以大致可以像下面这样理解。

null表示空值,即该处的值现在为空。比如,调用函数时,不需要传入某个参数,这时就可以传入null。

undefined表示“未定义”,下面是返回undefined的典型场景。

// 变量声明了,但没有赋值vari;i// undefined// 调用函数时,应该提供的参数没有提供,该参数等于undefinedfunctionf(x){returnx;}f()// undefined// 对象没有赋值的属性varo =newObject();o.p// undefined// 函数没有返回值时,默认返回undefinedfunctionf(){}f()// undefined

布尔值

布尔值代表“真”和“假”两个状态。“真”用关键字true表示,“假”用关键字false表示。布尔值只有这两个值。

下列运算符会返回布尔值:

两元逻辑运算符:&&(And),||(Or)

前置逻辑运算符:!(Not)

相等运算符:===,!==,==,!=

比较运算符:>,>=,<,<=

如果JavaScript预期某个位置应该是布尔值,会将该位置上现有的值自动转为布尔值。转换规则是除了下面六个值被转为false,其他值都视为true。

undefined

null

false

0

NaN

""或''(空字符串)

布尔值往往用于程序流程的控制,请看一个例子。

if('') {console.log(true);}// 没有任何输出

上面代码的if命令后面的判断条件,预期应该是一个布尔值,所以JavaScript自动将空字符串,转为布尔值false,导致程序不会进入代码块,所以没有任何输出。

需要特别注意的是,空数组([])和空对象({})对应的布尔值,都是true。

if([]) {console.log(true);}// trueif({}) {console.log(true);}// true

更多关于数据类型转换的介绍,参见《数据类型转换》一节。

2.2 数值

概述

整数和浮点数

JavaScript内部,所有数字都是以64位浮点数形式储存,即使整数也是如此。所以,1与1.0是相同的,是同一个数。

1===1.0// true

这就是说,在JavaScript语言的底层,根本没有整数,所有数字都是小数(64位浮点数)。容易造成混淆的是,某些运算只有整数才能完成,此时JavaScript会自动把64位浮点数,转成32位整数,然后再进行运算,参见《运算符》一节的”位运算“部分。

由于浮点数不是精确的值,所以涉及小数的比较和运算要特别小心。

0.1+0.2===0.3// false0.3/0.1// 2.9999999999999996(0.3-0.2) === (0.2-0.1)// false

数值精度

根据国际标准IEEE 754,JavaScript浮点数的64个二进制位,从最左边开始,是这样组成的。

第1位:符号位,0表示正数,1表示负数

第2位到第12位:储存指数部分

第13位到第64位:储存小数部分(即有效数字)

符号位决定了一个数的正负,指数部分决定了数值的大小,小数部分决定了数值的精度。

IEEE 754规定,有效数字第一位默认总是1,不保存在64位浮点数之中。也就是说,有效数字总是1.xx...xx的形式,其中xx..xx的部分保存在64位浮点数之中,最长可能为52位。因此,JavaScript提供的有效数字最长为53个二进制位。

(-1)^符号位 * 1.xx...xx* 2^指数位

上面公式是一个数在JavaScript内部实际的表现形式。

精度最多只能到53个二进制位,这意味着,绝对值小于2的53次方的整数,即-(253-1)到253-1,都可以精确表示。

Math.pow(2,53)// 9007199254740992Math.pow(2,53) +1// 9007199254740992Math.pow(2,53) +2// 9007199254740994Math.pow(2,53) +3// 9007199254740996Math.pow(2,53) +4// 9007199254740996

从上面示例可以看到,大于2的53次方以后,整数运算的结果开始出现错误。所以,大于等于2的53次方的数值,都无法保持精度。

Math.pow(2,53)// 9007199254740992// 多出的三个有效数字,将无法保存9007199254740992111// 9007199254740992000

上面示例表明,大于2的53次方以后,多出来的有效数字(最后三位的111)都会无法保存,变成0。

数值范围

根据标准,64位浮点数的指数部分的长度是11个二进制位,意味着指数部分的最大值是2047(2的11次方减1)。也就是说,64位浮点数的指数部分的值最大为2047,分出一半表示负数,则JavaScript能够表示的数值范围为21024到2-1023(开区间),超出这个范围的数无法表示。

如果指数部分等于或超过最大正值1024,JavaScript会返回Infinity(关于Infinity的介绍参见下文),这称为“正向溢出”;如果等于或超过最小负值-1023(即非常接近0),JavaScript会直接把这个数转为0,这称为“负向溢出”。

varx =0.5;for(vari =0; i <25; i++) {  x = x * x;}x// 0

上面代码对0.5连续做25次平方,由于最后结果太接近0,超出了可表示的范围,JavaScript就直接将其转为0。

至于具体的最大值和最小值,JavaScript提供Number对象的MAX_VALUE和MIN_VALUE属性表示(参见《Number对象》一节)。

Number.MAX_VALUE// 1.7976931348623157e+308Number.MIN_VALUE// 5e-324

数值的表示法

JavaScript的数值有多种表示方法,可以用字面形式直接表示,比如35(十进制)和0xFF(十六进制)。

数值也可以采用科学计数法表示,下面是几个科学计数法的例子。

123e3// 123000123e-3// 0.123-3.1E+12.1e-23

科学计数法允许字母e或E的后面,跟着一个整数,表示这个数值的指数部分。

以下两种情况,JavaScript会自动将数值转为科学计数法表示,其他情况都采用字面形式直接表示。

(1)小数点前的数字多于21位。

1234567890123456789012// 1.2345678901234568e+21123456789012345678901// 123456789012345680000

(2)小数点后的零多于5个。

// 小数点后紧跟5个以上的零,// 就自动转为科学计数法0.0000003// 3e-7// 否则,就保持原来的字面形式0.000003// 0.000003

数值的进制

使用字面量(literal)时,JavaScript对整数提供四种进制的表示方法:十进制、十六进制、八进制、2进制。

十进制:没有前导0的数值。

八进制:有前缀0o或0O的数值,或者有前导0、且只用到0-7的七个阿拉伯数字的数值。

十六进制:有前缀0x或0X的数值。

二进制:有前缀0b或0B的数值。

默认情况下,JavaScript内部会自动将八进制、十六进制、二进制转为十进制。下面是一些例子。

0xff// 2550o377// 2550b11// 3

如果八进制、十六进制、二进制的数值里面,出现不属于该进制的数字,就会报错。

0xzz// 报错0o88// 报错0b22// 报错

上面代码中,十六进制出现了字母z、八进制出现数字8、二进制出现数字2,因此报错。

通常来说,有前导0的数值会被视为八进制,但是如果前导0后面有数字8和9,则该数值被视为十进制。

0888// 8880777// 511

用前导0表示八进制,处理时很容易造成混乱。ES5的严格模式和ES6,已经废除了这种表示法,但是浏览器目前还支持。

特殊数值

JavaScript提供几个特殊的数值。

正零和负零

前面说过,JavaScript的64位浮点数之中,有一个二进制位是符号位。这意味着,任何一个数都有一个对应的负值,就连0也不例外。

在JavaScript内部,实际上存在2个0:一个是+0,一个是-0。它们是等价的。

-0=== +0// true0===-0// true0=== +0// true

几乎所有场合,正零和负零都会被当作正常的0。

+0// 0-0// 0(-0).toString()// '0'(+0).toString()// '0'

唯一有区别的场合是,+0或-0当作分母,返回的值是不相等的。

(1/ +0) === (1/-0)// false

上面代码之所以出现这样结果,是因为除以正零得到+Infinity,除以负零得到-Infinity,这两者是不相等的(关于Infinity详见后文)。

NaN

(1)含义

NaN是JavaScript的特殊值,表示“非数字”(Not a Number),主要出现在将字符串解析成数字出错的场合。

5-'x'// NaN

上面代码运行时,会自动将字符串x转为数值,但是由于x不是数值,所以最后得到结果为NaN,表示它是“非数字”(NaN)。

另外,一些数学函数的运算结果会出现NaN。

Math.acos(2)// NaNMath.log(-1)// NaNMath.sqrt(-1)// NaN

0除以0也会得到NaN。

0/0// NaN

需要注意的是,NaN不是一种独立的数据类型,而是一种特殊数值,它的数据类型依然属于Number,使用typeof运算符可以看得很清楚。

typeofNaN// 'number'

(2)运算规则

NaN不等于任何值,包括它本身。

NaN===NaN// false

由于数组的indexOf方法,内部使用的是严格相等运算符,所以该方法对NaN不成立。

[NaN].indexOf(NaN)// -1

NaN在布尔运算时被当作false。

Boolean(NaN)// false

NaN与任何数(包括它自己)的运算,得到的都是NaN。

NaN+32// NaNNaN-32// NaNNaN*32// NaNNaN/32// NaN

(3)判断NaN的方法

isNaN方法可以用来判断一个值是否为NaN。

isNaN(NaN)// trueisNaN(123)// false

但是,isNaN只对数值有效,如果传入其他值,会被先转成数值。比如,传入字符串的时候,字符串会被先转成NaN,所以最后返回true,这一点要特别引起注意。也就是说,isNaN为true的值,有可能不是NaN,而是一个字符串。

isNaN('Hello')// true// 相当于isNaN(Number('Hello'))// true

出于同样的原因,对于对象和数组,isNaN也返回true。

isNaN({})// true// 等同于isNaN(Number({}))// trueisNaN(['xzy'])// true// 等同于isNaN(Number(['xzy']))// true

但是,对于空数组和只有一个数值成员的数组,isNaN返回false。

isNaN([])// falseisNaN([123])// falseisNaN(['123'])// false

上面代码之所以返回false,原因是这些数组能被Number函数转成数值,请参见《数据类型转换》一节。

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

functionmyIsNaN(value){returntypeofvalue ==='number'&&isNaN(value);}

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

functionmyIsNaN(value){returnvalue !== value;}

Infinity

(1)定义

Infinity表示“无穷”,用来表示两种场景。一种是一个正的数值太大,或一个负的数值太小,无法表示;另一种是非0数值除以0,得到Infinity。

// 场景一Math.pow(2,Math.pow(2,100))// Infinity// 场景二0/0// NaN1/0// Infinity

上面代码中,第一个场景是一个表达式的计算结果太大,超出了JavaScript能够表示的范围,因此返回Infinity。第二个场景是0除以0会得到NaN,而非0数值除以0,会返回Infinity。

Infinity有正负之分,Infinity表示正的无穷,-Infinity表示负的无穷。

Infinity=== -Infinity// false1/-0// -Infinity-1/-0// Infinity

上面代码中,非零正数除以-0,会得到-Infinity,负数除以-0,会得到Infinity。

由于数值正向溢出(overflow)、负向溢出(underflow)和被0除,JavaScript都不报错,而是返回Infinity,所以单纯的数学运算几乎没有可能抛出错误。

Infinity大于一切数值(除了NaN),-Infinity小于一切数值(除了NaN)。

Infinity>1000// true-Infinity<-1000// true

Infinity与NaN比较,总是返回false。

Infinity>NaN// falseInfinity

(2)运算规则

Infinity的四则运算,符合无穷的数学计算规则。

5*Infinity// Infinity5-Infinity// -InfinityInfinity/5// Infinity5/Infinity// 0

Infinity加上或乘以Infinity,返回的还是Infinity。

Infinity+Infinity// InfinityInfinity*Infinity// Infinity

Infinity减去或除以Infinity,得到NaN。

Infinity-Infinity// NaNInfinity/Infinity// NaN

(3)isFinite函数

isFinite函数返回一个布尔值,检查某个值是不是正常数值,而不是Infinity。

isFinite(Infinity)// falseisFinite(-1)// trueisFinite(true)// trueisFinite(NaN)// false

上面代码表示,如果对NaN使用isFinite函数,也返回false,表示NaN不是一个正常值。

与数值相关的全局方法

parseInt()

(1)基本用法

parseInt方法用于将字符串转为整数。

parseInt('123')// 123

如果字符串头部有空格,空格会被自动去除。

parseInt('  81')// 81

如果parseInt的参数不是字符串,则会先转为字符串再转换。

parseInt(1.23)// 1// 等同于parseInt('1.23')// 1

字符串转为整数的时候,是一个个字符依次转换,如果遇到不能转为数字的字符,就不再进行下去,返回已经转好的部分。

parseInt('8a')// 8parseInt('12**')// 12parseInt('12.34')// 12parseInt('15e2')// 15parseInt('15px')// 15

上面代码中,parseInt的参数都是字符串,结果只返回字符串头部可以转为数字的部分。

如果字符串的第一个字符不能转化为数字(后面跟着数字的正负号除外),返回NaN。

parseInt('abc')// NaNparseInt('.3')// NaNparseInt('')// NaNparseInt('+')// NaNparseInt('+1')// 1

parseInt的返回值只有两种可能,不是一个十进制整数,就是NaN。

如果字符串以0x或0X开头,parseInt会将其按照十六进制数解析。

parseInt('0x10')// 16

如果字符串以0开头,将其按照10进制解析。

parseInt('011')// 11

对于那些会自动转为科学计数法的数字,parseInt会将科学计数法的表示方法视为字符串,因此导致一些奇怪的结果。

parseInt(1000000000000000000000.5)// 1// 等同于parseInt('1e+21')// 1parseInt(0.0000008)// 8// 等同于parseInt('8e-7')// 8

(2)进制转换

parseInt方法还可以接受第二个参数(2到36之间),表示被解析的值的进制,返回该值对应的十进制数。默认情况下,parseInt的第二个参数为10,即默认是十进制转十进制。

parseInt('1000')// 1000// 等同于parseInt('1000',10)// 1000

下面是转换指定进制的数的例子。

parseInt('1000',2)// 8parseInt('1000',6)// 216parseInt('1000',8)// 512

上面代码中,二进制、六进制、八进制的1000,分别等于十进制的8、216和512。这意味着,可以用parseInt方法进行进制的转换。

如果第二个参数不是数值,会被自动转为一个整数。这个整数只有在2到36之间,才能得到有意义的结果,超出这个范围,则返回NaN。如果第二个参数是0、undefined和null,则直接忽略。

parseInt('10',37)// NaNparseInt('10',1)// NaNparseInt('10',0)// 10parseInt('10',null)// 10parseInt('10',undefined)// 10

如果字符串包含对于指定进制无意义的字符,则从最高位开始,只返回可以转换的数值。如果最高位无法转换,则直接返回NaN。

parseInt('1546',2)// 1parseInt('546',2)// NaN

上面代码中,对于二进制来说,1是有意义的字符,5、4、6都是无意义的字符,所以第一行返回1,第二行返回NaN。

前面说过,如果parseInt的第一个参数不是字符串,会被先转为字符串。这会导致一些令人意外的结果。

parseInt(0x11,36)// 43// 等同于parseInt(String(0x11),36)parseInt('17',36)

上面代码中,十六进制的0x11会被先转为十进制的17,再转为字符串。然后,再用36进制解读字符串17,最后返回结果43。

这种处理方式,对于八进制的前缀0,尤其需要注意。

parseInt(011,2)// NaN// 等同于parseInt(String(011),2)parseInt('011',2)// 3

上面代码中,第一行的011会被先转为字符串9,因为9不是二进制的有效字符,所以返回NaN。第二行的字符串011,会被当作二进制处理,返回3。

ES5不再允许将带有前缀0的数字视为八进制数,而是要求忽略这个0。但是,为了保证兼容性,大部分浏览器并没有部署这一条规定。

parseFloat()

parseFloat方法用于将一个字符串转为浮点数。

parseFloat('3.14')// 3.14

如果字符串符合科学计数法,则会进行相应的转换。

parseFloat('314e-2')// 3.14parseFloat('0.0314E+2')// 3.14

如果字符串包含不能转为浮点数的字符,则不再进行往后转换,返回已经转好的部分。

parseFloat('3.14more non-digit characters')// 3.14

parseFloat方法会自动过滤字符串前导的空格。

parseFloat('\t\v\r12.34\n ')// 12.34

如果参数不是字符串,或者字符串的第一个字符不能转化为浮点数,则返回NaN。

parseFloat([])// NaNparseFloat('FF2')// NaNparseFloat('')// NaN

上面代码中,尤其值得注意,parseFloat会将空字符串转为NaN。

这些特点使得parseFloat的转换结果不同于Number函数。

parseFloat(true)// NaNNumber(true)// 1parseFloat(null)// NaNNumber(null)// 0parseFloat('')// NaNNumber('')// 0parseFloat('123.45#')// 123.45Number('123.45#')// NaN

2.3 字符串

概述

定义

字符串就是零个或多个排在一起的字符,放在单引号或双引号之中。

'abc'"abc"

单引号字符串的内部,可以使用双引号。双引号字符串的内部,可以使用单引号。

'key = "value"'"It's a long journey"

上面两个都是合法的字符串。

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

'Did she say \'Hello\'?'// "Did she say 'Hello'?""Did she say \"Hello\"?"// "Did she say "Hello"?"

字符串默认只能写在一行内,分成多行将会报错。

'a

b

c'// SyntaxError: Unexpected token ILLEGAL

上面代码将一个字符串分成三行,JavaScript就会报错。

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

varlongString ="Long \

long \

long \

string";longString// "Long long long string"

上面代码表示,加了反斜杠以后,原来写在一行的字符串,可以分成多行,效果与写在同一行完全一样。注意,反斜杠的后面必须是换行符,而不能有其他字符(比如空格),否则会报错。

连接运算符(+)可以连接多个单行字符串,用来模拟多行字符串。

varlongString ='Long '+'long '+'long '+'string';

另外,有一种利用多行注释,生成多行字符串的变通方法。

(function(){/*

line 1

line 2

line 3

*/}).toString().split('\n').slice(1,-1).join('\n')// "line 1 line 2 line 3"

转义

反斜杠(\\)在字符串内有特殊含义,用来表示一些特殊字符,所以又称为转义符。

需要用反斜杠转义的特殊字符,主要有下面这些:

\0代表没有内容的字符(\u0000)

\b后退键(\u0008)

\f换页符(\u000C)

\n换行符(\u000A)

\r回车键(\u000D)

\t制表符(\u0009)

\v垂直制表符(\u000B)

\'单引号(\u0027)

\"双引号(\u0022)

\\\\反斜杠(\u005C)

\XXX用三个八进制数(000到377)表示字符,XXX对应该字符的Unicode,比如\251表示版权符号。

\xXX用两个十六进制数(00到FF)表示字符,XX对应该字符的Unicode,比如\xA9表示版权符号。

\uXXXX用四位十六进制的Unicode编号代表某个字符,比如\u00A9表示版权符号。

下面是最后三种字符的特殊写法的例子。

'\251'// "©"'\xA9'// "©"'\u00A9'// "©"'\172'==='z'// true'\x7A'==='z'// true'\u007A'==='z'// true

如果非特殊字符前面使用反斜杠,则反斜杠会被省略。

'\a'// "a"

上面代码表示a是一个正常字符,前面加反斜杠没有特殊含义,则反斜杠会被自动省略。

如果字符串的正常内容之中,需要包含反斜杠,则反斜杠前需要再加一个反斜杠,用来对自身转义。

"Prev \\ Next"// "Prev \ Next"

字符串与数组

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

vars ='hello';s[0]// "h"s[1]// "e"s[4]// "o"// 直接对字符串使用方括号运算符'hello'[1]// "e"

如果方括号中的数字超过字符串的范围,或者方括号中根本不是数字,则返回undefined。

'abc'[3]// undefined'abc'[-1]// undefined'abc'['x']// undefined

但是,字符串与数组的相似性仅此而已。实际上,无法改变字符串之中的单个字符。

vars ='hello';deletes[0];s// "hello"s[1] ='a';s// "hello"s[5] ='!';s// "hello"

上面代码表示,字符串内部的单个字符无法改变和增删,这些操作会默默地失败。

字符串之所以类似于字符数组,实际是由于对字符串进行方括号运算时,字符串会自动转换为一个字符串对象(详见《标准库》一章的《包装对象》一节)。

length属性

length属性返回字符串的长度,该属性也是无法改变的。

vars ='hello';s.length// 5s.length =3;s.length// 5s.length =7;s.length// 5

上面代码表示字符串的length属性无法改变,但是不会报错。

字符集

JavaScript使用Unicode字符集,也就是说在JavaScript内部,所有字符都用Unicode表示。

不仅JavaScript内部使用Unicode储存字符,而且还可以直接在程序中使用Unicode,所有字符都可以写成"\uxxxx"的形式,其中xxxx代表该字符的Unicode编码。比如,\u00A9代表版权符号。

vars ='\u00A9';s// "©"

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

但是,UTF-16有两种长度:对于U+0000到U+FFFF之间的字符,长度为16位(即2个字节);对于U+10000到U+10FFFF之间的字符,长度为32位(即4个字节),而且前两个字节在0xD800到0xDBFF之间,后两个字节在0xDC00到0xDFFF之间。举例来说,U+1D306对应的字符为𝌆,它写成UTF-16就是0xD834 0xDF06。浏览器会正确将这四个字节识别为一个字符,但是JavaScript内部的字符长度总是固定为16位,会把这四个字节视为两个字符。

vars ='\uD834\uDF06';s// "𝌆"s.length// 2/^.$/.test(s)// falses.charAt(0)// ""s.charAt(1)// ""s.charCodeAt(0)// 55348s.charCodeAt(1)// 57094

上面代码说明,对于于U+10000到U+10FFFF之间的字符,JavaScript总是视为两个字符(字符的length属性为2),用来匹配单个字符的正则表达式会失败(JavaScript认为这里不止一个字符),charAt方法无法返回单个字符,charCodeAt方法返回每个字节对应的十进制值。

所以处理的时候,必须把这一点考虑在内。对于4个字节的Unicode字符,假定C是字符的Unicode编号,H是前两个字节,L是后两个字节,则它们之间的换算关系如下。

// 将大于U+FFFF的字符,从Unicode转为UTF-16H =Math.floor((C -0x10000) /0x400) +0xD800L = (C -0x10000) %0x400+0xDC00// 将大于U+FFFF的字符,从UTF-16转为UnicodeC = (H -0xD800) *0x400+ L -0xDC00+0x10000

下面的正则表达式可以识别所有UTF-16字符。

([\0-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF])

由于JavaScript引擎(严格说是ES5规格)不能自动识别辅助平面(编号大于0xFFFF)的Unicode字符,导致所有字符串处理函数遇到这类字符,都会产生错误的结果(详见《标准库》一章的String对象章节)。如果要完成字符串相关操作,就必须判断字符是否落在0xD800到0xDFFF这个区间。

下面是能够正确处理字符串遍历的函数。

functiongetSymbols(string){varlength = string.length;varindex =-1;varoutput = [];varcharacter;varcharCode;while(++index < length) {    character = string.charAt(index);    charCode = character.charCodeAt(0);if(charCode >=0xD800&& charCode <=0xDBFF) {      output.push(character + string.charAt(++index));    }else{      output.push(character);    }  }returnoutput;}varsymbols = getSymbols('𝌆');symbols.forEach(function(symbol){// ...});

替换(String.prototype.replace)、截取子字符串(String.prototype.substring,String.prototype.slice)等其他字符串操作,都必须做类似的处理。

Base64转码

Base64是一种编码方法,可以将任意字符转成可打印字符。使用这种编码方法,主要不是为了加密,而是为了不出现特殊字符,简化程序的处理。

JavaScript原生提供两个Base64相关方法。

btoa():字符串或二进制值转为Base64编码

atob():Base64编码转为原来的编码

varstring ='Hello World!';btoa(string)// "SGVsbG8gV29ybGQh"atob('SGVsbG8gV29ybGQh')// "Hello World!"

这两个方法不适合非ASCII码的字符,会报错。

btoa('你好')// Uncaught DOMException: The string to be encoded contains characters outside of the Latin1 range.

要将非ASCII码字符转为Base64编码,必须中间插入一个转码环节,再使用这两个方法。

functionb64Encode(str){returnbtoa(encodeURIComponent(str));}functionb64Decode(str){returndecodeURIComponent(atob(str));}b64Encode('你好')// "JUU0JUJEJUEwJUU1JUE1JUJE"b64Decode('JUU0JUJEJUEwJUU1JUE1JUJE')// "你好"

对象

概述

生成方法

对象(object)是JavaScript的核心概念,也是最重要的数据类型。JavaScript的所有数据都可以被视为对象。

简单说,所谓对象,就是一种无序的数据集合,由若干个“键值对”(key-value)构成。

varo = {p:'Hello World'};

上面代码中,大括号就定义了一个对象,它被赋值给变量o。这个对象内部包含一个键值对(又称为“成员”),p是“键名”(成员的名称),字符串Hello World是“键值”(成员的值)。键名与键值之间用冒号分隔。如果对象内部包含多个键值对,每个键值对之间用逗号分隔。

varo = {p1:'Hello',p2:'World'};

对象的生成方法,通常有三种方法。除了像上面那样直接使用大括号生成({}),还可以用new命令生成一个Object对象的实例,或者使用Object.create方法生成。

varo1 = {};varo2 =newObject();varo3 =Object.create(null);

上面三行语句是等价的。一般来说,第一种采用大括号的写法比较简洁,第二种采用构造函数的写法清晰地表示了意图,第三种写法一般用在需要对象继承的场合。关于第二种写法,详见《标准库》一章的Object对象一节,第三种写法详见《面向对象编程》一章。

键名

对象的所有键名都是字符串,所以加不加引号都可以。上面的代码也可以写成下面这样。

varo = {'p':'Hello World'};

如果键名是数值,会被自动转为字符串。

varo ={1:'a',3.2:'b',1e2:true,1e-2:true,.234:true,0xFF:true,};o// Object {//  1: "a",//  100: true,//  255: true,//  3.2: "b",//  0.01: true,//  0.234: true// }

但是,如果键名不符合标识名的条件(比如第一个字符为数字,或者含有空格或运算符),也不是数字,则必须加上引号,否则会报错。

varo = {'1p':"Hello World",'h w':"Hello World",'p+q':"Hello World"};

上面对象的三个键名,都不符合标识名的条件,所以必须加上引号。

注意,JavaScript的保留字可以不加引号当作键名。

varobj = {for:1,class:2};

属性

对象的每一个“键名”又称为“属性”(property),它的“键值”可以是任何数据类型。如果一个属性的值为函数,通常把这个属性称为“方法”,它可以像函数那样调用。

varo = {p:function(x){return2* x;  }};o.p(1)// 2

上面的对象就有一个方法p,它就是一个函数。

对象的属性之间用逗号分隔,最后一个属性后面可以加逗号(trailing comma),也可以不加。

varo = {p:123,m:function(){ ... },}

上面的代码中m属性后面的那个逗号,有或没有都不算错。但是,ECMAScript 3不允许添加逗号,所以如果要兼容老式浏览器(比如IE 8),那就不能加这个逗号。

属性可以动态创建,不必在对象声明时就指定。

varobj = {};obj.foo =123;obj.foo// 123

上面代码中,直接对obj对象的foo属性赋值,结果就在运行时创建了foo属性。

由于对象的方法就是函数,因此也有name属性。

varobj = {m1:functionm1(){},m2:function(){}};obj.m1.name// m1obj.m2.name// undefined

对象的引用

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

varo1 = {};varo2 = o1;o1.a =1;o2.a// 1o2.b =2;o1.b// 2

上面代码中,o1和o2指向同一个对象,因此为其中任何一个变量添加属性,另一个变量都可以读写该属性。

此时,如果取消某一个变量对于原对象的引用,不会影响到另一个变量。

varo1 = {};varo2 = o1;o1 =1;o2// {}

上面代码中,o1和o2指向同一个对象,然后o1的值变为1,这时不会对o2产生影响,o2还是指向原来的那个对象。

但是,这种引用只局限于对象,对于原始类型的数据则是传值引用,也就是说,都是值的拷贝。

varx =1;vary = x;x =2;y// 1

上面的代码中,当x的值发生变化后,y的值并不变,这就表示y和x并不是指向同一个内存地址。

表达式还是语句?

对象采用大括号表示,这导致了一个问题:如果行首是一个大括号,它到底是表达式还是语句?

{foo:1}

JavaScript引擎读到上面这行代码,会发现可能有两种含义。第一种可能是,这是一个表达式,表示一个包含foo属性的对象;第二种可能是,这是一个语句,表示一个代码区块,里面有一个标签foo,指向表达式123。

为了避免这种歧义性,JavaScript规定,如果行首是大括号,一律解释为语句(即代码块)。如果要解释为表达式(即对象),必须在大括号前加上圆括号。

({foo:1})

这种差异在eval语句中反映得最明显。

eval('{foo: 1}')// 123eval('({foo: 1})')// {foo: 123}

上面代码中,如果没有圆括号,eval将其理解为一个代码块;加上圆括号以后,就理解成一个对象。

属性的操作

读取属性

读取对象的属性,有两种方法,一种是使用点运算符,还有一种是使用方括号运算符。

varo = {p:'Hello World'};o.p// "Hello World"o['p']// "Hello World"

上面代码分别采用点运算符和方括号运算符,读取属性p。

请注意,如果使用方括号运算符,键名必须放在引号里面,否则会被当作变量处理。但是,数字键可以不加引号,因为会被当作字符串处理。

varo = {0.7:'Hello World'};o['0.7']// "Hello World"o[0.7]// "Hello World"

方括号运算符内部可以使用表达式。

o['hello'+' world']o[3+3]

数值键名不能使用点运算符(因为会被当成小数点),只能使用方括号运算符。

obj.0xFF// SyntaxError: Unexpected tokenobj[0xFF]// true

上面代码的第一个表达式,对数值键名0xFF使用点运算符,结果报错。第二个表达式使用方括号运算符,结果就是正确的。

检查变量是否声明

如果读取一个不存在的键,会返回undefined,而不是报错。可以利用这一点,来检查一个全局变量是否被声明。

// 检查a变量是否被声明if(a) {...}// 报错if(window.a) {...}// 不报错if(window['a']) {...}// 不报错

上面的后二种写法之所以不报错,是因为在浏览器环境,所有全局变量都是window对象的属性。window.a的含义就是读取window对象的a属性,如果该属性不存在,就返回undefined,并不会报错。

需要注意的是,后二种写法有漏洞,如果a属性是一个空字符串(或其他对应的布尔值为false的情况),则无法起到检查变量是否声明的作用。正确的做法是可以采用下面的写法。

// 写法一if(window.a ===undefined) {// ...}// 写法二if('a'inwindow) {// ...}

属性的赋值

点运算符和方括号运算符,不仅可以用来读取值,还可以用来赋值。

o.p ='abc';o['p'] ='abc';

上面代码分别使用点运算符和方括号运算符,对属性p赋值。

JavaScript允许属性的“后绑定”,也就是说,你可以在任意时刻新增属性,没必要在定义对象的时候,就定义好属性。

varo = {p:1};// 等价于varo = {};o.p =1;

查看所有属性

查看一个对象本身的所有属性,可以使用Object.keys方法。

varo = {key1:1,key2:2};Object.keys(o);// ['key1', 'key2']

属性的删除

删除一个属性,需要使用delete命令。

varo = {p:1};Object.keys(o)// ["p"]deleteo.p// trueo.p// undefinedObject.keys(o)// []

上面代码表示,一旦使用delete命令删除某个属性,再读取该属性就会返回undefined,而且Object.keys方法返回的该对象的所有属性中,也将不再包括该属性。

麻烦的是,如果删除一个不存在的属性,delete不报错,而且返回true。

varo = {};deleteo.p// true

上面代码表示,delete命令只能用来保证某个属性的值为undefined,而无法保证该属性是否真的存在。

只有一种情况,delete命令会返回false,那就是该属性存在,且不得删除。

varo =Object.defineProperty({},"p", {value:123,configurable:false});o.p// 123deleteo.p// false

上面代码之中,o对象的p属性是不能删除的,所以delete命令返回false(关于Object.defineProperty方法的介绍,请看《标准库》一章的Object对象章节)。

另外,需要注意的是,delete命令只能删除对象本身的属性,不能删除继承的属性(关于继承参见《面向对象编程》一节)。delete命令也不能删除var命令声明的变量,只能用来删除属性。

in运算符

in运算符用于检查对象是否包含某个属性(注意,检查的是键名,不是键值),如果包含就返回true,否则返回false。

varo = {p:1};'p'ino// true

在JavaScript语言中,所有全局变量都是顶层对象(浏览器的顶层对象就是window对象)的属性,因此可以用in运算符判断,一个全局变量是否存在。

// 假设变量x未定义// 写法一:报错if(x) {return1; }// 写法二:不正确if(window.x) {return1; }// 写法三:正确if('x'inwindow) {return1; }

上面三种写法之中,如果x不存在,第一种写法会报错;如果x的值对应布尔值false(比如x等于空字符串),第二种写法无法得到正确结果;只有第三种写法,才能正确判断变量x是否存在。

in运算符的一个问题是,它不能识别对象继承的属性。

varo =newObject();o.hasOwnProperty('toString')// false'toString'ino// true

上面代码中,toString方法不是对象o自身的属性,而是继承的属性,hasOwnProperty方法可以说明这一点。但是,in运算符不能识别,对继承的属性也返回true。

for...in循环

for...in循环用来遍历一个对象的全部属性。

varo = {a:1,b:2,c:3};for(variino) {console.log(o[i]);}// 1// 2// 3

下面是一个使用for...in循环,进行数组赋值的例子。

varprops = [], i =0;for(props[i++]in{x:1,y:2});props// ['x', 'y']

注意,for...in循环遍历的是对象所有可enumberable的属性,其中不仅包括定义在对象本身的属性,还包括对象继承的属性。

// name 是 Person 本身的属性functionPerson(name){this.name = name;}// describe是Person.prototype的属性Person.prototype.describe =function(){return'Name: '+this.name;};varperson =newPerson('Jane');// for...in循环会遍历实例自身的属性(name),// 以及继承的属性(describe)for(varkeyinperson) {console.log(key);}// name// describe

上面代码中,name是对象本身的属性,describe是对象继承的属性,for...in循环的遍历会包括这两者。

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

for(varkeyinperson) {if(person.hasOwnProperty(key)) {console.log(key);  }}// name

为了避免这一点,可以新建一个继承null的对象。由于null没有任何属性,所以新对象也就不会有继承的属性了。

with语句

with语句的格式如下:

with(object) {  statements;}

它的作用是操作同一个对象的多个属性时,提供一些书写的方便。

// 例一with(o) {  p1 =1;  p2 =2;}// 等同于o.p1 =1;o.p2 =2;// 例二with(document.links[0]){console.log(href);console.log(title);console.log(style);}// 等同于console.log(document.links[0].href);console.log(document.links[0].title);console.log(document.links[0].style);

注意,with区块内部的变量,必须是当前对象已经存在的属性,否则会创造一个当前作用域的全局变量。这是因为with区块没有改变作用域,它的内部依然是当前作用域。

varo = {};with(o) {  x ="abc";}o.x// undefinedx// "abc"

上面代码中,对象o没有属性x,所以with区块内部对x的操作,等于创造了一个全局变量x。正确的写法应该是,先定义对象o的属性x,然后在with区块内操作它。

varo = {};o.x =1;with(o) {  x =2;}o.x// 2

这是with语句的一个很大的弊病,就是绑定对象不明确。

with(o) {console.log(x);}

单纯从上面的代码块,根本无法判断x到底是全局变量,还是o对象的一个属性。这非常不利于代码的除错和模块化,编译器也无法对这段代码进行优化,只能留到运行时判断,这就拖慢了运行速度。因此,建议不要使用with语句,可以考虑用一个临时变量代替with。

with(o1.o2.o3) {console.log(p1 + p2);}// 可以写成vartemp = o1.o2.o3;console.log(temp.p1 + temp.p2);

with语句少数有用场合之一,就是替换模板变量。

varstr ='Hello <%= name %>!';

上面代码是一个模板字符串。假定有一个parser函数,可以将这个字符串解析成下面的样子。

parser(str)// '"Hello ", name, "!"'

那么,就可以利用with语句,进行模板变量替换。

varstr ='Hello <%= name %>!';varo = {name:'Alice'};functiontmpl(str, obj){  str ='var p = [];'+'with (obj) {p.push('+ parser(str) +')};'+'return p;'varr = (newFunction('obj', str))(obj);returnr.join('');}tmpl(str, o)// "Hello Alice!"

上面代码的核心逻辑是下面的部分。

varo = {name:'Alice'};varp = [];with(o) {  p.push('Hello ', name,'!');};p.join('')// "Hello Alice!"

上面代码中,with区块内部,模板变量name可以被对象o的属性替换,而p依然是全局变量。这就是很多模板引擎的实现原理。

数组

数组的定义

数组(array)是按次序排列的一组值。每个值的位置都有编号(从0开始),整个数组用方括号表示。

vararr = ['a','b','c'];

上面代码中的a、b、c就构成一个数组,两端的方括号是数组的标志。a是0号位置,b是1号位置,c是2号位置。

除了在定义时赋值,数组也可以先定义后赋值。

vararr = [];arr[0] ='a';arr[1] ='b';arr[2] ='c';

任何类型的数据,都可以放入数组。

vararr = [  {a:1},  [1,2,3],function(){returntrue;}];arr[0]// Object {a: 1}arr[1]// [1, 2, 3]arr[2]// function (){return true;}

上面数组arr的3个成员依次是对象、数组、函数。

如果数组的元素还是数组,就形成了多维数组。

vara = [[1,2], [3,4]];a[0][1]// 2a[1][1]// 4

数组的本质

本质上,数组属于一种特殊的对象。typeof运算符会返回数组的类型是object。

typeof[1,2,3]// "object"

上面代码表明,typeof运算符认为数组的类型就是对象。

数组的特殊性体现在,它的键名是按次序排列的一组整数(0,1,2...)。

vararr = ['a','b','c'];Object.keys(arr)// ["0", "1", "2"]

上面代码中,Object.keys方法返回数组的所有键名。可以看到数组的键名就是整数0、1、2。

由于数组成员的键名是固定的,因此数组不用为每个元素指定键名,而对象的每个成员都必须指定键名。

JavaScript语言规定,对象的键名一律为字符串,所以,数组的键名其实也是字符串。之所以可以用数值读取,是因为非字符串的键名会被转为字符串。

vararr = ['a','b','c'];arr['0']// 'a'arr[0]// 'a'

上面代码分别用数值和字符串作为键名,结果都能读取数组。原因是数值键名被自动转为了字符串。

需要注意的是,这一条在赋值时也成立。如果一个值可以被转换为整数,则以该值为键名,等于以对应的整数为键名。

vara = [];a['1000'] ='abc';a[1000]// 'abc'a[1.00] =6;a[1]// 6

上面代码表明,由于字符串“1000”和浮点数1.00都可以转换为整数,所以视同为整数键赋值。

上一节说过,对象有两种读取成员的方法:“点”结构(object.key)和方括号结构(object[key])。但是,对于数值的键名,不能使用点结构。

vararr = [1,2,3];arr.0// SyntaxError

上面代码中,arr.0的写法不合法,因为单独的数值不能作为标识符(identifier)。所以,数组成员只能用方括号arr[0]表示(方括号是运算符,可以接受数值)。

length属性

数组的length属性,返回数组的成员数量。

['a','b','c'].length// 3

JavaScript使用一个32位整数,保存数组的元素个数。这意味着,数组成员最多只有4294967295个(232-1)个,也就是说length属性的最大值就是4294967295。

数组的length属性与对象的length属性有区别,只要是数组,就一定有length属性,而对象不一定有。而且,数组的length属性是一个动态的值,等于键名中的最大整数加上1。

vararr = ['a','b'];arr.length// 2arr[2] ='c';arr.length// 3arr[9] ='d';arr.length// 10arr[1000] ='e';arr.length// 1001

上面代码表示,数组的数字键不需要连续,length属性的值总是比最大的那个整数键大1。另外,这也表明数组是一种动态的数据结构,可以随时增减数组的成员。

length属性是可写的。如果人为设置一个小于当前成员个数的值,该数组的成员会自动减少到length设置的值。

vararr = ['a','b','c'];arr.length// 3arr.length =2;arr// ["a", "b"]

上面代码表示,当数组的length属性设为2(即最大的整数键只能是1)那么整数键2(值为c)就已经不在数组中了,被自动删除了。

将数组清空的一个有效方法,就是将length属性设为0。

vararr = ['a','b','c'];arr.length =0;arr// []

如果人为设置length大于当前元素个数,则数组的成员数量会增加到这个值,新增的位置都是空位。

vara = ['a'];a.length =3;a[1]// undefined

上面代码表示,当length属性设为大于数组个数时,读取新增的位置都会返回undefined。

如果人为设置length为不合法的值,JavaScript会报错。

// 设置负值[].length =-1// RangeError: Invalid array length// 数组元素个数大于等于2的32次方[].length =Math.pow(2,32)// RangeError: Invalid array length// 设置字符串[].length ='abc'// RangeError: Invalid array length

值得注意的是,由于数组本质上是对象的一种,所以我们可以为数组添加属性,但是这不影响length属性的值。

vara = [];a['p'] ='abc';a.length// 0a[2.1] ='abc';a.length// 0

上面代码将数组的键分别设为字符串和小数,结果都不影响length属性。因为,length属性的值就是等于最大的数字键加1,而这个数组没有整数键,所以length属性保持为0。

类似数组的对象

在JavaScript中,有些对象被称为“类似数组的对象”(array-like object)。意思是,它们看上去很像数组,可以使用length属性,但是它们并不是数组,所以无法使用一些数组的方法。

下面就是一个类似数组的对象。

varobj = {0:'a',1:'b',2:'c',length:3};obj[0]// 'a'obj[2]// 'c'obj.length// 3

上面代码的变量obj是一个对象,但是看上去跟数组很像。所以只要有数字键和length属性,就是一个类似数组的对象。当然,变量obj无法使用数组特有的一些方法,比如pop和push方法。而且,length属性不是动态值,不会随着成员的变化而变化。

varobj = {length:0};obj[3] ='d';obj.length// 0

上面代码为对象obj添加了一个数字键,但是length属性没变。这就说明了obj不是数组。

典型的类似数组的对象是函数的arguments对象,以及大多数DOM元素集,还有字符串。

// arguments对象functionargs(){returnarguments}vararrayLike = args('a','b');arrayLike[0]// 'a'arrayLike.length// 2arrayLikeinstanceofArray// false// DOM元素集varelts =document.getElementsByTagName('h3');elts.length// 3eltsinstanceofArray// false// 字符串'abc'[1]// 'b''abc'.length// 3'abc'instanceofArray// false

数组的slice方法将类似数组的对象,变成真正的数组。

vararr =Array.prototype.slice.call(arrayLike);

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

// for循环functionlogArgs(){for(vari =0; i

由于字符串也是类似数组的对象,所以也可以用Array.prototype.forEach.call遍历。

Array.prototype.forEach.call('abc',function(chr){console.log(chr);});// a// b// c

in运算符

检查某个键名是否存在的运算符in,适用于对象,也适用于数组。

2in['a','b','c']// true'2'in['a','b','c']// true

上面代码表明,数组存在键名为2的键。由于键名都是字符串,所以数值2会自动转成字符串。

for...in循环和数组的遍历

使用for...in循环,可以遍历数组的所有元素。

vara = [1,2,3];for(variina) {console.log(a[i]);}// 1// 2// 3

需要注意的是,for...in会遍历数组所有的键,即使是非数字键。

vara = [1,2,3];a.foo =true;for(varkeyina) {console.log(key);}// 0// 1// 2// foo

上面代码在遍历数组时,也遍历到了非整数键foo。所以,使用for...in遍历数组的时候,一定要小心。

其他的数组遍历方法,就是使用length属性,结合for循环或者while循环。

// for循环vara = [1,2,3];for(vari =0; i < a.length; i++) {console.log(a[i]);}// while循环vari =0;while(i < a.length) {console.log(a[i]);  i++;}varl = a.length;while(l--) {console.log(a[l]);}

上面代码是三种遍历数组的写法。最后一种写法是逆向遍历,即从最后一个元素向第一个元素遍历。

数组的forEach方法,也可以用来遍历数组,详见《标准库》一章的Array对象部分。

varcolors = ['red','green','blue'];colors.forEach(function(color){console.log(color);});

数组的空位

当数组的某个位置是空元素,即两个逗号之间没有任何值,我们称该数组存在空位(hole)。

vara = [1, ,1];a.length// 3

上面代码表明,数组的空位不影响length属性。

需要注意的是,如果最后一个元素后面有逗号,并不会产生空位。也就是说,有没有这个逗号,结果都是一样的。

vara = [1,2,3,];a.length// 3a// [1, 2, 3]

上面代码中,数组最后一个成员后面有一个逗号,这不影响length属性的值,与没有这个逗号时效果一样。

数组的空位是可以读取的,返回undefined。

vara = [, , ,];a[1]// undefined

使用delete命令删除一个值,会形成空位。

vara = [1,2,3];deletea[1];a[1]// undefined

delete命令不影响length属性。

vara = [1,2,3];deletea[1];deletea[2];a.length// 3

上面代码用delete命令删除了两个键,对length属性没有影响。也就是说,length属性不过滤空位。所以,使用length属性进行数组遍历,一定要非常小心。

数组的某个位置是空位,与某个位置是undefined,是不一样的。如果是空位,使用数组的forEach方法、for...in结构、以及Object.keys方法进行遍历,空位都会被跳过。

vara = [, , ,];a.forEach(function(x, i){console.log(i +'. '+ x);})// 不产生任何输出for(variina) {console.log(i);}// 不产生任何输出Object.keys(a)// []

如果某个位置是undefined,遍历的时候就不会被跳过。

vara = [undefined,undefined,undefined];a.forEach(function(x, i){console.log(i +'. '+ x);});// 0. undefined// 1. undefined// 2. undefinedfor(variina) {console.log(i);}// 0// 1// 2Object.keys(a)// ['0', '1', '2']

这就是说,空位就是数组没有这个元素,所以不会被遍历到,而undefined则表示数组有这个元素,值是undefined,所以遍历不会跳过。

函数

概述

函数就是一段预先设置的代码块,可以反复调用,根据输入参数的不同,返回不同的值。

JavaScript有三种方法,可以声明一个函数。

函数的声明

(1)function命令

function命令声明的代码区块,就是一个函数。function命令后面是函数名,函数名后面是一对圆括号,里面是传入函数的参数。函数体放在大括号里面。

functionprint(s){console.log(s);}

上面的代码命名了一个print函数,以后使用print()这种形式,就可以调用相应的代码。这叫做函数的声明(Function Declaration)。

(2)函数表达式

除了用function命令声明函数,还可以采用变量赋值的写法。

varprint =function(s){console.log(s);};

这种写法将一个匿名函数赋值给变量。这时,这个匿名函数又称函数表达式(Function Expression),因为赋值语句的等号右侧只能放表达式。

采用函数表达式声明函数时,function命令后面不带有函数名。如果加上函数名,该函数名只在函数体内部有效,在函数体外部无效。

varprint =functionx(){console.log(typeofx);};x// ReferenceError: x is not definedprint()// function

上面代码在函数表达式中,加入了函数名x。这个x只在函数体内部可用,指代函数表达式本身,其他地方都不可用。这种写法的用处有两个,一是可以在函数体内部调用自身,二是方便除错(除错工具显示函数调用栈时,将显示函数名,而不再显示这里是一个匿名函数)。因此,下面的形式声明函数也非常常见。

varf =functionf(){};

需要注意的是,函数的表达式需要在语句的结尾加上分号,表示语句结束。而函数的声明在结尾的大括号后面不用加分号。总的来说,这两种声明函数的方式,差别很细微(参阅后文《变量提升》一节),这里可以近似认为是等价的。

(3)Function构造函数

还有第三种声明函数的方式:Function构造函数。

varadd =newFunction('x','y','return (x + y)');// 等同于functionadd(x, y){return(x + y);}

在上面代码中,Function构造函数接受三个参数,除了最后一个参数是add函数的“函数体”,其他参数都是add函数的参数。如果只有一个参数,该参数就是函数体。

varfoo =newFunction('return "hello world"');// 等同于functionfoo(){return"hello world";}

Function构造函数可以不使用new命令,返回结果完全一样。

总的来说,这种声明函数的方式非常不直观,几乎无人使用。

函数的重复声明

如果同一个函数被多次声明,后面的声明就会覆盖前面的声明。

functionf(){console.log(1);}f()// 2functionf(){console.log(2);}f()// 2

上面代码中,后一次的函数声明覆盖了前面一次。而且,由于函数名的提升(参见下文),前一次声明在任何时候都是无效的,这一点要特别注意。

圆括号运算符,return语句和递归

调用函数时,要使用圆括号运算符。圆括号之中,可以加入函数的参数。

functionadd(x, y){returnx + y;}add(1,1)// 2

上面代码中,函数名后面紧跟一对圆括号,就会调用这个函数。

函数体内部的return语句,表示返回。JavaScript引擎遇到return语句,就直接返回return后面的那个表达式的值,后面即使还有语句,也不会得到执行。也就是说,return语句所带的那个表达式,就是函数的返回值。return语句不是必需的,如果没有的话,该函数就不返回任何值,或者说返回undefined。

函数可以调用自身,这就是递归(recursion)。下面就是通过递归,计算斐波那契数列的代码。

functionfib(num){if(num >2) {returnfib(num -2) + fib(num -1);  }else{return1;  }}fib(6)// 8

上面代码中,fib函数内部又调用了fib,计算得到斐波那契数列的第6个元素是8。

第一等公民

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

这表明,函数与其他数据类型完全是平等的,所以又称函数为第一等公民。

functionadd(x, y){returnx + y;}// 将函数赋值给一个变量varoperator = add;// 将函数作为参数和返回值functiona(op){returnop;}a(add)(1,1)// 2

函数名的提升

JavaScript引擎将函数名视同变量名,所以采用function命令声明函数时,整个函数会像变量声明一样,被提升到代码头部。所以,下面的代码不会报错。

f();functionf(){}

表面上,上面代码好像在声明之前就调用了函数f。但是实际上,由于“变量提升”,函数f被提升到了代码头部,也就是在调用之前已经声明了。但是,如果采用赋值语句定义函数,JavaScript就会报错。

f();varf =function(){};// TypeError: undefined is not a function

上面的代码等同于下面的形式。

varf;f();f =function(){};

上面代码第二行,调用f的时候,f只是被声明了,还没有被赋值,等于undefined,所以会报错。因此,如果同时采用function命令和赋值语句声明同一个函数,最后总是采用赋值语句的定义。

varf =function(){console.log('1');}functionf(){console.log('2');}f()// 1

不能在条件语句中声明函数

根据ECMAScript的规范,不得在非函数的代码块中声明函数,最常见的情况就是if和try语句。

if(foo) {functionx(){}}try{functionx(){}}catch(e) {console.log(e);}

上面代码分别在if代码块和try代码块中声明了两个函数,按照语言规范,这是不合法的。但是,实际情况是各家浏览器往往并不报错,能够运行。

但是由于存在函数名的提升,所以在条件语句中声明函数,可能是无效的,这是非常容易出错的地方。

if(false){functionf(){}}f()// 不报错

上面代码的原始意图是不声明函数f,但是由于f的提升,导致if语句无效,所以上面的代码不会报错。要达到在条件语句中定义函数的目的,只有使用函数表达式。

if(false) {varf =function(){};}f()// undefined

函数的属性和方法

name属性

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

functionf1(){}f1.name// 'f1'varf2 =function(){};f2.name// ''varf3 =functionmyName(){};f3.name// 'myName'

上面代码中,函数的name属性总是返回紧跟在function关键字之后的那个函数名。对于f2来说,返回空字符串,匿名函数的name属性总是为空字符串;对于f3来说,返回函数表达式的名字(真正的函数名还是f3,myName这个名字只在函数体内部可用)。

length属性

length属性返回函数预期传入的参数个数,即函数定义之中的参数个数。

functionf(a, b){}f.length// 2

上面代码定义了空函数f,它的length属性就是定义时的参数个数。不管调用时输入了多少个参数,length属性始终等于2。

length属性提供了一种机制,判断定义时和调用时参数的差异,以便实现面向对象编程的”方法重载“(overload)。

toString()

函数的toString方法返回函数的源码。

functionf(){  a();  b();  c();}f.toString()// function f() {//  a();//  b();//  c();// }

函数内部的注释也可以返回。

functionf(){/*

  这是一个

  多行注释

*/}f.toString()// "function f(){/*//  这是一个//  多行注释// */}"

利用这一点,可以变相实现多行字符串。

varmultiline =function(fn){vararr = fn.toString().split('\n');returnarr.slice(1, arr.length -1).join('\n');};functionf(){/*

  这是一个

  多行注释

*/}multiline(f.toString())// " 这是一个//  多行注释"

函数作用域

定义

作用域(scope)指的是变量存在的范围。Javascript只有两种作用域:一种是全局作用域,变量在整个程序中一直存在,所有地方都可以读取;另一种是函数作用域,变量只在函数内部存在。

在函数外部声明的变量就是全局变量(global variable),它可以在函数内部读取。

varv =1;functionf(){console.log(v);}f()// 1

上面的代码表明,函数f内部可以读取全局变量v。

在函数内部定义的变量,外部无法读取,称为“局部变量”(local variable)。

functionf(){varv =1;}v// ReferenceError: v is not defined

上面代码中,变量v在函数内部定义,所以是一个局部变量,函数之外就无法读取。

函数内部定义的变量,会在该作用域内覆盖同名全局变量。

varv =1;functionf(){varv =2;console.log(v);}f()// 2v// 1

上面代码中,变量v同时在函数的外部和内部有定义。结果,在函数内部定义,局部变量v覆盖了全局变量v。

注意,对于var命令来说,局部变量只能在函数内部声明,在其他区块中声明,一律都是全局变量。

if(true) {varx =5;}console.log(x);// 5

上面代码中,变量x在条件判断区块之中声明,结果就是一个全局变量,可以在区块之外读取。

函数内部的变量提升

与全局作用域一样,函数作用域内部也会产生“变量提升”现象。var命令声明的变量,不管在什么位置,变量声明都会被提升到函数体的头部。

functionfoo(x){if(x >100) {vartmp = x -100;  }}

上面的代码等同于

functionfoo(x){vartmp;if(x >100) {    tmp = x -100;  };}

函数本身的作用域

函数本身也是一个值,也有自己的作用域。它的作用域绑定其声明时所在的作用域。

vara =1;varx =function(){console.log(a);};functionf(){vara =2;  x();}f()// 1

上面代码中,函数x是在函数f的外部声明的,所以它的作用域绑定外层,内部变量a不会到函数f体内取值,所以输出1,而不是2。

很容易犯错的一点是,如果函数A调用函数B,却没考虑到函数B不会引用函数A的内部变量。

varx =function(){console.log(a);};functiony(f){vara =2;  f();}y(x)// ReferenceError: a is not defined

上面代码将函数x作为参数,传入函数y。但是,函数x是在函数y体外声明的,作用域绑定外层,因此找不到函数y的内部变量a,导致报错。

参数

概述

函数运行的时候,有时需要提供外部数据,不同的外部数据会得到不同的结果,这种外部数据就叫参数。

functionsquare(x){returnx * x;}square(2)// 4square(3)// 9

上式的x就是square函数的参数。每次运行的时候,需要提供这个值,否则得不到结果。

参数的省略

函数参数不是必需的,Javascript允许省略参数。

functionf(a, b){returna;}f(1,2,3)// 1f(1)// 1f()// undefinedf.length// 2

上面代码的函数f定义了两个参数,但是运行时无论提供多少个参数(或者不提供参数),JavaScript都不会报错。

被省略的参数的值就变为undefined。需要注意的是,函数的length属性与实际传入的参数个数无关,只反映函数预期传入的参数个数。

但是,没有办法只省略靠前的参数,而保留靠后的参数。如果一定要省略靠前的参数,只有显式传入undefined。

functionf(a, b){returna;}f( ,1)// SyntaxError: Unexpected token ,(…)f(undefined,1)// undefined

上面代码中,如果省略第一个参数,就会报错。

默认值

通过下面的方法,可以为函数的参数设置默认值。

functionf(a){  a = a ||1;returna;}f('')// 1f(0)// 1

上面代码的||表示“或运算”,即如果a有值,则返回a,否则返回事先设定的默认值(上例为1)。

这种写法会对a进行一次布尔运算,只有为true时,才会返回a。可是,除了undefined以外,0、空字符、null等的布尔值也是false。也就是说,在上面的函数中,不能让a等于0或空字符串,否则在明明有参数的情况下,也会返回默认值。

为了避免这个问题,可以采用下面更精确的写法。

functionf(a){  (a !==undefined&& a !==null) ? a = a : a =1;returna;}f()// 1f('')// ""f(0)// 0

上面代码中,函数f的参数是空字符或0,都不会触发参数的默认值。

传递方式

函数参数如果是原始类型的值(数值、字符串、布尔值),传递方式是传值传递(passes by value)。这意味着,在函数体内修改参数值,不会影响到函数外部。

varp =2;functionf(p){  p =3;}f(p);p// 2

上面代码中,变量p是一个原始类型的值,传入函数f的方式是传值传递。因此,在函数内部,p的值是原始值的拷贝,无论怎么修改,都不会影响到原始值。

但是,如果函数参数是复合类型的值(数组、对象、其他函数),传递方式是传址传递(pass by reference)。也就是说,传入函数的原始值的地址,因此在函数内部修改参数,将会影响到原始值。

varobj = {p:1};functionf(o){  o.p =2;}f(obj);obj.p// 2

上面代码中,传入函数f的是参数对象obj的地址。因此,在函数内部修改obj的属性p,会影响到原始值。

注意,如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值。

varobj = [1,2,3];functionf(o){  o = [2,3,4];}f(obj);obj// [1, 2, 3]

上面代码中,在函数f内部,参数对象obj被整个替换成另一个值。这时不会影响到原始值。这是因为,形式参数(o)与实际参数obj存在一个赋值关系。

// 函数f内部o = obj;

上面代码中,对o的修改都会反映在obj身上。但是,如果对o赋予一个新的值,就等于切断了o与obj的联系,导致此后的修改都不会影响到obj了。

某些情况下,如果需要对某个原始类型的变量,获取传址传递的效果,可以将它写成全局对象的属性。

vara =1;functionf(p){window[p] =2;}f('a');a// 2

上面代码中,变量a本来是传值传递,但是写成window对象的属性,就达到了传址传递的效果。

同名参数

如果有同名的参数,则取最后出现的那个值。

functionf(a, a){console.log(a);}f(1,2)// 2

上面的函数f有两个参数,且参数名都是a。取值的时候,以后面的a为准。即使后面的a没有值或被省略,也是以其为准。

functionf(a, a){console.log(a);}f(1)// undefined

调用函数f的时候,没有提供第二个参数,a的取值就变成了undefined。这时,如果要获得第一个a的值,可以使用arguments对象。

functionf(a, a){console.log(arguments[0]);}f(1)// 1

arguments对象

(1)定义

由于JavaScript允许函数有不定数目的参数,所以我们需要一种机制,可以在函数体内部读取所有参数。这就是arguments对象的由来。

arguments对象包含了函数运行时的所有参数,arguments[0]就是第一个参数,arguments[1]就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用。

varf =function(one){console.log(arguments[0]);console.log(arguments[1]);console.log(arguments[2]);}f(1,2,3)// 1// 2// 3

arguments对象除了可以读取参数,还可以为参数赋值(严格模式不允许这种用法)。

varf =function(a, b){arguments[0] =3;arguments[1] =2;returna + b;}f(1,1)// 5

可以通过arguments对象的length属性,判断函数调用时到底带几个参数。

functionf(){returnarguments.length;}f(1,2,3)// 3f(1)// 1f()// 0

(2)与数组的关系

需要注意的是,虽然arguments很像数组,但它是一个对象。数组专有的方法(比如slice和forEach),不能在arguments对象上直接使用。

但是,可以通过apply方法,把arguments作为参数传进去,这样就可以让arguments使用数组方法了。

// 用于apply方法myfunction.apply(obj,arguments).// 使用与另一个数组合并Array.prototype.concat.apply([1,2,3],arguments)

要让arguments对象使用数组方法,真正的解决方法是将arguments转为真正的数组。下面是两种常用的转换方法:slice方法和逐一填入新数组。

varargs =Array.prototype.slice.call(arguments);// orvarargs = [];for(vari =0; i

(3)callee属性

arguments对象带有一个callee属性,返回它所对应的原函数。

varf =function(one){console.log(arguments.callee === f);}f()// true

可以通过arguments.callee,达到调用函数自身的目的。

函数的其他知识点

闭包

闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。

要理解闭包,首先必须理解变量作用域。前面提到,JavaScript有两种作用域:全局作用域和函数作用域。函数内部可以直接读取全局变量。

varn =999;functionf1(){console.log(n);}f1()// 999

上面代码中,函数f1可以读取全局变量n。

但是,在函数外部无法读取函数内部声明的变量。

functionf1(){varn =999;}console.log(n)// Uncaught ReferenceError: n is not defined(

上面代码中,函数f1内部声明的变量n,函数外是无法读取的。

如果出于种种原因,需要得到函数内的局部变量。正常情况下,这是办不到的,只有通过变通方法才能实现。那就是在函数的内部,再定义一个函数。

functionf1(){varn =999;functionf2(){console.log(n);// 999}}

上面代码中,函数f2就在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。这就是JavaScript语言特有的"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

既然f2可以读取f1的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗!

functionf1(){varn =999;functionf2(){console.log(n);  }returnf2;}varresult = f1();result();// 999

上面代码中,函数f1的返回值就是函数f2,由于f2可以读取f1的内部变量,所以就可以在外部获得f1的内部变量了。

闭包就是函数f2,即能够读取其他函数内部变量的函数。由于在JavaScript语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。闭包最大的特点,就是它可以“记住”诞生的环境,比如f2记住了它诞生的环境f1,所以从f2可以得到f1的内部变量。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

闭包的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。请看下面的例子,闭包使得内部变量记住上一次调用时的运算结果。

functioncreateIncrementor(start){returnfunction(){returnstart++;  };}varinc = createIncrementor(5);inc()// 5inc()// 6inc()// 7

上面代码中,start是函数createIncrementor的内部变量。通过闭包,start的状态被保留了,每一次调用都是在上一次调用的基础上进行计算。从中可以看到,闭包inc使得函数createIncrementor的内部环境,一直存在。所以,闭包可以看作是函数内部作用域的一个接口。

为什么会这样呢?原因就在于inc始终在内存中,而inc的存在依赖于createIncrementor,因此也始终在内存中,不会在调用结束后,被垃圾回收机制回收。

闭包的另一个用处,是封装对象的私有属性和私有方法。

functionPerson(name){var_age;functionsetAge(n){    _age = n;  }functiongetAge(){return_age;  }return{name: name,getAge: getAge,setAge: setAge  };}varp1 = person('张三');p1.setAge(25);p1.getAge()// 25

上面代码中,函数Person的内部变量_age,通过闭包getAge和setAge,变成了返回对象p1的私有变量。

注意,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。

立即调用的函数表达式(IIFE)

在Javascript中,一对圆括号()是一种运算符,跟在函数名之后,表示调用该函数。比如,print()就表示调用print函数。

有时,我们需要在定义函数之后,立即调用该函数。这时,你不能在函数的定义之后加上圆括号,这会产生语法错误。

function(){/* code */}();// SyntaxError: Unexpected token (

产生这个错误的原因是,function这个关键字即可以当作语句,也可以当作表达式。

// 语句functionf(){}// 表达式varf =functionf(){}

为了避免解析上的歧义,JavaScript引擎规定,如果function关键字出现在行首,一律解释成语句。因此,JavaScript引擎看到行首是function关键字之后,认为这一段都是函数的定义,不应该以圆括号结尾,所以就报错了。

解决方法就是不要让function出现在行首,让引擎将其理解成一个表达式。最简单的处理,就是将其放在一个圆括号里面。

(function(){/* code */}());// 或者(function(){/* code */})();

上面两种写法都是以圆括号开头,引擎就会认为后面跟的是一个表示式,而不是函数定义语句,所以就避免了错误。这就叫做“立即调用的函数表达式”(Immediately-Invoked Function Expression),简称IIFE。

注意,上面两种写法最后的分号都是必须的。如果省略分号,遇到连着两个IIFE,可能就会报错。

// 报错(function(){/* code */}())(function(){/* code */}())

上面代码的两行之间没有分号,JavaScript会将它们连在一起解释,将第二行解释为第一行的参数。

推而广之,任何让解释器以表达式来处理函数定义的方法,都能产生同样的效果,比如下面三种写法。

vari =function(){return10; }();true&&function(){/* code */}();0,function(){/* code */}();

甚至像下面这样写,也是可以的。

!function(){/* code */}();~function(){/* code */}();-function(){/* code */}();+function(){/* code */}();

new关键字也能达到这个效果。

newfunction(){/* code */}newfunction(){/* code */}()// 只有传递参数时,才需要最后那个圆括号

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

// 写法一vartmp = newData;processData(tmp);storeData(tmp);// 写法二(function(){vartmp = newData;  processData(tmp);  storeData(tmp);}());

上面代码中,写法二比写法一更好,因为完全避免了污染全局变量。

eval命令

eval命令的作用是,将字符串当作语句执行。

eval('var a = 1;');a// 1

上面代码将字符串当作语句运行,生成了变量a。

放在eval中的字符串,应该有独自存在的意义,不能用来与eval以外的命令配合使用。举例来说,下面的代码将会报错。

eval('return;');

由于eval没有自己的作用域,都在当前作用域内执行,因此可能会修改其他外部变量的值,造成安全问题。

vara =1;eval('a = 2');a// 2

上面代码中,eval命令修改了外部变量a的值。由于这个原因,所以eval有安全风险,如果无法做到作用域隔离,最好不要使用。此外,eval的命令字符串不会得到JavaScript引擎的优化,运行速度较慢,也是另一个不应该使用它的理由。通常情况下,eval最常见的场合是解析JSON数据字符串,正确的做法是这时应该使用浏览器提供的JSON.parse方法。

ECMAScript 5将eval的使用分成两种情况,像上面这样的调用,就叫做“直接使用”,这种情况下eval的作用域就是当前作用域(即全局作用域或函数作用域)。另一种情况是,eval不是直接调用,而是“间接调用”,此时eval的作用域总是全局作用域。

vara =1;functionf(){vara =2;vare =eval;  e('console.log(a)');}f()// 1

上面代码中,eval是间接调用,所以即使它是在函数中,它的作用域还是全局作用域,因此输出的a为全局变量。

eval的间接调用的形式五花八门,只要不是直接调用,几乎都属于间接调用。

eval.call(null,'...')window.eval('...')(1,eval)('...')(eval,eval)('...')(1?eval:0)('...')(__ =eval)('...')vare =eval; e('...')(function(e){ e('...') })(eval)(function(e){returne })(eval)('...')(function(){arguments[0]('...') })(eval)this.eval('...')this['eval']('...')[eval][0]('...')eval.call(this,'...')eval('eval')('...')

上面这些形式都是eval的间接调用,因此它们的作用域都是全局作用域。

与eval作用类似的还有Function构造函数。利用它生成一个函数,然后调用该函数,也能将字符串当作命令执行。

varjsonp ='foo({"id":42})';varf =newFunction("foo", jsonp );// 相当于定义了如下函数// function f(foo) {//  foo({"id":42});// }f(function(json){console.log( json.id );// 42})

上面代码中,jsonp是一个字符串,Function构造函数将这个字符串,变成了函数体。调用该函数的时候,jsonp就会执行。这种写法的实质是将代码放到函数作用域执行,避免对全局作用域造成影响。

运算符

运算符是处理数据的基本方法,用来从现有数据得到新的数据。JavaScript与其他编程语言一样,提供了多种运算符。本节逐一介绍这些运算符。

加法运算符

加法运算符(+)是最常见的运算符之一,但是使用规则却相对复杂。因为在JavaScript语言里面,这个运算符可以完成两种运算,既可以处理算术的加法,也可以用作字符串连接,它们都写成+。

// 加法1+1// 2true+true// 21+true// 2// 字符串连接'1'+'1'// "11"'1.1'+'1.1'// "1.11.1"

它的算法步骤如下。

如果运算子是对象,先自动转成原始类型的值(即先执行该对象的valueOf方法,如果结果还不是原始类型的值,再执行toString方法;如果对象是Date实例,则先执行toString方法)。

两个运算子都是原始类型的值以后,只要有一个运算子是字符串,则两个运算子都转为字符串,执行字符串连接运算。

否则,两个运算子都转为数值,执行加法运算。

下面是一些例子。

'1'+ {foo:'bar'}// "1[object Object]"'1'+1// "11"'1'+true// "1true"'1'+ [1]// "11"

上面代码中,由于运算符左边是一个字符串,导致右边的运算子都会先转为字符串,然后执行字符串连接运算。

这种由于参数不同,而改变自身行为的现象,叫做“重载”(overload)。由于加法运算符是运行时决定到底执行那种运算,使用的时候必须很小心。

'3'+4+5// "345"3+4+'5'// "75"

上面代码中,运算结果由于字符串的位置不同而不同。

下面的写法,可以用来将一个值转为字符串。

x +''

上面代码中,一个值加上空字符串,会使得该值转为字符串形式。

加法运算符会将其他类型的值,自动转为字符串,然后再执行连接运算。

[1,2] + [3]// "1,23"// 等同于String([1,2]) +String([3])// '1,2' + '3'

上面代码中,两个数组相加,会先转成字符串,然后再连接。这种数据类型的自动转换,参见《数据类型转换》一节。

加法运算符一定有左右两个运算子,如果只有右边一个运算子,就是另一个运算符,叫做“数值运算符”。

+ -3// 等同于 +(-3)+1+2// 等同于 +(1 + 2)+'1'// 1

上面代码中,数值运算符用于返回右边运算子的数值形式,详细解释见下文。

你可能会问,如果只有左边一个运算子,会出现什么情况?答案是会报错。

1+// SyntaxError: Unexpected end of input

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

1-'2'// -11*'2'// 21/'2'// 0.5

上面代码中,减法、除法和乘法运算符,都是将字符串自动转为数值,然后再运算。

由于加法运算符与其他算术运算符的这种差异,会导致一些意想不到的结果,计算时要小心。

varnow =newDate();typeof(now +1)// "string"typeof(now -1)// "number"

上面代码中,now是一个Date对象的实例。加法运算时,得到的是一个字符串;减法运算时,得到却是一个数值。

算术运算符

JavaScript提供9个算术运算符,用来完成基本的算术运算。

加法运算符(Addition):x + y

减法运算符(Subtraction):x - y

乘法运算符(Multiplication):x * y

除法运算符(Division):x / y

余数运算符(Remainder):x % y

自增运算符(Increment):++x或者x++

自减运算符(Decrement):--x或者x--

数值运算符(Convert to number):+x

负数值运算符(Negate):-x

减法、乘法、除法运算法比较单纯,就是执行相应的数学运算。下面介绍其他几个算术运算符。

余数运算符

余数运算符(%)返回前一个运算子被后一个运算子除,所得的余数。

12%5// 2

需要注意的是,运算结果的正负号由第一个运算子的正负号决定。

-1%2// -11%-2// 1

为了得到正确的负数的余数值,需要先使用绝对值函数。

// 错误的写法functionisOdd(n){returnn %2===1;}isOdd(-5)// falseisOdd(-4)// false// 正确的写法functionisOdd(n){returnMath.abs(n %2) ===1;}isOdd(-5)// trueisOdd(-4)// false

余数运算符还可以用于浮点数的运算。但是,由于浮点数不是精确的值,无法得到完全准确的结果。

6.5%2.1// 0.19999999999999973

自增和自减运算符

自增和自减运算符,是一元运算符,只需要一个运算子。它们的作用是将运算子首先转为数值,然后加上1或者减去1。它们会修改原始变量。

varx =1;++x// 2x// 2--x// 1x// 1

上面代码的变量x自增后,返回2,再进行自减,返回1。这两种情况都会使得,原始变量x的值发生改变。

自增和自减运算符有一个需要注意的地方,就是放在变量之后,会先返回变量操作前的值,再进行自增/自减操作;放在变量之前,会先进行自增/自减操作,再返回变量操作后的值。

varx =1;vary =1;x++// 1++y// 2

上面代码中,x是先返回当前值,然后自增,所以得到1;y是先自增,然后返回新的值,所以得到2。

数值运算符,负数值运算符

数值运算符(+)同样使用加号,但是加法运算符是二元运算符(需要两个操作数),它是一元运算符(只需要一个操作数)。

数值运算符的作用在于可以将任何值转为数值(与Number函数的作用相同)。

+true// 1+[]// 0+{}// NaN

上面代码表示,非数值类型的值经过数值运算符以后,都变成了数值(最后一行NaN也是数值)。具体的类型转换规则,参见《数据类型转换》一节。

负数值运算符(-),也同样具有将一个值转为数值的功能,只不过得到的值正负相反。连用两个负数值运算符,等同于数值运算符。

varx =1;-x// -1-(-x)// 1

上面代码最后一行的圆括号不可少,否则会变成递减运算符。

数值运算符号和负数值运算符,都会返回一个新的值,而不会改变原始变量的值。

赋值运算符

赋值运算符(Assignment Operators)用于给变量赋值。

最常见的赋值运算符,当然就是等号(=),表达式x = y表示将y的值赋给x。

除此之外,JavaScript还提供其他11个复合的赋值运算符。

x += y// 等同于 x = x + yx -= y// 等同于 x = x - yx *= y// 等同于 x = x * yx /= y// 等同于 x = x / yx %= y// 等同于 x = x % yx >>= y// 等同于 x = x >> yx <<= y// 等同于 x = x << yx >>>= y// 等同于 x = x >>> yx &= y// 等同于 x = x & yx |= y// 等同于 x = x | yx ^= y// 等同于 x = x ^ y

这些复合的赋值运算符,都是先进行指定运算,然后将得到值返回给左边的变量。

比较运算符

比较运算符用于比较两个值,然后返回一个布尔值,表示是否满足比较条件。

2>1// true

上面代码计算2是否大于1,返回true。

JavaScript一共提供了8个比较运算符。

==相等

===严格相等

!=不相等

!==严格不相等

<小于

<=小于或等于

>大于

>=大于或等于

比较运算符的算法

比较运算符可以比较各种类型的值,不仅仅是数值。

它的算法步骤如下。

如果运算子是对象,先自动转成原始类型的值(即先执行该对象的valueOf方法,如果结果还不是原始类型的值,再执行toString方法)。

如果两个运算子都是字符串,则按照字典顺序比较(实际上是比较Unicode码点)。

否则,将两个运算子都转成数值,再进行比较。

下面是一个例子。

[2] > [1]// true// 等同于 '[2]' > '[1]'[2] > [11]// true// 等同于 '[2]' > '[11]'

上面代码是两个数组的比较,它们会先转成原始类型的值(这个例子是字符串),再进行比较。

5>'4'// truetrue>false// true2>true// true

上面代码中,字符串和布尔值都会先转成数值,再进行比较。

字符串的比较

字符串按照字典顺序进行比较。

'cat'>'dog'// false'cat'>'catalog'// false

JavaScript引擎内部首先比较首字符的Unicode编号,如果相等,再比较第二个字符的Unicode编号,以此类推。

'cat'>'Cat'// true'

上面代码中,小写的c的Unicode编号(99)大于大写的C的Unicode编号(67),所以返回true。

由于,JavaScript的所有字符都有Unicode编号,因此汉字也可以比较。

'大'>'小'// false

上面代码中,“大”的Unicode编号是22823,“小”是23567,因此返回true。

严格相等运算符

JavaScript提供两个相等运算符:==和===。

简单说,它们的区别是相等运算符(==)比较两个值是否相等,严格相等运算符(===)比较它们是否为“同一个值”。如果两个值不是同一类型,严格相等运算符(===)直接返回false,而相等运算符(==)会将它们转化成同一个类型,再用严格相等运算符进行比较。

严格相等运算符的运算规则如下。

(1)不同类型的值

如果两个值的类型不同,直接返回false。

1==="1"// falsetrue==="true"// false

上面代码比较数值的1与字符串的“1”、布尔值的true与字符串“true”,因为类型不同,结果都是false。

(2)同一类的原始类型值

同一类型的原始类型的值(数值、字符串、布尔值)比较时,值相同就返回true,值不同就返回false。

1===0x1// true

上面代码比较十进制的1与十六进制的1,因为类型和值都相同,返回true。

需要注意的是,NaN与任何值都不相等(包括自身)。另外,正0等于负0。

NaN===NaN// false+0===-0// true

(3)同一类的复合类型值

两个复合类型(对象、数组、函数)的数据比较时,不是比较它们的值是否相等,而是比较它们是否指向同一个对象。

({} === {})// false[] === []// false(function(){} ===function(){})// false

上面代码分别比较两个空对象、两个空数组、两个空函数,结果都是不相等。原因是对于复合类型的值,严格相等运算比较的是,它们是否引用同一个内存地址,而运算符两边的空对象、空数组、空函数的值,都存放在不同的内存地址,结果当然是false。另外,空对象的比较和空函数的比较,都放在括号内,是为了避免JavaScript引擎把行首的空对象解释成代码块,把行首的空函数解释成函数的定义。

如果两个变量引用同一个对象,则它们相等。

varv1 = {};varv2 = v1;v1 === v2// true

(4)undefined和null

undefined和null与自身严格相等。

undefined===undefined// truenull===null// true

由于变量声明后默认值是undefined,因此两个只声明未赋值的变量是相等的。

varv1;varv2;v1 === v2// true

(5)严格不相等运算符

严格相等运算符有一个对应的“严格不相等运算符”(!==),两者的运算结果正好相反。

1!=='1'// true

相等运算符

相等运算符比较相同类型的数据时,与严格相等运算符完全一样。

比较不同类型的数据时,相等运算符会先将数据进行类型转换,然后再用严格相等运算符比较。类型转换规则如下。

(1)原始类型的值

原始类型的数据会转换成数值类型再进行比较。

1==true// true// 等同于 1 === 10==false// true// 等同于 0 === 02==true// false// 等同于 2 === 12==false// false// 等同于 2 === 0'true'==true// false// 等同于 Number('true') === Number(true)// 等同于 NaN === 1''==0// true// 等同于 Number('') === 0// 等同于 0 === 0''==false// true// 等同于 Number('') === Number(false)// 等同于 0 === 0'1'==true// true// 等同于 Number('1') === Number(true)// 等同于 1 === 1'\n  123  \t'==123// true// 因为字符串转为数字时,省略前置和后置的空格

上面代码将字符串和布尔值都转为数值,然后再进行比较。字符串与布尔值的类型转换规则,参见《数据类型转换》一节。

(2)对象与原始类型值比较

对象(这里指广义的对象,包括数值和函数)与原始类型的值比较时,对象转化成原始类型的值,再进行比较。

[1] ==1// true// 等同于 Number([1]) == 1[1] =='1'// true// 等同于 String([1]) == Number('1')[1] ==true// true// 等同于 Boolean([1]) == true

上面代码中,数组[1]分别与数值、字符串和布尔值进行比较,会先转成该类型,再进行比较。比如,与数值1比较时,数组[1]会被自动转换成数值1,因此得到true。对象的类型转换规则,参见《数据类型转换》一节。

(3)undefined和null

undefined和null与其他类型的值比较时,结果都为false,它们互相比较时结果为true。

false==null// falsefalse==undefined// false0==null// false0==undefined// falseundefined==null// true

(4)相等运算符的缺点

相等运算符隐藏的类型转换,会带来一些违反直觉的结果。

''=='0'// false0==''// true0=='0'// true2==true// false2==false// falsefalse=='false'// falsefalse=='0'// truefalse==undefined// falsefalse==null// falsenull==undefined// true' \t\r\n '==0// true

上面这些表达式都很容易出错,因此不要使用相等运算符(==),最好只使用严格相等运算符(===)。

(5)不相等运算符

相等运算符有一个对应的“不相等运算符”(!=),两者的运算结果正好相反。

1!='1'// false

布尔运算符

布尔运算符用于将表达式转为布尔值,一共包含四个运算符。

取反运算符:!

且运算符:&&

或运算符:||

三元运算符:?:

取反运算符(!)

取反运算符形式上是一个感叹号,用于将布尔值变为相反值,即true变成false,false变成true。

!true// false!false// true

对于非布尔值的数据,取反运算符会自动将其转为布尔值。规则是,以下六个值取反后为true,其他值取反后都为false。

undefined

null

false

0(包括+0和-0)

NaN

空字符串('')

这意味着,取反运算符有转换数据类型的作用。

!undefined// true!null// true!0// true!NaN// true!""// true!54// false!'hello'// false![]// false!{}// false

上面代码中,不管什么类型的值,经过取反运算后,都变成了布尔值。

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

!!x// 等同于Boolean(x)

上面代码中,不管x是什么类型的值,经过两次取反运算后,变成了与Boolean函数结果相同的布尔值。所以,两次取反就是将一个值转为布尔值的简便写法。

取反运算符的这种将任意数据自动转为布尔值的功能,对下面三种布尔运算符(且运算符、或运算符、三元条件运算符)都成立。

且运算符(&&)

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

't'&&''// ""'t'&&'f'// "f"'t'&& (1+2)// 3''&&'f'// ""''&&''// ""varx =1;(1-1) && ( x +=1)// 0x// 1

上面代码的最后一部分表示,由于且运算符的第一个运算子的布尔值为false,则直接返回它的值0,而不再对第二个运算子求值,所以变量x的值没变。

这种跳过第二个运算子的机制,被称为“短路”。有些程序员喜欢用它取代if结构,比如下面是一段if结构的代码,就可以用且运算符改写。

if(i !==0) {  doSomething();}// 等价于i && doSomething();

上面代码的两种写法是等价的,但是后一种不容易看出目的,也不容易除错,建议谨慎使用。

且运算符可以多个连用,这时返回第一个布尔值为false的表达式的值。

true&&'foo'&&''&&4&&'foo'&&true// ''

上面代码中第一个布尔值为false的表达式为第三个表达式,所以得到一个空字符串。

或运算符(||)

或运算符(||)的运算规则是:如果第一个运算子的布尔值为true,则返回第一个运算子的值,且不再对第二个运算子求值;如果第一个运算子的布尔值为false,则返回第二个运算子的值。

't'||''// "t"'t'||'f'// "t"''||'f'// "f"''||''// ""

短路规则对这个运算符也适用。

或运算符可以多个连用,这时返回第一个布尔值为true的表达式的值。

false||0||''||4||'foo'||true// 4

上面代码中第一个布尔值为true的表达式是第四个表达式,所以得到数值4。

或运算符常用于为一个变量设置默认值。

functionsaveText(text){  text = text ||'';// ...}// 或者写成saveText(this.text ||'')

上面代码表示,如果函数调用时,没有提供参数,则该参数默认设置为空字符串。

三元条件运算符(?:)

三元条件运算符用问号(?)和冒号(:),分隔三个表达式。如果第一个表达式的布尔值为true,则返回第二个表达式的值,否则返回第三个表达式的值。

't'?'hello':'world'// "hello"0?'hello':'world'// "world"

上面代码的t和0的布尔值分别为true和false,所以分别返回第二个和第三个表达式的值。

通常来说,三元条件表达式与if...else语句具有同样表达效果,前者可以表达的,后者也能表达。但是两者具有一个重大差别,if...else是语句,没有返回值;三元条件表达式是表达式,具有返回值。所以,在需要返回值的场合,只能使用三元条件表达式,而不能使用if..else。

console.log(true?'T':'F');

上面代码中,console.log方法的参数必须是一个表达式,这时就只能使用三元条件表达式。如果要用if...else语句,就必须改变整个代码写法了。

位运算符

简介

位运算符用于直接对二进制位进行计算,一共有7个。

或运算(or):符号为|;,表示两个二进制位中只要有一个为1,则结果为1,否则为0。

与运算(and):符号为&,表示如果两个二进制位都为1,则结果为1,否则为0。

否运算(not):符号为~,表示将一个二进制位变成相反值。

异或运算(xor):符号为ˆ,表示如果两个二进制位中有且仅有一个为1时,结果为1,否则为0。

左移运算(left shift):符号为<<,详见下文解释。

右移运算(right shift):符号为>>,详见下文解释。

带符号位的右移运算(zero filled right shift):符号为>>>,详见下文解释。

这些位运算符直接处理每一个比特位,所以是非常底层的运算,好处是速度极快,缺点是很不直观,许多场合不能使用它们,否则会带来过度的复杂性。

有一点需要特别注意,位运算符只对整数起作用,如果一个运算子不是整数,会自动转为整数后再运行。另外,虽然在JavaScript内部,数值都是以64位浮点数的形式储存,但是做位运算的时候,是以32位带符号的整数进行运算的,并且返回值也是一个32位带符号的整数。

i = i |0;

上面这行代码的意思,就是将i(不管是整数或小数)转为32位整数。

利用这个特性,可以写出一个函数,将任意数值转为32位整数。

functionToInt32(x){returnx |0;}ToInt32(1.001)// 1ToInt32(1.999)// 1ToInt32(1)// 1ToInt32(-1)// -1ToInt32(Math.pow(2,32) +1)// 1ToInt32(Math.pow(2,32) -1)// -1

上面代码中,最后两行得到1和-1,是因为一个整数大于32位的数位都会被舍去。

“或运算”与“与运算”

这两种运算比较容易理解,就是逐位比较两个运算子。“或运算”的规则是,两个二进制位之中只要有一个为1,就返回1,否则返回0。“与运算”的规则是,两个二进制位之中只要有一个位为0,就返回0,否则返回1。

0|3// 30&3// 0

上面两个表达式,0和3的二进制形式分别是00和11,所以进行“或运算”会得到11(即3),进行”与运算“会得到00(即0)。

位运算只对整数有效,遇到小数时,会将小数部分舍去,只保留整数部分。所以,将一个小数与0进行或运算,等同于对该数去除小数部分,即取整数位。

2.9|0// 2-2.9|0// -2

需要注意的是,这种取整方法不适用超过32位整数最大值2147483647的数。

2147483649.4|0;// -2147483647

否运算

“否运算”将每个二进制位都变为相反值(0变为1,1变为0)。它的返回结果有时比较难理解,因为涉及到计算机内部的数值表示机制。

~3// -4

上面表达式对3进行“否运算”,得到-4。之所以会有这样的结果,是因为位运算时,JavaScirpt内部将所有的运算子都转为32位的二进制整数再进行运算。3在JavaScript内部是00000000000000000000000000000011,否运算以后得到11111111111111111111111111111100,由于第一位是1,所以这个数是一个负数。JavaScript内部采用2的补码形式表示负数,即需要将这个数减去1,再取一次反,然后加上负号,才能得到这个负数对应的10进制值。这个数减去1等于11111111111111111111111111111011,再取一次反得到00000000000000000000000000000100,再加上负号就是-4。考虑到这样的过程比较麻烦,可以简单记忆成,一个数与自身的取反值相加,等于-1。

~-3// 2

上面表达式可以这样算,-3的取反值等于-1减去-3,结果为2。

对一个整数连续两次“否运算”,得到它自身。

~~3// 3

所有的位运算都只对整数有效。否运算遇到小数时,也会将小数部分舍去,只保留整数部分。所以,对一个小数连续进行两次否运算,能达到取整效果。

~~2.9// 2~~47.11// 47~~1.9999// 1~~3// 3

使用否运算取整,是所有取整方法中最快的一种。

对字符串进行否运算,JavaScript引擎会先调用Number函数,将字符串转为数值。

// 以下例子相当于~Number('011')~'011'// -12~'42 cats'// -1~'0xcafebabe'// 889275713~'deadbeef'// -1// 以下例子相当于~~Number('011')~~'011';// 11~~'42 cats';// 0~~'0xcafebabe';// -889275714~~'deadbeef';// 0

Number函数将字符串转为数值的规则,参见《数据的类型转换》一节。否运算对特殊数值的处理是:超出32位的整数将会被截去超出的位数,NaN和Infinity转为0。

对于其他类型的参数,否运算也是先用Number转为数值,然后再进行处理。

~~[]// 0~~NaN// 0~~null// 0

异或运算

“异或运算”在两个二进制位不同时返回1,相同时返回0。

0^3// 3

上面表达式中,0的二进制形式是00,3的二进制形式是11,它们每一个二进制位都不同,所以得到11(即3)。

“异或运算”有一个特殊运用,连续对两个数a和b进行三次异或运算,aˆ=b, bˆ=a, aˆ=b,可以互换它们的值(详见维基百科)。这意味着,使用“异或运算”可以在不引入临时变量的前提下,互换两个变量的值。

vara =10;varb =99;a ^= b, b ^= a, a ^= b;a// 99b// 10

这是互换两个变量的值的最快方法。

异或运算也可以用来取整。

12.9^0// 12

左移运算符(<<)

左移运算符表示将一个数的二进制值,向前移动指定的位数,尾部补0,即乘以2的指定次方。

// 4 的二进制形式为100,// 左移一位为1000(即十进制的8)// 相当于乘以2的1次方4<<1// 8-4<<1// -8

上面代码中,-4左移一位得到-8,是因为-4的二进制形式是11111111111111111111111111111100,左移一位后得到11111111111111111111111111111000,该数转为十进制(减去1后取反,再加上负号)即为-8。

如果左移0位,就相当于将该数值转为32位整数,等同于取整,对于正数和负数都有效。

13.5<<0// 13-13.5<<0// -13

左移运算符用于二进制数值非常方便。

varcolor = {r:186,g:218,b:85};// RGB to HEXvarrgb2hex =function(r, g, b){return'#'+ ((1<<24) + (r <<16) + (g <<8) + b)    .toString(16)    .substr(1);}rgb2hex(color.r,color.g,color.b)// "#bada55"

上面代码使用左移运算符,将颜色的RGB值转为HEX值。

右移运算符(>>)

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

4>>1// 2/*

// 因为4的二进制形式为00000000000000000000000000000100,

// 右移一位得到00000000000000000000000000000010,

// 即为十进制的2

*/-4>>1// -2/*

// 因为-4的二进制形式为11111111111111111111111111111100,

// 右移一位,头部补1,得到11111111111111111111111111111110,

// 即为十进制的-2

*/

右移运算可以模拟2的整除运算。

5>>1// 相当于 5 / 2 = 221>>2// 相当于 21 / 4 = 521>>3// 相当于 21 / 8 = 221>>4// 相当于 21 / 16 = 1

带符号位的右移运算符(>>>)

该运算符表示将一个数的二进制形式向右移动,不管正数或负数,头部一律补0。所以,该运算总是得到正值,这就是它的名称“带符号位的右移”的涵义。对于正数,该运算的结果与右移运算符(>>)完全一致,区别主要在于负数。

4>>>1// 2-4>>>1// 2147483646/*

// 因为-4的二进制形式为11111111111111111111111111111100,

// 带符号位的右移一位,得到01111111111111111111111111111110,

// 即为十进制的2147483646。

*/

这个运算实际上将一个值转为32位无符号整数。

查看一个负整数在计算机内部的储存形式,最快的方法就是使用这个运算符。

-1>>>0// 4294967295

上面代码表示,-1作为32位整数时,内部的储存形式使用无符号整数格式解读,值为 4294967295(即2^32 -1,等于32个1)。

开关作用

位运算符可以用作设置对象属性的开关。

假定某个对象有四个开关,每个开关都是一个变量。那么,可以设置一个四位的二进制数,它的每个位对应一个开关。

varFLAG_A =1;// 0001varFLAG_B =2;// 0010varFLAG_C =4;// 0100varFLAG_D =8;// 1000

上面代码设置A、B、C、D四个开关,每个开关分别占有一个二进制位。

然后,就可以用“与运算”检验,当前设置是否打开了指定开关。

varflags =3;// 二进制的0101if(flags & FLAG_C) {// ...}// 0101 & 0100 => 0100 => true

上面代码检验是否打开了开关C。如果打开,会返回true,否则返回false。

现在假设需要打开ABD三个开关,我们可以构造一个掩码变量。

varmask = FLAG_A | FLAG_B | FLAG_D;// 0001 | 0010 | 1000 => 1011

上面代码对ABD三个变量进行“或运算”,得到掩码值为二进制的1011。

有了掩码,“或运算”可以将当前设置改成指定设置。

flags = flags | mask;

“与运算”可以将当前设置中凡是与开关设置不一样的项,全部关闭。

flags = flags & mask;

“异或运算”可以切换(toggle)当前设置,即第一次执行可以得到当前设置的相反值,再执行一次又得到原来的值。

flags = flags ^ mask;

“否运算”可以翻转当前设置,即原设置为0,运算后变为1;原设置为1,运算后变为0。

flags = ~flags;

其他运算符

圆括号运算符

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

把表达式放在圆括号之中,将返回表达式的值。

{% highlight javascript %}

(1) // 1

('a') // a

(1+2) // 3

{% endhighlight %}

把对象放在圆括号之中,则会返回对象的值,即对象本身。

varo = {p:1};(o)// Object {p: 1}

将函数放在圆括号中,会返回函数本身。如果圆括号紧跟在函数的后面,就表示调用函数,即对函数求值。

functionf(){return1;}(f)// function f(){return 1;}f()// 1

上面的代码先定义了一个函数,然后依次将函数放在圆括号之中、将圆括号跟在函数后面,得到的结果是不一样的。

由于圆括号的作用是求值,如果将语句放在圆括号之中,就会报错,因为语句没有返回值。

(vara =1)// SyntaxError: Unexpected token var

void运算符

void运算符的作用是执行一个表达式,然后不返回任何值,或者说返回undefined。

void0// undefinedvoid(0)// undefined

上面是void运算符的两种写法,都正确。建议采用后一种形式,即总是使用括号。因为void运算符的优先性很高,如果不使用括号,容易造成错误的结果。比如,void 4 + 7实际上等同于(void 4) + 7。

下面是void运算符的一个例子。

varx =3;void(x =5)//undefinedx// 5

这个运算符主要是用于书签工具(bookmarklet),以及用于在超级链接中插入代码,目的是返回undefined可以防止网页跳转。

点击打开新窗口

上面代码用于在网页中创建一个链接,点击后会打开一个新窗口。如果没有void,点击后就会在当前窗口打开链接。

下面是常见的网页中触发鼠标点击事件的写法。

文字

上面代码有一个问题,函数f必须返回false,或者说onclick事件必须返回false,否则会引起浏览器跳转到example.com。

functionf(){// some codereturnfalse;}

或者写成

文字

void运算符可以取代上面两种写法。

文字

下面的代码会提交表单,但是不会产生页面跳转。

文字

逗号运算符

逗号运算符用于对两个表达式求值,并返回后一个表达式的值。

'a','b'// "b"varx =0;vary = (x++,10);x// 1y// 10

上面代码中,逗号运算符返回后一个表达式的值。

运算顺序

(1)运算符的优先级

JavaScript各种运算符的优先级别(Operator Precedence)是不一样的。优先级高的运算符先执行,优先级低的运算符后执行。

4+5*6// 34

上面的代码中,乘法运算符(*)的优先性高于加法运算符(+),所以先执行乘法,再执行加法,相当于下面这样。

4+ (5*6)// 34

如果多个运算符混写在一起,常常会导致令人困惑的代码。

varx =1;vararr = [];vary = arr.length <=0|| arr[0] ===undefined? x : arr[0];

上面代码中,变量y的值就很难看出来,因为这个表达式涉及5个运算符,到底谁的优先级最高,实在不容易记住。

根据语言规格,这五个运算符的优先级从高到低依次为:小于等于(<=)、严格相等(===)、或(||)、三元(?:)、等号(=)。因此上面的表达式,实际的运算顺序如下。

vary = ((arr.length <=0) || (arr[0] ===undefined)) ? x : arr[0];

记住所有运算符的优先级,几乎是不可能的,也是没有必要的。

(2)圆括号的作用

圆括号可以用来提高运算的优先级,因为它的优先级是最高的,即圆括号中的运算符会第一个运算。

(4+5) *6// 54

上面代码中,由于使用了圆括号,加法会先于乘法执行。

由于运算符的优先级别十分繁杂,且都是来自硬性规定,因此建议总是使用圆括号,保证运算顺序清晰可读,这对代码的维护和除错至关重要。

(3)左结合与右结合

对于优先级别相同的运算符,大多数情况,计算顺序总是从左到右,这叫做运算符的“左结合”(left-to-right associativity),即从左边开始计算。

x + y + z

上面代码先计算最左边的x与y的和,然后再计算与z的和。

但是少数运算符的计算顺序是从右到左,即从右边开始计算,这叫做运算符的“右结合”(right-to-left associativity)。其中,最主要的是赋值运算符(=)和三元条件运算符(?:)。

w = x = y = z;

q = a ? b : c ? d : e ? f : g;

上面代码的运算结果,相当于下面的样子。

w = (x = (y = z));

q = a ? b : (c ? d : (e ? f : g));

数据类型转换

JavaScript是一种动态类型语言,变量没有类型限制,可以随时赋予任意值。

varx = y ?1:'a';

上面代码中,变量x到底是数值还是字符串,取决于另一个变量y的值。只有在代码运行时,才可能知道x的类型。

虽然变量没有类型,但是数据本身和各种运算符是有类型的。如果运算符发现,数据的类型与预期不符,就会自动转换类型。比如,减法运算符预期两侧的运算子应该是数值,如果不是,就会自动将它们转为数值。

'4'-'3'// 1

上面代码中,虽然是两个字符串相减,但是依然会得到结果1,原因就在于JavaScript将它们自动转为了数值。

本节讲解数据类型自动转换的规则,在此之前,先讲解如何手动强制转换数据类型。

强制转换

强制转换主要指使用Number、String和Boolean三个构造函数,手动将各种类型的值,转换成数字、字符串或者布尔值。

Number()

使用Number函数,可以将任意类型的值转化成数值。

下面分成两种情况讨论,一种是参数是原始类型的值,另一种是参数是对象。

(1)原始类型值的转换规则

原始类型的值主要是字符串、布尔值、undefined和null,它们都能被Number转成数值或NaN。

// 数值:转换后还是原来的值Number(324)// 324// 字符串:如果可以被解析为数值,则转换为相应的数值Number('324')// 324// 字符串:如果不可以被解析为数值,返回NaNNumber('324abc')// NaN// 空字符串转为0Number('')// 0// 布尔值:true 转成1,false 转成0Number(true)// 1Number(false)// 0// undefined:转成 NaNNumber(undefined)// NaN// null:转成0Number(null)// 0

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

parseInt('42 cats')// 42Number('42 cats')// NaN

上面代码中,parseInt逐个解析字符,而Number函数整体转换字符串的类型。

另外,Number函数会自动过滤一个字符串前导和后缀的空格。

Number('\t\v\r12.34\n')// 12.34

(2)对象的转换规则

如果参数是对象,Number将其转为数值的规则比较复杂。JavaScript的内部处理步骤如下。

调用对象自身的valueOf方法。如果返回原始类型的值,则直接对该值使用Number函数,不再进行后续步骤。

如果valueOf方法返回的还是对象,则改为调用对象自身的toString方法。如果返回原始类型的值,则对该值使用Number函数,不再进行后续步骤。

如果toString方法返回的是对象,就报错。

请看下面的例子。

varobj = {a:1};Number(obj)// NaN// 等同于if(typeofobj.valueOf() ==='object') {Number(obj.toString());}else{Number(obj.valueOf());}

上面代码中,Number函数将obj对象转为数值。首先,调用obj.valueOf方法, 结果返回对象本身;于是,继续调用obj.toString方法,这时返回字符串[object Object],对这个字符串使用Number函数,得到NaN。

默认情况下,对象的valueOf方法返回对象本身,所以一般总是会调用toString方法,而toString方法返回对象的类型字符串(比如[object Object])。所以,会有下面的结果。

Number({})// NaN

如果toString方法返回的不是原始类型的值,结果就会报错。

varobj = {valueOf:function(){return{};  },toString:function(){return{};  }};Number(obj)// TypeError: Cannot convert object to primitive value

上面代码的valueOf和toString方法,返回的都是对象,所以转成数值时会报错。

从上面的例子可以看出,valueOf和toString方法,都是可以自定义的。

Number({valueOf:function(){return2;  }})// 2Number({toString:function(){return3;  }})// 3Number({valueOf:function(){return2;  },toString:function(){return3;  }})// 2

上面代码对三个对象使用Number函数。第一个对象返回valueOf方法的值,第二个对象返回toString方法的值,第三个对象表示valueOf方法先于toString方法执行。

String()

使用String函数,可以将任意类型的值转化成字符串。转换规则如下。

(1)原始类型值的转换规则

数值:转为相应的字符串。

字符串:转换后还是原来的值。

布尔值:true转为"true",false转为"false"。

undefined:转为"undefined"。

null:转为"null"。

String(123)// "123"String('abc')// "abc"String(true)// "true"String(undefined)// "undefined"String(null)// "null"

(2)对象的转换规则

String函数将对象转为字符串的步骤,与Number函数的处理步骤基本相同,只是互换了valueOf方法和toString方法的执行顺序。

先调用对象自身的toString方法。如果返回原始类型的值,则对该值使用String函数,不再进行以下步骤。

如果toString方法返回的是对象,再调用valueOf方法。如果返回原始类型的值,则对该值使用String函数,不再进行以下步骤。

如果valueOf方法返回的是对象,就报错。

下面是一个例子。

String({a:1})// "[object Object]"// 等同于String({a:1}.toString())// "[object Object]"

上面代码先调用对象的toString方法,发现返回的是字符串[object Object],就不再调用valueOf方法了。

如果toString法和valueOf方法,返回的都是对象,就会报错。

varobj = {valueOf:function(){console.log('valueOf');return{};  },toString:function(){console.log('toString');return{};  }};String(obj)// TypeError: Cannot convert object to primitive value

下面是通过自定义toString方法,改变转换成字符串时的返回值的例子。

String({toString:function(){return3;  }})// "3"String({valueOf:function(){return2;  }})// "[object Object]"String({valueOf:function(){return2;  },toString:function(){return3;  }})// "3"

上面代码对三个对象使用String函数。第一个对象返回toString方法的值(数值3),第二个对象返回的还是toString方法的值([object Object]),第三个对象表示toString方法先于valueOf方法执行。

Boolean()

使用Boolean函数,可以将任意类型的变量转为布尔值。

它的转换规则相对简单:除了以下六个值的转换结果为false,其他的值全部为true。

undefined

null

-0

0或+0

NaN

''(空字符串)

Boolean(undefined)// falseBoolean(null)// falseBoolean(0)// falseBoolean(NaN)// falseBoolean('')// false

注意,所有对象(包括空对象)的转换结果都是true,甚至连false对应的布尔对象new Boolean(false)也是true。

Boolean({})// trueBoolean([])// trueBoolean(newBoolean(false))// true

所有对象的布尔值都是true,这是因为JavaScript语言设计的时候,出于性能的考虑,如果对象需要计算才能得到布尔值,对于obj1 && obj2这样的场景,可能会需要较多的计算。为了保证性能,就统一规定,对象的布尔值为true。

自动转换

下面介绍自动转换,它是以强制转换为基础的。

遇到以下三种情况时,JavaScript会自动转换数据类型,即转换是自动完成的,对用户不可见。

// 1. 不同类型的数据互相运算123+'abc'// "123abc"// 2. 对非布尔值类型的数据求布尔值if('abc') {console.log('hello')}// "hello"// 3. 对非数值类型的数据使用一元运算符(即“+”和“-”)+ {foo:'bar'}// NaN- [1,2,3]// NaN

自动转换的规则是这样的:预期什么类型的值,就调用该类型的转换函数。比如,某个位置预期为字符串,就调用String函数进行转换。如果该位置即可以是字符串,也可能是数值,那么默认转为数值。

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

自动转换为布尔值

当JavaScript遇到预期为布尔值的地方(比如if语句的条件部分),就会将非布尔值的参数自动转换为布尔值。系统内部会自动调用Boolean函数。

因此除了以下六个值,其他都是自动转为true。

undefined

null

-0

0或+0

NaN

''(空字符串)

下面这个例子中,条件部分的每个值都相当于false,使用否定运算符后,就变成了true。

if( !undefined&& !null&& !0&& !NaN&& !'') {console.log('true');}// true

下面两种写法,有时也用于将一个表达式转为布尔值。它们内部调用的也是Boolean函数。

// 写法一expression ?true:false// 写法二!! expression

自动转换为字符串

当JavaScript遇到预期为字符串的地方,就会将非字符串的数据自动转为字符串。系统内部会自动调用String函数。

字符串的自动转换,主要发生在加法运算时。当一个值为字符串,另一个值为非字符串,则后者转为字符串。

'5'+1// '51''5'+true// "5true"'5'+false// "5false"'5'+ {}// "5[object Object]"'5'+ []// "5"'5'+function(){}// "5function (){}"'5'+undefined// "5undefined"'5'+null// "5null"

这种自动转换很容易出错。

varobj = {width:'100'};obj.width +20// "10020"

上面代码中,开发者可能期望返回120,但是由于自动转换,实际上返回了一个字符10020。

自动转换为数值

当JavaScript遇到预期为数值的地方,就会将参数值自动转换为数值。系统内部会自动调用Number函数。

除了加法运算符有可能把运算子转为字符串,其他运算符都会把运算子自动转成数值。

'5'-'2'// 3'5'*'2'// 10true-1// 0false-1// -1'1'-1// 0'5'* []// 0false/'5'// 0'abc'-1// NaN

上面代码中,运算符两侧的运算子,都被转成了数值。

一元运算符也会把运算子转成数值。

+'abc'// NaN-'abc'// NaN+true// 1-false// 0

错误处理机制

Error对象

一旦代码解析或运行时发生错误,JavaScript引擎就会自动产生并抛出一个Error对象的实例,然后整个程序就中断在发生错误的地方。

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

name:错误名称

message:错误提示信息

stack:错误的堆栈(非标准属性,但是大多数平台支持)

利用name和message这两个属性,可以对发生什么错误有一个大概的了解。

if(error.name){console.log(error.name +": "+ error.message);}

上面代码表示,显示错误的名称以及出错提示信息。

stack属性用来查看错误发生时的堆栈。

functionthrowit(){thrownewError('');}functioncatchit(){try{    throwit();  }catch(e) {console.log(e.stack);// print stack trace}}catchit()// Error//    at throwit (~/examples/throwcatch.js:9:11)//    at catchit (~/examples/throwcatch.js:3:9)//    at repl:1:5

上面代码显示,抛出错误首先是在throwit函数,然后是在catchit函数,最后是在函数的运行环境中。

JavaScript的原生错误类型

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

(1)SyntaxError

SyntaxError是解析代码时发生的语法错误。

// 变量名错误var1a;// 缺少括号console.log'hello');

(2)ReferenceError

ReferenceError是引用一个不存在的变量时发生的错误。

unknownVariable// ReferenceError: unknownVariable is not defined

另一种触发场景是,将一个值分配给无法分配的对象,比如对函数的运行结果或者this赋值。

console.log() =1// ReferenceError: Invalid left-hand side in assignmentthis=1// ReferenceError: Invalid left-hand side in assignment

上面代码对函数console.log的运行结果和this赋值,结果都引发了ReferenceError错误。

(3)RangeError

RangeError是当一个值超出有效范围时发生的错误。主要有几种情况,一是数组长度为负数,二是Number对象的方法参数超出范围,以及函数堆栈超过最大值。

newArray(-1)// RangeError: Invalid array length(1234).toExponential(21)// RangeError: toExponential() argument must be between 0 and 20

(4)TypeError

TypeError是变量或参数不是预期类型时发生的错误。比如,对字符串、布尔值、数值等原始类型的值使用new命令,就会抛出这种错误,因为new命令的参数应该是一个构造函数。

new123//TypeError: number is not a funcvarobj = {};obj.unknownMethod()// TypeError: undefined is not a function

上面代码的第二种情况,调用对象不存在的方法,会抛出TypeError错误。

(5)URIError

URIError是URI相关函数的参数不正确时抛出的错误,主要涉及encodeURI()、decodeURI()、encodeURIComponent()、decodeURIComponent()、escape()和unescape()这六个函数。

decodeURI('%2')// URIError: URI malformed

(6)EvalError

eval函数没有被正确执行时,会抛出EvalError错误。该错误类型已经不再在ES5中出现了,只是为了保证与以前代码兼容,才继续保留。

以上这6种派生错误,连同原始的Error对象,都是构造函数。开发者可以使用它们,人为生成错误对象的实例。

newError("出错了!");newRangeError("出错了,变量超出有效范围!");newTypeError("出错了,变量类型无效!");

上面代码表示新建错误对象的实例,实质就是手动抛出错误。可以看到,错误对象的构造函数接受一个参数,代表错误提示信息(message)。

自定义错误

除了JavaScript内建的7种错误对象,还可以定义自己的错误对象。

functionUserError(message){this.message = message ||"默认信息";this.name ="UserError";}UserError.prototype =newError();UserError.prototype.constructor = UserError;

上面代码自定义一个错误对象UserError,让它继承Error对象。然后,就可以生成这种自定义的错误了。

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

throw语句

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

// 抛出一个字符串throw"Error!";// 抛出一个数值throw42;// 抛出一个布尔值throwtrue;// 抛出一个对象throw{toString:function(){return"Error!"; } };

上面代码表示,throw可以接受各种值作为参数。JavaScript引擎一旦遇到throw语句,就会停止执行后面的语句,并将throw语句的参数值,返回给用户。

如果只是简单的错误,返回一条出错信息就可以了,但是如果遇到复杂的情况,就需要在出错以后进一步处理。这时最好的做法是使用throw语句手动抛出一个Error对象。

thrownewError('出错了!');

上面语句新建一个Error对象,然后将这个对象抛出,整个程序就会中断在这个地方。

throw语句还可以抛出用户自定义的错误。

functionUserError(message){this.message = message ||"默认信息";this.name ="UserError";}UserError.prototype.toString =function(){returnthis.name +': "'+this.message +'"';}thrownewUserError("出错了!");

可以通过自定义一个assert函数,规范化throw抛出的信息。

functionassert(expression, message){if(!expression)throw{name:'Assertion Exception',message: message};}

上面代码定义了一个assert函数,它接受一个表达式和一个字符串作为参数。一旦表达式不为真,就抛出指定的字符串。它的用法如下。

assert(typeofmyVar !='undefined','myVar is undefined!');

console对象的assert方法,与上面函数的工作机制一模一样,所以可以直接使用。

console.assert(typeofmyVar !='undefined','myVar is undefined!');

try...catch结构

为了对错误进行处理,需要使用try...catch结构。

try{thrownewError('出错了!');}catch(e) {console.log(e.name +": "+ e.message);console.log(e.stack);}// Error: 出错了!//  at :3:9//  ...

上面代码中,try代码块一抛出错误(上例用的是throw语句),JavaScript引擎就立即把代码的执行,转到catch代码块。可以看作,错误可以被catch代码块捕获。catch接受一个参数,表示try代码块抛出的值。

functionthrowIt(exception){try{throwexception;  }catch(e) {console.log('Caught: '+ e);  }}throwIt(3);// Caught: 3throwIt('hello');// Caught: hellothrowIt(newError('An error happened'));// Caught: Error: An error happened

上面代码中,throw语句先后抛出数值、字符串和错误对象。

catch代码块捕获错误之后,程序不会中断,会按照正常流程继续执行下去。

try{throw"出错了";}catch(e) {console.log(111);}console.log(222);// 111// 222

上面代码中,try代码块抛出的错误,被catch代码块捕获后,程序会继续向下执行。

catch代码块之中,还可以再抛出错误,甚至使用嵌套的try...catch结构。

varn =100;try{thrown;}catch(e) {if(e <=50) {// ...}else{throwe;  }}

上面代码中,catch代码之中又抛出了一个错误。

为了捕捉不同类型的错误,catch代码块之中可以加入判断语句。

try{  foo.bar();}catch(e) {if(einstanceofEvalError) {console.log(e.name +": "+ e.message);  }elseif(einstanceofRangeError) {console.log(e.name +": "+ e.message);  }// ...}

上面代码中,catch捕获错误之后,会判断错误类型(EvalError还是RangeError),进行不同的处理。

try...catch结构是JavaScript语言受到Java语言影响的一个明显的例子。这种结构多多少少是对结构化编程原则一种破坏,处理不当就会变成类似goto语句的效果,应该谨慎使用。

finally代码块

try...catch结构允许在最后添加一个finally代码块,表示不管是否出现错误,都必需在最后运行的语句。

functioncleansUp(){try{thrownewError('Sorry...');  }finally{console.log('Performing clean-up');  }}cleansUp()// Performing clean-up// Error: Sorry...

上面代码说明,throw语句抛出错误以后,finally继续得到执行。

functionidle(x){try{console.log(x);return'result';  }finally{console.log("FINALLY");  }}idle('hello')// hello// FINALLY// "result"

上面代码说明,即使有return语句在前,finally代码块依然会得到执行,且在其执行完毕后,才会显示return语句的值。

下面的例子说明,return语句的执行是排在finally代码之前,只是等finally代码执行完毕后才返回。

varcount =0;functioncountUp(){try{returncount;  }finally{    count++;  }}countUp()// 0count// 1

上面代码说明,return语句的count的值,是在finally代码块运行之前,就获取完成了。

下面是finally代码块用法的典型场景。

openFile();try{  writeFile(Data);}catch(e) {  handleError(e);}finally{  closeFile();}

上面代码首先打开一个文件,然后在try代码块中写入文件,如果没有发生错误,则运行finally代码块关闭文件;一旦发生错误,则先使用catch代码块处理错误,再使用finally代码块关闭文件。

下面的例子充分反应了try...catch...finally这三者之间的执行顺序。

functionf(){try{console.log(0);throw"bug";  }catch(e) {console.log(1);returntrue;// 这句原本会延迟到finally代码块结束再执行console.log(2);// 不会运行}finally{console.log(3);returnfalse;// 这句会覆盖掉前面那句returnconsole.log(4);// 不会运行}console.log(5);// 不会运行}varresult = f();// 0// 1// 3result// false

上面代码中,catch代码块结束执行之前,会先执行finally代码块。从catch转入finally的标志,不仅有return语句,还有throw语句。

functionf(){try{throw'出错了!';  }catch(e) {console.log('捕捉到内部错误');throwe;// 这句原本会等到finally结束再执行}finally{returnfalse;// 直接返回}}try{  f();}catch(e) {// 此处不会执行console.log('caught outer "bogus"');}//  捕捉到内部错误

上面代码中,进入catch代码块之后,一遇到throw语句,就会去执行finally代码块,其中有return false语句,因此就直接返回了,不再会回去执行catch代码块剩下的部分了。

某些情况下,甚至可以省略catch代码块,只使用finally代码块。

openFile();try{  writeFile(Data);}finally{  closeFile();}

编程风格

所谓"编程风格"(programming style),指的是编写代码的样式规则。不同的程序员,往往有不同的编程风格。

有人说,编译器的规范叫做"语法规则"(grammar),这是程序员必须遵守的;而编译器忽略的部分,就叫"编程风格"(programming style),这是程序员可以自由选择的。这种说法不完全正确,程序员固然可以自由选择编程风格,但是好的编程风格有助于写出质量更高、错误更少、更易于维护的程序。

所以,"编程风格"的选择不应该基于个人爱好、熟悉程度、打字量等因素,而要考虑如何尽量使代码清晰易读、减少出错。你选择的,不是你喜欢的风格,而是一种能够清晰表达你的意图的风格。这一点,对于JavaScript这种语法自由度很高的语言尤其重要。

必须牢记的一点是,如果你选定了一种“编程风格”,就应该坚持遵守,切忌多种风格混用。如果你加入他人的项目,就应该遵守现有的风格。

缩进

空格和Tab键,都可以产生缩进效果(indent)。

Tab键可以节省击键次数,但不同的文本编辑器对Tab的显示不尽相同,有的显示四个空格,有的显示两个空格,所以有人觉得,空格键可以使得显示效果更统一。

无论你选择哪一种方法,都是可以接受的,要做的就是始终坚持这一种选择。不要一会使用Tab键,一会使用空格键。

区块

如果循环和判断的代码体只有一行,JavaScript允许该区块(block)省略大括号。

if(a)  b();  c();

上面代码的原意可能是下面这样。

if(a) {  b();  c();}

但是,实际效果却是下面这样。

if(a) {  b();}  c();

因此,总是使用大括号表示区块。

另外,区块起首的大括号的位置,有许多不同的写法。

最流行的有两种。一种是起首的大括号另起一行:

block{// ...}

另一种是起首的大括号跟在关键字的后面。

block {// ...}

一般来说,这两种写法都可以接受。但是,JavaScript要使用后一种,因为JavaScript会自动添加句末的分号,导致一些难以察觉的错误。

return{key: value};// 相当于return;{key: value};

上面的代码的原意,是要返回一个对象,但实际上返回的是undefined,因为JavaScript自动在return语句后面添加了分号。为了避免这一类错误,需要写成下面这样。

return{key: value};

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

圆括号

圆括号(parentheses)在JavaScript中有两种作用,一种表示函数的调用,另一种表示表达式的组合(grouping)。

// 圆括号表示函数的调用console.log('abc');// 圆括号表示表达式的组合(1+2) *3

我们可以用空格,区分这两种不同的括号。

表示函数调用时,函数名与左括号之间没有空格。

表示函数定义时,函数名与左括号之间没有空格。

其他情况时,前面位置的语法元素与左括号之间,都有一个空格。

按照上面的规则,下面的写法都是不规范的。

foo (bar)return(a+b);if(a ===0) {...}functionfoo(b){...}function(x){...}

上面代码的最后一行是一个匿名函数,function是语法关键字,不是函数名,所以与左括号之间应该要有一个空格。

行尾的分号

分号表示一条语句的结束。JavaScript规定,行尾的分号可以省略。事实上,确实有一些开发者行尾从来不写分号。但是,由于下面要讨论的原因,建议还是不要这个分号。

不使用分号的情况

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

(1)for和while循环

for( ; ; ) {}// 没有分号while(true) {}// 没有分号

需要注意的是do...while循环是有分号的。

do{  a--;}while(a >0);// 分号不能省略

(2)分支语句:if,switch,try

if(true) {}// 没有分号switch() {}// 没有分号try{}catch{}// 没有分号

(3)函数的声明语句

functionf(){}// 没有分号

但是函数表达式仍然要使用分号。

varf =functionf(){};

以上三种情况,如果使用了分号,并不会出错。因为,解释引擎会把这个分号解释为空语句。

分号的自动添加

除了上一节的三种情况,所有语句都应该使用分号。但是,如果没有使用分号,大多数情况下,JavaScript会自动添加。

vara =1// 等同于vara =1;

这种语法特性被称为“分号的自动添加”(Automatic Semicolon Insertion,简称ASI)。

因此,有人提倡省略句尾的分号。麻烦的是,如果下一行的开始可以与本行的结尾连在一起解释,JavaScript就不会自动添加分号。

// 等同于 var a = 3vara=3// 等同于 'abc'.length'abc'.length// 等同于 return a + b;returna +b;// 等同于 obj.foo(arg1, arg2);obj.foo(arg1,arg2);// 等同于 3 * 2 + 10 * (27 / 6)3*2+10* (27/6)

上面代码都会多行放在一起解释,不会每一行自动添加分号。这些例子还是比较容易看出来的,但是下面这个例子就不那么容易看出来了。

x = y(function(){// ...})();// 等同于x = y(function(){...})();

下面是更多不会自动添加分号的例子。

// 解释为 c(d+e)vara = b + c(d+e).toString();// 解释为 a = b/hi/g.exec(c).map(d)// 正则表达式的斜杠,会当作除法运算符a = b/hi/g.exec(c).map(d);// 解释为'b'['red', 'green'],// 即把字符串当作一个数组,按索引取值vara ='b'['red','green'].forEach(function(c){console.log(c);})// 解释为 function(x) { return x }(a++)// 即调用匿名函数,结果f等于0vara =0;varf =function(x){returnx }(a++)

只有下一行的开始与本行的结尾,无法放在一起解释,JavaScript引擎才会自动添加分号。

if(a <0) a =0console.log(a)// 等同于下面的代码,// 因为0console没有意义if(a <0) a =0;console.log(a)

另外,如果一行的起首是“自增”(++)或“自减”(--)运算符,则它们的前面会自动添加分号。

a = b = c =1a++b--cconsole.log(a, b, c)// 1 2 0

上面代码之所以会得到“1 2 0”的结果,原因是自增和自减运算符前,自动加上了分号。上面的代码实际上等同于下面的形式。

a = b = c =1;a;++b;--c;

如果continue、break、return和throw这四个语句后面,直接跟换行符,则会自动添加分号。这意味着,如果return语句返回的是一个对象的字面量,起首的大括号一定要写在同一行,否则得不到预期结果。

return{first:'Jane'};// 解释成return;{first:'Jane'};

由于解释引擎自动添加分号的行为难以预测,因此编写代码的时候不应该省略行尾的分号。

不应该省略结尾的分号,还有一个原因。有些JavaScript代码压缩器不会自动添加分号,因此遇到没有分号的结尾,就会让代码保持原状,而不是压缩成一行,使得压缩无法得到最优的结果。

另外,不写结尾的分号,可能会导致脚本合并出错。所以,有的代码库在第一行语句开始前,会加上一个分号。

;vara =1;// ...

上面这种写法就可以避免与其他脚本合并时,排在前面的脚本最后一行语句没有分号,导致运行出错的问题。

全局变量

JavaScript最大的语法缺点,可能就是全局变量对于任何一个代码块,都是可读可写。这对代码的模块化和重复使用,非常不利。

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

变量声明

JavaScript会自动将变量声明"提升"(hoist)到代码块(block)的头部。

if(!o) {varo = {};}// 等同于varo;if(!o) {  o = {};}

为了避免可能出现的问题,最好把变量声明都放在代码块的头部。

for(vari =0; i <10; i++) {// ...}// 写成vari;for(i =0; i <10; i++) {// ...}

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

new命令

JavaScript使用new命令,从构造函数生成一个新对象。

varo =newmyObject();

上面这种做法的问题是,一旦你忘了加上new,myObject()内部的this关键字就会指向全局对象,导致所有绑定在this上面的变量,都变成全局变量。

因此,建议使用Object.create()命令,替代new命令。如果不得不使用new,为了防止出错,最好在视觉上把构造函数与其他函数区分开来。比如,构造函数的函数名,采用首字母大写(InitialCap),其他函数名一律首字母小写。

with语句

with可以减少代码的书写,但是会造成混淆。

with(o) { foo = bar;}

上面的代码,可以有四种运行结果:

o.foo = bar;

o.foo = o.bar;

foo = bar;

foo = o.bar;

这四种结果都可能发生,取决于不同的变量是否有定义。因此,不要使用with语句。

相等和严格相等

JavaScript有两个表示"相等"的运算符:"相等"(==)和"严格相等"(===)。

因为"相等"运算符会自动转换变量类型,造成很多意想不到的情况:

0==''// true1==true// true2==true// false0=='0'// truefalse=='false'// falsefalse=='0'// true’ \t\r\n' == 0 // true

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

语句的合并

有些程序员追求简洁,喜欢合并不同目的的语句。比如,原来的语句是

a = b;if(a) {// ...}

他喜欢写成下面这样。

if(a = b) {// ...}

虽然语句少了一行,但是可读性大打折扣,而且会造成误读,让别人误解这行代码的意思是下面这样。

if(a === b){// ...}

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

自增和自减运算符

自增(++)和自减(--)运算符,放在变量的前面或后面,返回的值不一样,很容易发生错误。事实上,所有的++运算符都可以用+= 1代替。

++x// 等同于x +=1;

改用+= 1,代码变得更清晰了。有一个很可笑的例子,某个JavaScript函数库的源代码中出现了下面的片段:

++x;

++x;

这个程序员忘了,还有更简单、更合理的写法。

x +=2;

建议自增(++)和自减(--)运算符尽量使用+=和-=代替。

switch...case结构

switch...case结构要求,在每一个case的最后一行必须是break语句,否则会接着运行下一个case。这样不仅容易忘记,还会造成代码的冗长。

而且,switch...case不使用大括号,不利于代码形式的统一。此外,这种结构类似于goto语句,容易造成程序流程的混乱,使得代码结构混乱不堪,不符合面向对象编程的原则。

functiondoAction(action){switch(action) {case'hack':return'hack';break;case'slash':return'slash';break;case'run':return'run';break;default:thrownewError('Invalid action.');  }}

上面的代码建议改写成对象结构。

functiondoAction(action){varactions = {'hack':function(){return'hack';    },'slash':function(){return'slash';    },'run':function(){return'run';    }  };if(typeofactions[action] !=='function') {thrownewError('Invalid action.');  }returnactions[action]();}

建议避免使用switch...case结构,用对象结构代替。


著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处js基础知识

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270