4.3 处理业务
在JS中,使用函数(function)对象用来处理业务是最常见的用法。JS中真正对对象的操作大部分都是通过函数对象来执行的。函数的创建方式,前面已经介绍过,其中用来处理业务的是函数体。函数体主要包括变量、操作符和语句三大部分内容。
本节首先介绍JS中的函数变量、操作符和语句,然后介绍函数中最容易出问题的变量作用域和闭包。因为本书不是针对零基础读者的,所以对函数的变量、操作符和语句这部分的介绍并不会非常详细、面面俱到。如果大家需要详细学习这部分内容,那么可以参考其他相关资料。
4.3.1 变量
在函数中,变量的主要作用是暂存业务处理过程中用到的一些值。JS中的变量是使用var关键字来定义的,无论什么类型的变量都使用它来定义,因此JS是一种弱类型语言。但是,它的变量是区分大小写的,也就是说,red、Red、reD是三个不同的变量。JS中的变量早期要求必须是用 “_”、“$”、英文字母和数字组成,而且第一个字符不可以是数字。对于现在的引擎来说,只要在词法解析的时候不引起误会就可以了,即使使用中文也是可以的。
另外,变量也不可以使用JS的关键字和保留字,这在任何语言里都是一样的,否则就乱套了。
ES5.1中规定了以下关键字。
break do instanceof typeof case else new var catch finally return void continue for switch while debugger function this with default if throw delete in try
ES2015中规定了以下关键字。
break do instanceof typeof case else in var catch export new void class extends return while const finally super continue wit for switch yield debugger function this default if throw delete import try
除了上面的关键字外,还有一些词虽然不是关键字,但是在后续版本中可能会成为关键字,现在是保留字,最好也不要使用。保留字可分为普通保留字和严格模式的保留字两种类型,严格模式的保留字只在“strict model”模式中会报错。
ES5.1中的普通保留字如下。
class enum extends super const export import
ES2015中的普通保留字如下。
enum await
ES5.1中的严格模式保留字如下。
implements let private public yield interface package protected static
ES2015中的严格模式保留字如下。
implements package protected interface private public
多知道点
JS中的strict model
ES5中引入了strict model(严格模式)。严格模式下的JS程序需要比非严格模式下的程序更加规范。严格模式对语法做了比较严格的要求,例如,不可以使用with语句、不可以重复定义变量、不可以不定义变量直接使用(在非严格模式下会自动定义为全局变量)等。如果使用严格模式,那么只需要在代码中加入“strict model”字符串就可以了。可以将它用到全局中,也可以用到指定的函数中。如果只用到指定的函数中,则只需要在函数内部添加“strict model”字符串就可以了。
非严格模式主要是为了向前兼容。在新写的程序中应该尽量使用严格模式,不过最好局部使用而不是全局使用的,因为页面中可能还会引用别人写的代码,例如一些库文件,它们不一定是按严格模式写的,所以最好使用局部的(function级)严格模式。
4.3.2 操作符
相信大家对操作符都不陌生,操作符是直接告诉编译器怎么操作对象的工具。本节首先给大家列出了ES2015中的所有操作符,并对每个操作符列出其功能,列举相应的示例,最后再对一些特殊的操作符或者特殊的用法进行单独讲解。
1. ES2015操作符列表
表4-1列出了ES2015中所用的操作符。
表4-1 ES2015所用的操作符
(续)
2.使用细节与技巧
(1)==和===
这两个符号都是用来判断是否相等的,但是具体用法存在一些区别。因为JS是一种弱类型语言,所以不同类型之间也可以比较是否相等,如果用==则会比较转换后的值是否相等,如果用===比较则只要类型不同就返回false,例如下面的例子。
var a= 1, b="1"; console.log(a==b); //输出true console.log(a===b); //输出false
(2)>>和>>>
这两个符号的作用都是右移位,下面先来介绍一下左移和右移的概念。在计算机中保存的数据(主要指整数)也像平时所写的数一样,除了数字本身外还有“位”的概念,例如4567这个数的千位为4,百位为5,十位为6,个位为7,把不同的数字放到不同的位,其权重就不一样了。假如现在的数只有4位(就像某些需要填数字的单据上一格填一个数字,一共有4格,只能填4个数字),那么4567左移一位就是5670,左移两位就是6700,而右移一位就成了0456,右移两位就成了0045。在计算机中的左移右移跟这里类似,只是它不一定是4位,而且每一位都只能是0或1。在程序中经常使用左移右移来做2的整数倍的乘除法,这就像十进制中左移一位扩大10倍右移一位缩小到原来的1/10一样,不过左移右移要比乘除法的计算简单很多。对于处理器来说也一样,移位要比计算乘除速度快。
不过计算机中的移位跟上述十进制的移位还是存在区别的。在计算机中因为只有0和1,所以为了区分正负数,现有的做法是将负数用补码来表示,这样只要看最高位是0还是1,就可以区分正负数了(如果读者不明白补码也没关系,这里只要知道有符号数将最高位用作符号位,符号位0表示正数,符号位1表示负数即可)。但是这时候问题就来了,对于一个负数来说,右移一位后其符号位(最高位)正常应该变成0,这样就变成正数了,此时就会出现问题了,因此在右移的时候对有符号数和无符号分别使用了不同的操作符。
当然,这只是表层的区别,在底层有符号数和无符号数使用的是两套不同的进位/溢出标志,例如x86处理器中有符号数用OF,无符号数用CF标志。另外,数据本身只是一串由0和1组成的编码,是无法区分有符号数还是无符号数的,只是人为将其看作有符号数或者无符号数而已(在程序的底层会通过类型标志进行区分)。对于一些强类型语言来说,在定义数据的时候就会指定数据的类型,这样在使用时,就可以清楚地知道应该将其看作有符号数还是无符号数。因为JS是一种弱类型语言,变量的类型可以任意转换,所以对于-1>>>0这样的操作来说,操作的目标是表示-1的一串数(对于32位数来说就是0XFFFFFFFF,即32个1),操作的过程是将其当作无符号数左移0位,这时虽然不会对数字本身做任何修改,但是,因为在操作的过程中已经将其看作无符号数,所以其结果也就变成了4294967295,即无符号数的32个1,同样这也是有符号数-1的32位编码。
(3)&&和||
逻辑与操作符&&的判断逻辑是依次判断每个表达式,当遇到false的表达式时就会马上返回而不再继续执行后面的表达式,如果所有表达式都为true,则返回true,例如下面的语句。
(表达式1)&& (表达式2) && (表达式3) && (表达式4) && (表达式5)
当表达式1和表达式2都为true而表达式3为false时,表达式4和表达式5就不再判断了,也就不会被执行。利用这个特性可以执行一些条件语句,例如下面的语句。
b=a>5&&a-3;
这条语句在a>5的时候就会执行a-3并将计算结果赋值给b,否则将false赋值给b,相当于下面的语句。
if(a>5){ b=a-3; }else{ b=false; }
通过多个表达式的逻辑与组合可以很便捷地完成一组相互具有依赖性的操作。
逻辑或操作符||使用好了也非常方便。它的判断逻辑是依次判断每个表达式,当遇到第一个true表达式的时候,马上返回而不再继续执行后面的表达式,如果所有表达式都为false则返回false,例如下面的语句。
typeof jQuery! ="undefined"||importjQuery();
这条语句首先通过判断jQuery是否存在来判断是否需要引入jQuery,如果不存在,则调用importjQuery函数导入jQuery,否则就不导入。
另外,点操作符也非常重要,后面会进行详细介绍。按位操作在硬件中使用得比较多,在软件编程中主要用于组合或提取某个标志量,在后面用到的时候还会介绍。“…”和“=>”是ES2015中新增的操作符,后面会进行专门讲解。其他操作符比较简单,通过表4-1中的例子可以很容易地理解,本书就不详细介绍了。
4.3.3 语句
语句是用来执行具体功能的,可以分为单条语句和语句块。单条语句以分号结束,语句块用花括号包含,语句块可以包含多条语句。
语句主要由变量(或属性)、操作符和关键字组成。有的语句使用操作符完成,还有的语句需要使用相应的关键字。使用操作符的语句只需要按照4.3.1节介绍的各种操作符的使用方法来完成就可以了。下面将介绍使用关键字完成的语句。
1. var和let语句
var关键字用来定义变量,如果在函数内部定义,则定义的变量只能在函数内部使用,如果在函数外部定义,则会成为全局变量。在函数中使用变量时会优先使用内部变量,只有找不到内部变量时才会使用全局变量,下面来看个例子。
var color = "red"; function localColor(){ var color = "blue"; console.log(color); } function globalColor(){ console.log(color); } localColor(); //blue globalColor(); //red
localColor方法中由于定义了内部变量color,因此会输出blue; globalColor方法中由于没有定义内部变量color,因此会输出全局变量red。
多知道点
在JS中函数是怎么执行的
函数无非两部分:数据和对数据的操作。数据又分为外部数据和内部数据,对于外部数据,本书将在后边的作用域链中进行介绍。内部数据又分为参数和变量两部分。在函数每次执行的时候参数都会被赋予一个新值,而变量则每次都会被设置为一个相同的初始值。
函数的变量和参数是怎么保存的呢?对于多个数据来说,最常用也是最简单的保存方式就是使用数组保存,这样按序号查找起来就非常方便了。而且,一般来说,一个函数的参数和变量都会集中保存在一个数组或者跟数组类似的结构(例如栈)中。但是,数组本身存在一个非常致命的缺点,它要求每个元素的长度都相等,这对于参数(或变量)来说是很难符合要求的。但是,为了使用数组(或栈)的便捷性通常会在数组中保存一个包含地址的数据(除地址外,还可能包含数据类型等其他数据),而不是实际的数据,这样既可以使用数组,又可以保存不同长度的数据。此时,在函数中使用参数(或变量)的时候只需要使用“第几个参数(或变量)”就可以了,至于数组中具体一个元素使用多少位,则需要根据不同的硬件平台(例如,是32位还是64位)和具体引擎的开发者来确定。但是,这里还存在一个小问题,对于复杂的数据来说,这样保存无可厚非,而对于直接使用数组元素就可以保存的简单数据(例如整数)来说,再使用这种方式就显得复杂了,而且多一步通过地址查找数据的操作也会影响效率,因此这种情况一般会直接将值保存到数组中,而不是保存地址。
函数在每次执行之前都会新建一个参数数组和一个变量数组(当然也可以合并为一个数组,而且通常会使用栈来实现),然后将调用时所传递的参数设置到参数数组中,而变量数组在每次执行前都具有相同的内容,对数据进行操作时只需要使用“第几个参数”或者“第几个变量”即可。
简单的数据(例如整数)会直接保存在数组中,而对于复杂的数据,数组中只保存地址,具体的数据保存在堆中。可以简单地将堆理解为一堆草纸,其所保存的数据是所有函数所共享的,不过也并不是每个函数都可以调用堆中所有的数据。因为调用堆中数据的前提是能找到,如果找不到当然也就调用不了。例如,在函数中定义了一个字符串的对象变量s,这时就会将s的内容保存到堆中,然后将堆中所保存数据的地址保存到函数的变量数组中,这时对于函数外部来说,虽然可以访问堆中的数据,但是因为没有s的地址,所以也就无法访问s这个字符串变量了。
下面来看一个例子,首先对下面的代码设置断点,然后使用FireBug进行调试。
function paramF(p1){ var msg="hello"; console.log(p1); //此行设置断点 for(var i in arguments){ console.log(arguments[i]); } } paramF("a", "b", "c"); //a a b c
当执行到断点处时,在FireBug中可以看到如图4-6所示的结构。
图4-6 FireBug的调试界面
从图4-6的右侧可以看出,函数在执行时会将参数p1和函数中所用到的变量msg、i放到相同的地位,即在函数内部执行的时候就不会区分是参数还是变量。另外,在JS的函数中,会自动创建一个名为arguments的内部变量,然后将所有参数的地址保存到其中。arguments类似数组对象,可以通过它来获取函数调用时所传递的参数。接着来看下面的例子。
function paramF(p1){ console.log(p1); for(var i in arguments){ console.log(arguments[i]); } } paramF("a"); //a a paramF("a", "b", "c"); //a a b c
paramF方法首先打印了参数p1的值,然后遍历打印arguments中所有参数的值。可以看出,参数p1的值和arguments[0]的值是一样的,函数的参数按顺序依次保存在arguments变量中。还可以看到,在调用函数时传入参数的个数也可以和定义时不一样。例如,虽然paramF函数定义时只有一个参数,但是在调用时却可以传递三个参数,当然也可以传递任意个数的参数,甚至不传递参数,因此JS中不存在同名函数重载的用法。
函数定义时的参数(通常叫形参)和arguments对象的关系如下:在JS的函数调用前JS引擎会创建一个arguments对象,然后在其中保存调用时的参数(通常叫实参),而形参其实只是一个名字,在实际操作时会将其翻译为arguments对象的一个元素。例如,对于“console.log(p1); ”这条语句,在操作时会被翻译为“控制台打印arguments的第一个元素”,即函数的形参只是一个名字,是给程序员看的,引擎在实际操作时会自动将其翻译为arguments中的一个元素,可以使用下面的例子来验证。
function paramF(p1){ console.log(arguments[0]===p1); } paramF("www.excelib.com"); //true
当然,这里给大家介绍的只是一种实现方案,还有其他方案。例如,可以直接把参数对象放入栈中,不同参数可以使用偏移量来表示,不过原理都是一样的。
在JS中使用var定义的变量是函数级作用域而不是块级作用域,即一个语句块内部定义的变量在语句块外部也可以使用,例如下面的例子。
(function (num){ if(num>36){ var result=true; } console.log(result); })(81); //true
这里的result是在if语句块中定义的,但是在if语句块外部依然可以调用。这是因为JS的方法在执行时会将其自身所有使用var定义的变量统一放到前面介绍的变量数组中,所以在一个函数中,所有使用var定义的变量都是同等地位的,即在JS中使用var定义的变量是function级作用域而不是块级作用域。
多知道点
JS中自运行的匿名函数
在JS中可以使用匿名函数,其原理非常简单。前面说过,在JS中函数其实也是一种对象,在底层只要用一块内存将其保存下来即可。在调用时只需要找到这块内存,然后创建好执行环境(包含前面所介绍的参数数组、变量数组等内容)就可以执行了。所以有两个关键方面:①将函数对象保存到一块内存中;②找到这块内存。通常使用函数名来查找这块内存的地址,不过函数名只是查找这块内存的一个工具,最主要的目的其实是找到这块内存,也就是说,即使没有函数名也可以,只要能找到这块内存就行。因此可以使用匿名函数,其用法如下。
首先使用function关键字定义一个函数,然后将其使用小括号括起来(这只是语法的要求,否则后面的执行语句无法被引擎正确识别),这样就将函数定义好了,引擎会为其分配一块内存来保存。然后直接在后面加个小括号,并将参数放入其中,这样引擎就知道要使用这块内存所保存的函数来执行了。因为对于JS来说,在函数后面加小括号是调用函数的意思,这是JS的语法规则(如果是我们来设计,当然也可以设计为见到“调用XXX”的字符串执行函数)。这时既有保存函数的内存也有内存的地址,这样就可以执行了。例如,上面的例子中首先定义了一个函数,然后使用小括号将其扩起来,后面又加了一个表示执行的小括号,并将参数81放入其中,这样就可以执行了。
下面再来看一个例子。
var log=(function (){ console.log("创建日志函数"); return function(param){ console.log(param); }; })(); log("www.excelib.com");
这里也创建了一个自运行的匿名函数,不过其返回值仍然是一个匿名函数,也就是说函数自运行后返回的结果仍然是一个函数。把返回的函数赋值给log变量,就可以使用log变量来调用返回的函数了(注意与前面所介绍的函数表达式创建函数的区别)。这里其实包含两块保存函数的内存,自运行的匿名函数本身有一块内存来保存,当碰到后面表示执行的小括号后就会自动执行,另外还有一块内存来保存所返回的函数,而返回的值其实是这块内存的地址,这样log变量指向了这块保存函数的内存,因此也可以使用log来调用此函数。
虽然JS表面看起来有很多复杂的东西,但只要理解了其本质(特别是内存模型)后就很简单了。
在ES2015中可以使用let来定义块级变量,这样定义的变量在块的外部不可以使用。例如,将前面例子中的result改用let来定义,在if语句块外面就无法使用result输出结果了。
2. if-else语句
if-else语句的作用是进行条件判断。当需要进行判断时,就要使用if语句来完成,其结构如下。
if(条件){ 语句块 }
当条件为true时,执行语句块里的相应内容,否则不执行,例如下面的例子。
function sayHello(lang){ var hello = "你好"; if(lang=="en-us"){ hello = "hello"; } return hello; } sayHello(); //你好 sayHello("en-us"); //hello
在sayHello方法中,如果传入值为“en-us”的lang参数,则会返回“hello”,否则会返回“你好”。
有些时候需要对多种情况进行判断,可以组合使用if-else语句,在else后边可以接着写if语句,表示对另外一种情况的判断,也可以不跟if语句,用来表示如果所有条件都不符合时执行的默认操作,例如,将前面的例子做如下修改。
function sayHello(lang){ var hello; if(lang=="en-us"){ hello = "hello"; }else if(lang=="zh-tw"){ hello = "妳好"; }else if(lang=="zh-hk"){ hello = "妳好"; }else{ hello = "你好"; }
return hello; }
这时sayHello方法就对“en-us”“zh-tw”和“zh-hk”三种情况做了判断,如果都不是就会执行最后的默认语句块,即将hello设置为“你好”。
if语句的条件还可以使用前面介绍过的逻辑操作符来对多个条件进行组合判断,例如用||表示或,用&&表示与,用!表示非,上面例子中的sayHello方法可以写成下面的形式。
function sayHello(lang){ var hello; if(lang=="en-us"){ hello = "hello"; }else if(lang=="zh-tw" || lang=="zh-hk"){ hello = "妳好"; }else{ hello = "你好"; } return hello; }
这个例子就把lang=="zh-tw"和lang=="zh-hk"两个条件合并到一起组成一个条件,当lang为“zh-tw”或者“zh-hk”的时候都会给hello赋值 “妳好”。
3. while语句
while语句和if语句的结构相同,都是一个条件和一个语句块,只是关键字不同,while语句的结构如下。
while(条件){ 语句块 }
while语句和if语句的不同之处在于:if语句如果条件成立的话会且只会执行一次语句块里的内容,而while语句会反复执行语句块里的内容,直到条件不成立为止。我们来看下面的例子。
var step = 5; function widthTo(obj, width){ var owidth = obj.width; var isAdd = width - owidth>0; while(owidth! =width){ if(isAdd){ owidth+=step; }else{ owidth-=step; } if(width-owidth<step || owidth-width<step){ owidth = width; }
sleep(50); //休眠50ms, JS自身并无此函数,这里只是为了说明问题 obj.width = owidth; } }
在这个例子中,widthTo方法的作用是将相应对象的width属性修改为指定大小,并且不是一次修改到位而是按指定步长逐次修改,具体修改时需要先判断是变大还是变小,然后再修改,每次修改等待50ms,这么做就可以给人一种简单动画的感觉。这里使用的就是while循环,当对象的width属性没有达到目标大小时就会一直向目标方向变化,只有达到目标大小时才会停止并继续向下执行。while语句也可以理解为执行多次if语句,例如,上面代码中的while语句相当于下面的语句。
if(owidth! =width){ 具体操作; } if(owidth! =width){ 具体操作; } if(owidth! =width){ 具体操作; } ……
当if语句块足够多的时候也可以完成与while语句相同的功能。当然,实际使用时没有这么用的,这里只是为了让大家更加清晰地理解while语句的原理。
4. do-while语句
do-while语句和while语句类似,只是while语句的条件判断在语句执行之前,而do-while语句的条件判断在语句执行之后,do-while语句的结构如下。
do{ 语句块 } while(条件)
在do-while语句中,每执行完一次语句之后进行一次条件判断,如果条件成立,就会循环执行,直到条件不成立为止。由于do-while语句是在执行语句之后判断,所以语句块至少会执行一次。我们来看下面的例子。
function getTopLeader(person){ var leader; do{ leader = person; person = leader.getLeader(); }while(person! =null); return leader; }
这个例子中,getTopLeader方法要获取TopLeader(最高领导)。要找最高领导很简单,随便找个人问他的领导是谁,如果他有领导,则继续问他的领导的领导是谁,直到有人说我没有领导,好,他就是最高领导,我们将他返回去就可以了。这里使用了do-while语句。do-while语句与while语句的区别是,do-while语句至少会执行一次语句块的内容。在上面的例子中,首先查询上级领导,然后判断是否为空,查询上级领导的操作至少会执行一次,因此我们使用了do-while语句。其实do-while语句也可以换成while语句来执行,只需要在执行前先执行一次语句块中的内容。例如,上面例子中的getTopLeader方法也可以写成下面的形式。
function getTopLeader(person){ var leader = person; person = leader.getLeader(); //在while前先执行了一次 while(person! =null){ leader = person; person = leader.getLeader(); } return leader; }
这两种写法的效果是完全相同的。
5. for语句
for语句也是一种循环语句,并且比while语句和do-while语句更加灵活。for语句的结构如下。
for(表达式1; 表达式2; 表达式3){ 语句块 }
for语句的执行步骤如下。
1)执行表达式1,一般用来定义循环变量。
2)判断表达式2是否为真,如果不为真则结束for语句块。
3)执行语句块。
4)执行表达式3,一般用于每次执行后修改循环变量的值。
5)跳转到第2步重新判断。
这里要特别注意表达式3的执行位置,可以通过下面的例子清晰地看到上述过程。
var k; for(var i=0, j=""; i<=1; i++, j=1, k=1){ console.log(i); console.log(typeof i); console.log(typeof j); console.log(typeof k);
console.log("----------------"); }
这里的表达式1和表达式3都通过使用逗号来并列使用多条语句。表达式1为var i=0, j="",定义了两个变量i和j, i是number类型,值为0, j为string类型,值为空字符串;表达式2为i<=1,判断i是否小于等于1;表达式3为i++, j=1, k=1,共三条语句,i加1并将j和k设置为1,这里的j由原来的string类型转换为number类型,并且初始化了k变量。执行后输出结果如下。
0 number string undefined ---------------- 1 number number number ----------------
从输出结果可以看出,在第一次执行语句块的时候(i为0)表达式1已经执行了,因此这时i为number类型,j为string类型,k还没有初始化。第一次执行完语句块之后会执行表达式3,这时将j变为number类型并将k初始化,所以第二次输出的j和k都是number类型了。第二次执行完语句块之后会再次执行表达式3(每次执行完语句块之后都会执行表达式3),这时会将i变为2,然后判断表达式2的时候就不符合条件了,也就不再循环。
我们再来看一个计算10的阶乘的例子。
var n=10, result; for(var i=1, result=1; i<=n; i++){ result*=i; }
这个例子中,首先定义了两个变量n和result, n用来指定要计算谁的阶乘,result用于保存计算的结果,然后使用for语句执行具体阶乘计算并将结果保存到result中。
for语句中包含表达式1、表达式2、表达式3,并且都可以为空,但是它们之间的分号不可以省略,如果不需要相应的表达式,可以不写内容但是不可省略分号,例如下面的代码。
var n=10, result= 1, i=1; for(; i<=n; ){ result*=i; i++; }
这个例子的执行结果和上一个例子的执行结果完全相同,只是将循环变量i的定义放到for语句前面并将i++放到语句块中。需要注意的是,虽然这时for语句中的表达式1和表达式3都不需要了,但是分号是不可以省略的。只有表达式2的这种情况可以简单地使用while循环来完成,例如上面的代码还可以写出如下形式。
var n=10, result= 1, i=1; while(i<=n){ result*=i; i++; }
从这里就可以看出for循环的功能是最强大的,而while和do-while循环在特定条件下的使用方式会比较简单。
6. for-in语句
for-in语句可以遍历对象的属性,准确来说是遍历对象中可以遍历的属性(对象的属性是否可遍历会在后面详细讲解)。for-in语句的结构如下。
for(属性名in对象){ 语句块 }
for-in语句在遍历过程中直接获取的是属性的名称,可以使用方括号来获取属性的值,例如下面的例子。
var obj = {n:1, b:true, s:"hi"}; for(var propName in obj){ console.log(propName + ":" + obj[propName]); }
执行后输出结果如下。
n:1 b:true s:hi
ES2015中新增了for-of语句,它可以直接获取属性的值,后面再详细介绍。
7. continue语句
continue用于循环语句块中,作用是跳过本次循环,进行下一次循环语句块的执行(即跳过循环体中未执行的语句),在进入下一次执行之前也会判断条件是否成立,如果条件不成立就会结束循环。另外,如果是for语句,那么在执行判断前还会先执行表达式3。请看下面的例子。
var array = ["a", "b"]; array[3] = "c"; for(var i=0; i<array.length; i++){ console.log("enter block") if(! array[i]){
continue; } console.log(i+"->"+array[i]); }
在此例中,首先定义了一个数组array,并对其进行初始化。需要注意的是,数组的第一个元素的编号是0,因此array数组的a元素和b元素分别对应编号0和1。然后又给编号为3的元素赋值c,这时array数组的编号为2的元素是没有内容的。array数组初始化完之后,使用for循环遍历其中的元素并输出,在输出之前判断是否存在,如果不存在,则使用continue语句跳过本次执行(输出),进入下一次循环,最后的执行结果如下。
enter block 0->a enter block 1->b enter block enter block 3->c
当i为2时进入循环语句块,这时会输出“enter block”,但是当判断到array[2]不存在时,就会跳过而不执行输出语句,然后接着执行i++,判断条件并进入i=3时语句块的执行,这时又会输出“enter block”,因此输出的结果中有两次连续的“enter block”。
8. break语句
break语句可以用在循环中,也可以用在switch语句中。对于switch语句中break的用法,我们放到switch语句中讲解。在循环语句中,break的作用是跳出循环,它跟continue的区别是break会直接结束循环,而continue只是跳过本次语句块的执行而不会结束循环。将上一小节中的continue改为break,代码如下。
var array = ["a", "b"]; array[3] = "c"; for(var i=0; i<array.length; i++){ console.log("enter into block") if(! array[i]){ break; } console.log(i+"->"+array[i]); }
这时控制台输出的结果如下。
enter into block 0->a enter into block 1->b enter into block
当i=2时会结束循环,所以编号为3的元素就不会被打印。
9. return语句
return语句在函数中的作用是结束函数并返回结果。返回的结果跟在return语句后面,使用空格分开,例如下面的add函数。
function add(a, b){ var c = a+b; return c; }
add函数将两个参数a、b的值相加后赋值给内部变量c并返回。return语句除了可以返回变量外,还可以直接返回表达式,即上述代码中的add函数可以不定义c变量而直接返回a+b,形式如下。
function add(a, b){ return a+b; }
return除了返回结果外,还经常用于结束函数的执行,例如下面的代码。
function setColor(obj, color){ if(typeof obj ! = "object"){ return; } if(color! ="red" && color! ="green" && color! ="blue"){ return; } obj.color = color; }
这个例子中,setColor函数的作用是将obj的color属性设置为指定的值。首先判断obj是不是object类型,如果不是则不进行操作而直接返回,然后判断color是不是red、green、blue中的一种,如果不是也不进行操作而直接返回,最后将obj的color属性设置为传入的color。这里的return语句的主要功能是结束函数执行。
10. with语句
JS是一种面向对象的语言,对象主要是通过其属性来使用,如果需要对同一个对象的多个属性多次进行操作,就需要多次写对象的名称,例如下面的操作。
$("#cur").css("color", "yellow"); $("#cur").css("backgroundColor", "red");
这个例子中重复使用了$("#cur"),这时就可以使用with语句。
with ($("#cur")){ css("color", "yellow"); css("backgroundColor", "red"); }
这两种写法的作用是相同的,只是将所要操作的对象$("#cur")统一放到with语句的小括号中。with语句的作用就是指定所操作的对象,但是由于它会影响运行速度且不利于优化,所以不建议使用,并且在strict model(严格模式)下已经禁用了with语句。
11. switch-case-default语句
switch语句用于对指定变量进行分类处理,不同的类型使用不同的case语句进行区分,例如下面的例子。
var grade = "优"; switch (grade){ case "不及格": console.log("低于60分"); break; case "及格": console.log("60到75分"); break; case "良": console.log("75到90分"); break; case "优": console.log("90分(含)以上"); break; }
上述代码根据不同的等级输出相应分数的范围,等级写在圆括号中,每种类型的值写到case后面。需要注意的是,case和值之间由空格分隔,值后面有冒号,而且每一种情况结束后都要使用break语句跳出,否则会接着执行下一种类型的相应语句。
另外,在switch语句中经常会用到default语句,用于在所有case都不符合条件时执行。default语句应放在所有case语句之后,例如下面的例子。
var grade = "良好"; switch (grade){ case "不及格": console.log("低于60分"); break; case "及格": console.log("60到75分"); break; case "良": console.log("75到90分"); break; case "优": console.log("90分(含)以上"); break; default : console.log("没有这种等级"); }
上面的代码中,grade的值为“良好”,而“良好”在所有的case中都没有,这时就会执行default并打印“没有这种等级”。
switch和if-else语句的区别是,switch语句只是对单一的变量进行分类处理,而if-else可以在不同的判断条件中对不同的变量进行判断。因此,if-else语句更加灵活,switch语句更加简单、清晰。
12. try-catch-finally语句
try语句用于处理异常,其结构如下。
try{ 正常语句块 } catch (error) { 异常处理语句块 } finally { 最后执行语句 }
当“正常语句块”在执行过程中发生错误时,就会执行“异常处理语句块”,而无论是否抛出异常都会执行“最后执行语句”。我们来看下面的例子。
function getMessage(person){ try{ var result = person.name+", "+person.isEngineer(); }catch (error){ console.log(error.name+":"+error.message); result = "处理异常"; }finally{ return result; } } var msg = getMessage({name:"张三"}); console.log(msg);
这个例子中,getMessage方法中将传入person的name属性和isEngineer()方法的返回值连接到一起并返回,连接过程在try正常语句块中。如果在处理过程中遇到异常,就会执行catch的异常处理语句块。这里的异常处理语句块在控制台打印出异常信息并将“处理异常”赋给返回值,最后在finally语句中将结果返回,无论拼接过程是否发生了异常,最终都会返回result。在最后的调用语句中,传入的对象只有name属性而没有isEngineer方法,此时,执行就会抛出异常,进而会执行异常处理语句块,打印异常信息并将“处理异常”赋值给返回值。上述代码的执行结果如下。
TypeError:person.isEngineer is not a function 处理异常
第一行是在异常处理语句块中打印的,第二行是getMessage调用完后的返回值,是最后一行的代码打印的。在实际使用中,应该在捕获到异常后,返回事先指定好的一个值,而不是直接返回一个字符串,例如可以为getMessage对象定义一个专门用来表示异常的返回值属性。
getMessage.errorMsg="处理异常";
这样在调用getMessage函数之后,就可以使用它来判断是否正确执行。例如,可以按照如下形式使用。
var msg = getMessage({name:"张三"}); if(msg === getMessage.errorMsg){ //getMessage方法执行异常后的操作 }else{ console.log(msg); }
另外,如果没有必须执行的语句,那么也可以省略finally语句块。
13. throw语句
throw用于主动抛出异常。JS中throw抛出的异常可以是任何类型的,而且可以使用try-catch语句进行捕获,catch捕获到的就是throw所抛出的,例如下面的例子。
function add5(n){ if(typeof n ! = "number") throw "不是数字怎么加"; return n+5; } try{ add5("a"); }catch (error){ console.log(error); }
在这个例子中,add5方法的作用是将传入的参数加5后返回。在运算之前先判断传入的参数是否为数字类型,如果不是则抛出异常,异常信息为“不是数字怎么加”,抛出异常后就不再往下执行了。在调用时因为传入了非数字的“a”,所以会抛出异常,异常信息可以通过catch来捕获并输出到控制台,代码执行后控制台会打印“不是数字怎么加”。当然,如果想和其他JS异常使用统一的异常处理结构,那么抛出的异常也可以封装为包含name和message属性的对象,例如上面的例子可以写成如下形式。
function add5(n){ if(typeof n ! = "number") throw {name:"类型错误", message:"不是数字怎么加"}; return n+5; } try{ add5("a"); }catch (error){ console.log(error.name+":"+error.message); }
14. typeof语句
typeof语句的作用是获取变量的类型,调用语法如下。
typeof变量
typeof在ES2015中的返回值一共有7种:undefined、function、object、boolean、number、string和symbol,请看下面的例子。
var a; console.log(typeof undefined); //undefined console.log(typeof a); //undefined console.log(typeof b); //undefined console.log(typeof null); //object console.log(typeof function(){}); //function console.log(typeof {}); //object console.log(typeof true); //boolean console.log(typeof 123.7); //number console.log(typeof "str"); //string console.log(typeof Symbol("abc")); //symbol console.log(typeof [1,2,3]); //object
需要特别注意的是,null和数组的类型都是object,因为null本身也是一个对象,而数组中可以包含其他任何类型的元素,它并不是底层对象,所以它们没有自己独有的类型。
undefined是一种特殊的类型,它所代表的是这么一种类型:它们有名字,但是不知道自己是什么类型。即只要有名字但是没有赋值的变量都是undefined类型。对于上述例子中的a和b来说,虽然a定义了,b没有定义,但是它们都属于“有名字没值”的变量,因此它们都是undefined类型。如果用之前学习过的内存模型来说,undefined类型的对象就是只占用一块内存空间(用来保存变量名字)的对象。
另外,symbol类型是ES2015中新增的内容,本书会在后面给大家详细介绍。
15. instanceof语句
instanceof语句比typeof语句更进了一步,可以判断一个对象是不是某种类型的实例。就好像周围的东西可以分为动物、植物、空气、水、金属等大的类型,而动物、植物以及金属还可以再进行更细的分类。typeof的作用就是查看一样东西属于什么大的类型,例如是动物还是植物,而instanceof可以判断东西的具体类型,例如,如果是动物的话,可以判断是不是人,如果是人还可以判断是黄种人还是白种人、黑种人等。instanceof语句的结构如下。
obj instanceof TypeObject
instanceof语句的返回值为布尔类型,表示判断是否正确。例如,下面的例子使用instanceof来判断arr变量是否为数组类型。
var arr = [1,2,3]; console.log(arr instanceof Array); //true
后面将会学到function类型的对象,可以使用new关键字来创建object类型的对象,那时就可以使用instanceof来判断一个变量是否为指定类型的function对象创建的。
4.3.4 变量作用域
这里所说的变量作用域主要是指使用var定义的变量的作用域,这种变量的作用域是function级的,这一点我们在前面讲var语句的时候已经给大家介绍过了。JS中的function是可以嵌套使用的,嵌套的function中的变量的作用域又是怎样的呢?请先看个例子。
var v=0; function f1(){ var v=1; function f2(){ console.log(v); } f2(); } f1(); //1
这个例子中,定义了全局变量v,在函数f1中定义了局部变量v, f2定义在f1函数中,当调用函数f1时,会在其内部调用函数f2, f2中用到了变量v,这时v会使用f1函数中定义的v。
在调用嵌套函数时,引擎会根据嵌套的层次自动创建一个参数作用域链,然后将各层次函数所定义的变量从外到内依次存放到作用域链中。例如,在上述示例中,执行函数f2时,会首先将全局对象(浏览器中指页面本身,也就是Window对象)放在最下层,然后放f1,最后放f2。可以在f2函数中加入断点,然后使用FireBug清楚地看到这一点,加入断点后,执行代码时FireBug中的结果如图4-7所示。
图4-7 加入断点后,执行代码时FireBug中的显示结果
用到变量的时候会在变量作用域链中从上往下查找变量的值。例如,在上述示例中会先在f2中查找,如果找不到就会去f1查找,如果还查找不到就会到全局变量中查找。从图4-1中可以看到f2中没有v变量,因此会向下查找f1,在f1可以找到变量v,找到后就会使用f1中定义的变量v,最后会将“1”打印到控制台。
再来看下面的例子。
var x= 0, y= 0, z= 0, w=0; function f1(){ var y=1, z=1, w=1; function f2(){ var z= 2, w=2; function f3(){ var w=3; console.log(x); console.log(y); console.log(z); console.log(w); } f3(); } f2(); } f1();
这个例子最终会输出什么呢?
这个例子中,每个变量查找的顺序都是f3→f2→f1→全局变量。对于x来说,会一直到全局变量才能找到,y会在f1中找到,z会在f2中找到,w将直接使用f3自己定义的局部变量,所以最后的输出结果如下。
0 1 2 3
多知道点
调用子函数与嵌套函数中变量查找的区别
在变量作用域中很容易混淆子函数和嵌套函数中变量的查找过程。对于嵌套函数中的变量,会按照函数嵌套的层次在作用域链中从上往下查找,而调用子函数时并不会使用父函数中的变量,例如下面的例子。
var v=0; function logV(){
console.log(v); } function f(){ var v=1; logV(); } f();
在上述示例中,在函数f中调用了子函数logV,在logV中打印了变量v,这时v会使用全局变量而不会使用f中的局部变量,最后会打印出0,这是因为在调用logV函数时又会创建新的作用域链,新建的作用域链只包含logV函数嵌套定义的层级而不会包含调用时的f函数。也就是说,这个例子中有两套独立的作用域链,调用f函数时有一套,在f中调用logV函数时有另外一套,它们都只有两层,第一层为全局变量,第二层为函数自身,在logV函数中会使用全局变量v而不会使用f函数中的v(因为f函数根本不在logV函数调用的作用域链中)。
4.3.5 闭包
闭包是JS中非常重要且对于新手来说又不容易理解的一个概念。4.3.4节介绍了JS中变量是function级作用域,也就是说,在function中定义的变量可以在function内部(包括内部定义的嵌套function中)使用,而在function外部是无法使用的。但是,本书之前介绍过,函数就是一块保存了现有数据的内存,只要找到这块内存就可以对其进行调用。因此,如果想办法获取到内部定义的嵌套函数,那样不就可以在外部使用嵌套函数来调用内部定义的局部变量了吗?这种用法就是闭包,例如下面这个例子。
function f1(){ var v=1; function f2(){ console.log(v); } return f2; } var f = f1(); f(); //1
这个例子中,函数f1中定义了变量v,正常情况下在f1外面是无法访问v的,但是f1中嵌套定义的函数f2是可以访问v的,而且在调用f1时会返回函数f2,这样就可以在f1外部访问f1的局部变量v,这就是闭包。当然,如果需要还可以在f2中直接返回v的值,这样就可以在f1外部获取v的值。
需要注意的是,在使用闭包时,在保存返回函数的变量失效之前定义闭包的function会一直保存在内存中,例如下面的例子。
function f1(){ var v=1; function f2(){ console.log(v++); } return f2; } var f = f1(); f(); //1 f(); //2 f(); //3
这个例子中,f2函数在打印出v的值后又将v的值加了1,连续调用f函数时会在控制台依次打印出1 2 3,这说明f1函数一直在内存中保存着。这是因为保存f1返回嵌套函数f2的变量f是全局变量,它会一直保存在内存中,而f所指向的f2函数在执行时需要依赖f1,所以f1就会一直保存在内存中。这里需要注意f2本身因为没有被依赖,所以f2并不会一直保存在内存中,通过下面的例子可以清楚地看到这一点。
function f1(){ var v=1; function f2(){ var v1 = 1; console.log(v+", "+v1); v++; v1++; } return f2; } var f = f1(); f(); //1,1 f(); //2,1 f(); //3,1
从上面的例子可以看出,f1中定义的变量v在每次调用时会累加,这说明每次调用时使用的都是原来的数据,而f2中定义的变量v1则在每次调用时都会创建新的数据。其原理其实非常简单,在函数f1执行时会创建一套f1的变量数组,在函数f2执行时会创建另外一套f2的变量数组。按照JS中变量作用域链的规则,在f2中可以调用执行f1时所创建的变量数组,为了f2可以正确执行,只要在f2还可能被调用的时候执行f1时所创建的执行环境(包括变量数组)就不会被释放,因此,f1中定义的变量v会使用同一个,而f2每次执行完之后所创建的执行环境就没用了,会被释放,而在下次执行时又会创建新的执行环境。