4.1 声明语句

声明语句也叫变量语句,这种语句会创建新变量。可以在声明变量时给出初始值,如果没有明确给出,变量的值就是undefined。

        var count = 0;
        var dogs = ["Sparky","Spot","Spike"];
        var response;
        var melta = {latitude:35.8,longitude:14.6};
        var finished = false;
        var winner = null;

这段脚本明确给出了几个变量的初始值,事实上,除response之外都有了初始值。这时候,变量response的值就是undefined
可以在一个语句中声明多个变量,可以给部分变量赋初始值。

        var start = 0,finish = 72,input,output = [];

这个语句会将input初始化为undefined值。


4.2 表达式语句

表达式语句对表达式求值但,忽略得到的值。这听起来有点傻,因为可以编写出类似于下面的无用脚本;

        2+2;                    // 计算2+2,仅此而已
        "hello";                // 也有效,但完全没用
        Math.sqrt(100);         // 完成计算,也仅此而已
        "a".toLowerCase();      // 得到一个新字符串,但忽略它

但是可以借助表达式的副作用来创建有用的语句,所谓"副作用",就是一些操作,可以生成可见结果、改变变量或对象属性中保存的值,或者修改对象的结构等。之前的delete运算符与alert和赋值一样,都会产生副作用

        var x = 2;                // 声明x,初始化为2
        alert(x);                // 显示2
        alert(10*x);            // 显示20
        var y;                    // 声明y,但不明确赋值
        alert(y);                // 显示undefined

记住,变量声明和赋值就是简单地把一个值放到一个变量里,仅此而已。除此之外,没有任何其他后果,比如不会设定变量与变量之间的关系。在前面的脚本中,声明var z = y不会让z和y成为同一个变量,也不会要求它们在之后始保存相同的值。这个语句意思仅仅是"新建一个变量z,让它的初始值等于y的当前值"。之后再给y重新赋一个新值时,z的值不会变。


4.3 条件执行

JavaScript提供了一些语句和几个运算符,可以让我们编写的脚本在特定条件下执行一操作,在不同条件下则执行另一组操作。

4.3.1 if语句

if语句会根据你提供的条件,最多执行许多候选操作中的一种。这个语句的最一般形式是有一个if部分,0个或多个else if部分,还可根据需要带有else部分。每个候选项都是大括号中的语句序列。这些条件会自上而下一次评估。只要一个条件为真,就会执行它对应的候选操作,然后结束整个if语句。

        var score = Math.round(Math.random()*100);
        if (score>=90) {
            grade = "A";
        } else if (score>=80) {
            grade = "B";
        } else if (score>=70) {
            grade = "C";
        } else if (score>=60) {
            grade = "D";
        } else {
            grade = "F";
        };
        alert(score+" is a "+grade)

下图用一种UMKL(Unified Modeling Language,统一建模语言)活动图解读了上述语句。UML是一种非常流行的软件系统可视化语言。这张图传达的事实就是:会逐个检测每个条件,一旦发现一个条件成立,就不再进行测试。
图片描述

练习

  • 编写一个if语句,当变量s没有包含长度为16的字符串时,提示一条消息(注意,这个语句没有else ifelse部分)

        var s = prompt("Enter a something");
        if (s.length!==16) {
            alert("string length isn't 16")
        };
  • 编写一个if语句,检查变量x的值,(1)如果x>0,则将它加到数组变量values的末尾;(2)如果x<0,则将它加到values的前面;(3)如果x===0,则递增变量zeroCount;(4)柔则,什么也不做。

        var values = [];
        var zeroCount = 0;
        var x = Number(prompt("Enter a number"));
        if (x>0) {
            values.push(x);
        } else if (x<0) {
            values.unshift(x);
        } else if (x===0) {
            zeroCount++;
        };

4.3.2 ?:条件表达式

有时你可能更喜欢使用条件表达式,而不是if语句。条件表达式就是"x?y:z",当x为真时,表达式的结果为y,当x为假时,表达式的结果为z。

        if (latitude>=0) {
            hemisphere = "north";
        } else {
            hemisphere = "south";
        }
        // 写为:
        hemisphere = (latitude>=0) ? "north" : "south";

条件表达式有时会用在构建字符串的表达式中,如下(这里的present假定是一个保存布尔值的变量:)

        var notice = "She is "+(present? "" : "n't")+" here.";

4.3.3 switch语句

另一种条件语句 —— switch语句,将一个值一系列case进行比较,知道找出一个与它的值相等(===)的case,然后从该处开始执行语句。可以根据需要包含一个default case,它会匹配所有值。

        switch (direction.toLowerCase()){
            case "north" : row -= 1;
                break;
            case "south" : row += 1;
                break;
            case "east"  : column += 1;
                break;
            case "west"  : column -= 1;
                break;
            default: alert("Illegal direction");
        }

break语句会终止整个switch语句。每种case都以一个break结束,这是一种很好的做法否则,执行会"直落"到下一个case(不会再与剩下的case表达式进行比较)。例如,上面的脚本我们省略了break,而且方向是"east",会发生什么情况呢?column中的值会被递增,然后递减,然后弹出一个提示框,告诉你"east"不是一个合法方向。

有时,"直落"正是我们想要的。假定在一群参加竞赛的人中,每个人都获得一个证书,但达到一级奖励的人还会得到一个背包,达到二级奖励的人还会得到一次滑雪度假,而达到三级奖励的人还会奖励一俩汽车。这种情况就可以使用没有break语句的编码。

        /*
         * 这个脚本会为一个给定奖励级别生别一组奖励
         * 每个奖励级别的参赛者会得到该级奖励和所有
         * 低级奖励。它使用了一种很丑陋的switch语句
         * 形式,其中的各个case之间没有相互距离。
        */
        
        var level = +prompt("Enter your prize level,1-3");
        var prizes = [];
        switch (level){
            case 3 : prizes.push("car");
            case 2 : prizes.push("ski vacation");
            case 1 : prizes.push("backpack");
            default: prizes.push("certificate");
        }
        alert(prizes);

‘’

一个具有直落效果的switch语句的活动图

像这样的计算很少会在现实中发生所以一般来说,还是避免使用那些不以break(或其他某一中断语句)结束的case,以防与switch更常见的形式混淆。事实上,之前介绍的JSLint代码质量检查工具就是将"直落"看作错误!我们总能为直落switch找出替代方法。


4.3.4 用查询避免条件代码

来看一种情景,至少对于许多程序员新手来说,这种情景似乎是需要使用if语句,?:运算符或者switch语句。假定有一个变量state,保存德国一个州名,我们希望将这个州的首府名赋值给变量capital,如果不能识别州名,则为其赋值undefined

        // 丑陋的代码 —— 不要使用这一代码
        if (state === "Baden-Wurttemberg") {
            capital = "Strttgart";
        } else if (state = "Bayern") {
            capital = "Munchen";
        } else if (state = "Berlin") {
            capital = "Berlin";
        } else if (state = "Potsdam") {
            capital = "Potsdam";
        } else if (state = "Bremen") {
            capital = "Bremen";
        } else if (state = "Hamburg") {
            capital = "Hessen";
        } else {
            capital = undefined;
        }

此代码对每个州都重复两段内容:state ===capital = 。switch语句可以消除前者,但必须为每个州都增加一个break —— 没有实质性的改进。

        // 丑陋的代码 —— 不要使用这一代码
        switch (state){
            case "Baden-Wurttemberg" : capital = "Stuttgart";
                break;
            case "Bayern" : capital = "Munchen";
                break;
            case "Berlin" : capital = "Berlin";
                break;
            case "Brandenburg" : capital = "Potsdam";
                break;
            case "Bremen" : capital = "Bremen";
                break;
            case "Hamburg" : capital = "Hambrug";
                break;
            case "Hessen" : capital = "Wiesbaden"
                break;
            default:capital = undefined;
        }

我们将在第三次尝试中使用的条件表达式可以消除赋值的重复作用,但无法消除相等检测中的重复。

    capital = 
          (state === "Baden-Wurttemberg") ? "Stuttgart"
        : (state === "Bayern") ? "Munchen"
        : (state === "Berlin") ? "Berlin"
        : (state === "Brandenburg") ? "Potsdam"
        : (state === "Bremen") ? "Bremen"
        : (state === "Hamburg") ? "Hamburg"
        : (state === "Hessen") ? "Wiesbaden"
        : undefined;

我们不能将所有重复片段排除在外,这一定意味着存在一种更好的方法来查找首府,事实上也的确存在。如果剔除了所有关于测试与赋值的内容,还剩下什么呢?就是州和首府!我们并不需要什么精确的计算来将州和首府关联在一起,可以在数据而非代码中定义这种关系。我们所需要的就是一个简单对象。

        var CAPITALS = {
            "Baden-Wurttemberg" : "Stuttgart",
            "Bayern" : "Munchen",
            "Brandenburg" : "Potsdam",
            "Bremen" : "Bremen",
            "Hamburg" : "Hamburg",
            "Hessen" : "Wiesbaden"
        };

现在,只需写出:

        capital = CAPITALS[state];

就能获得变量state中所包含州的首府。可以认为这一代码是在一个由州及首府组成的表格中查询首府。在这样使用一个对象时,就说它是一个查询表、一个词典、一个映射、一个关联数组、或一个数列查询表通常是将键映射到值key = value)。这里的州是键,首府是值。在适用时,查询代码要优于条件代码,原因有如下几个。

  • 没有冗余的代码片段,是脚本更短、更易于理解。

  • 州和首府的显示位置紧挨在一起,更容易"看出"它们之间的关联。

  • 代码(查询)和数据(查询表)在本质上是不同的东西,不应当混放在一起。

  • JavaScript引擎执行查询的速度要远快与条件代码。

还有一个例子,我们使用词典来关联手机键盘上的数字和字母。
为了将带有字母的电话号码转换为号码,我们可以使用:

        var LETTER_TO_NUMBER = {
            A:2,
            B:2,
            C:2,
            D:3,
            E:3,
            F:3,
            G:4,
            H:4,
            I:4,
            J:5,
            K:5,
            L:5,
            M:6,
            N:6,
            O:6,
            P:7,
            Q:7,
            R:7,
            S:7,
            T:8,
            U:8,
            V:8,
            W:9,
            X:9,
            Y:9,
            Z:9
        };

比如LETTER TO NUMBER["C"] === 2。能不能在另一个方向上使用这个映射呢?单个数字可以映射为多个字母,所以我们将使用字符串:

        var NUMBER_TO_LETTER = {
            2:"ABC",
            3:"DEF",
            4:"GHI",
            5:"JKL",
            6:"MNO",
            7:"PQRS",
            8:"TUV",
            9:"WXYZ"
        };

这里,我们说:“给定一个数字n,NUMBER TO LETTER[n]”的值是一个字符串,其中包含了与键盘上的n相关联的所有字母(而且也只有这些字母)。”在后续介绍处理整个手机号码脚本时间,将会用到这些词典中的第一个。


练习

  • 用你自己的语言,说明可以用查询代替条件代码结构的情景(为之前的分数与成绩示例使用查询策略)

        var score = {
        100:"A",
        99:"A",
        98:"A",
        80:"B",
        85:"B",
        89:"B",
    };
    alert(score[100])
    

    情景:查询电话归属地、邮编。元素周期表......

  • 假定你注意到某一代码中存在表达式CAPITALS[Saarland]。几乎可以肯定它有什么错误?

    1. 可能存在语法错误,方括号运算符中如果不是变量,必须用引号括起


4.3.5 短路执行

之前的布尔值一章中,如果x和y都是布尔值,则AND-ALSO和OR-ELSE运算符具有以下特性:

  • 当且仅当xy为真(或均为假)时,x || y为真

  • 当且仅当xy均为真时,x && y为真

        if (t<0 && t>100) {
            /**执行某些操作**/
        }
        // 与下面代码的含义完全相同
        if (t<0) {
            if (t>100) {
                /**执行某些操作**/
            }
        }

如果第一部分的求值已经获得了足够多的信息,那就讲第二部分的求值短路,也就是跳过。
我们现在明白AND-ALSO和OR-ELSE名字的缘由了。

  • x and also y就是说:若(if)x为真,则(then)(且仅在此时)去查看y是否也(also)为真。

  • x or else y 就是说:若(if)x为真,那很好;否则(else)必须去查看y是否为真。

    第二部分根据条件决定是否求值。

短路运算符有一个很重要的功能:它们并不真的需要布尔值操作数,注意:

        alert(27 && 52);        // 52
        alert(0 && 52);            // 0
        alert(27 || 52);        // 27
        alert(0 || 52);            // 52

换句话说,JavaScript不会将数字转换为布尔值。

  • 为计算x && y的值,JavaScript首先对x求值如果x为假(false或可以转换为false),则整个表达式得出x的值(不再计算y。否则整个表达式得出y的值。

  • 为计算x || y的值,JavaScript首先对x求值。如果x为真(true或可以转换为true),则整个表达式得出x的值(不再计算y。否则,整个表达式得出y的值。

    利用这一行为,可以编写一些小巧的代码。假定有一个变量favoriteColor,我们知道,如果你有最喜欢的颜色,这个变量就包含了这种颜色的名称,如果没有最爱的颜色,那它的值就是undefined。如果有喜欢的颜色,就用这种颜色为汽车喷漆,如果没有,则喷为黑色;

            car.color = favoriteColor || "black";

    这一行代码是有效的,因为favoriteColor中包含了一个非空字符串,它为真,所以会为其指定car的颜色。如果favoriteColorundefined为假),则根据定义,||表达式的值就是它的第二个(右侧)操作数,也就是黑色。

练习

  • 对表达式 3 && "xyz" || null求值。

    1. 首先对3 && "xyz"求值,3转换为true,继续对右边表达式求值,为true,造成 ||短路,不对null进行计算,返回"xyz"

  • 对表达式3 && "" || null求值。

    1. 首先对3 && ""求值计算,空字符串转换为false,而因||需要,继续对右边表达式求值,返回null

  • 判断正误:对于变量xyx && y总是等同于x ? y : x

    1. 正确

    2. 首先对x求值,判断是否短路,x为真,求值yx为假,那只有x已求值

  • 判断正误:对于变量xyx || y总是等同于x ? x : y

    1. 正确

    2. 首先对x求值,判断是否短路,x为真,短路并返回,x为假,继续求值y


4.4 迭代

上一节研究了不同情况下做不同事情的方法。现在来看一遍又一遍地重复同一事情的方法。

4.4.1 whiledo-while语句

JavaScript的while语句会在条件为真时重复执行代码。下面的例子要求用户输入一个恰有五个字符的字符串,并一直询问,知道用户遵守指令为止。当用户输入不满足要求时,脚本会计算尝试次数(并重复提示)。只有在输入了可接受的字符串时,while语句才会结束。

        var numberOfTries = 1;
        while (prompt("Enter a 5-charater string").length !== 5){
            numberOfTries += 1;
        }
        if (numberOfTries>1) {
            alert("Took you "+numberOfTries+" tries to get this right");
        }

while语句的一般形式为:

while (test) { stmts }

首先执行第一个test,如果它为真,则执行循环体,重复整个语句。这就是说,条件测试出现在循环体之前,循环体可能一次也不会执行。而在do-while语句中国,循环体至少会执行一次。一般形式为:

do {stmts} while (test)

可以用do-while语句重写以上脚本:

        var numberOfTries = 0;
        do {
            var input = prompt("Enter a 5-character string");
            numberOfTries++;
        } while (input.length!==5);
        if (numberOfTries>1) {
            alert("Took you "+numberOfTries+" tries to get this right");
        }

图片描述


4.2.2 for语句

第二种循环 —— for语句,也就是在条件为真时一直循环,但它通常用于迭代遍历一个固定的项目集,比如一个数值范围、一个字符串中的字符、一个数组的索引、一个对象链,等等。这个语句一般形式是:

        for (init ; test ; each) {stmts}

JavaScript引擎首先运行init代码,然后,只要test为真,则运行循环体,然后运行each。init部分通常会声明一或多个变量(但并非总是如此)。

        // 显示4,6,8,10,...,20为偶数
        for (var Number=4;Number<=20;Number+=2) {
            alert(Number+" is even ")
        }

这里number被初始化为4,而且因为它小于等于20,所以将提示"4 is even",然后将number直接设置为6,接下来将显示"6 is even",number变为8。最后显示20,因为最后number将跳到22,条件为false。
在处理数组时会使用for语句。下面例子中,处理一个单词列表,也就是字符串words[0]、words[1]、words[2],等等。如何"逐步遍历"这个列表呢?可以创建一个变量i,它会在每次的迭代中递增1.

        // 显示一个字符串,又每个数组项目的首个字母组成
        var words = ["as","far","as","i","know"];
        var result = "";
        for (var i=0;i<words.length;i++) {
            result += words[i].charAt(0).toUpperCase();
        }
        alert(result);
        // 为什么使用i,因为数组元素索引(index),于是使用其首字母,一个传统了。
        

下面是一个另一个与数组有关的例子,它演示了一种常见模式,用于检查每个数组元素,看它是否满足某一条件。 // 显示一个数组中的0的个数

        var a = [7,3,0,0,9,-5,2,1,0,1,7];
        var numberOfZeros = 0;
        for (var i=0,n=a.length;i<n;i++) {
            if (a[i]===0) {
                numberOfZeros++;
            }
        }
        alert(numberOfZeros);

注意这个示例还演示了如何在for循环的初始化部分声明两个变量。将n初始化为数组的长度,循环终止检测变得简单一点。
除了迭代数值范围和数组元素之外,for语句还经常用于迭代一个字符串中的各个字符。

  1. 如果字符是数字,则直接"将它传送"给结果字符串

  2. 如果字符在A~Z之间,则查找对应的手机键盘数字,并传送该数字。

  3. 如果字符是其他内容,则忽略它。

        var LETTER_TO_NUMBER = {A:2,B:2,C:2,D:3,E:3,F:3,G:4,H:4,I:4,J:5,K:5,L:5,M:6,N:6,O:6,P:7,Q:7,R:7,S:7,T:8,U:8,V:8,W:9,X:9,Y:9,Z:9};
        var phoneText = prompt("Enter a phone number (letters permitted)").toUpperCase(); 
        var result = "";
        for (var i=0;i<phoneText.length;i++) {
            var c = phoneText.charAt(i);
            if (/\d/.test(c)) {
                result += c;
            } else if (c in LETTER_TO_NUMBER) {
                result += LETTER_TO_NUMBER[c];
            }
        }
        alert("The phone number is: "+result);

在迭代另一类序列时也经常看到for循环:对象的链接结构

图片描述

        var scores = {
            score:29,
            next:{
                score:99,
                next:{
                    score:47,
                    next:null
                }
            }
        };
        var total = 0;
        for (var p=scores;p!=null;p=p.next) {
            total += p.score;
        };
        alert(total);
      

变量p引用了对象scores,并将p.score属性运算后赋值给变量total,并且每次执行完代码块后,p引用的对象的属性变为p.next.score,下一次就是p.netx.next.scorescores.next.scorescores.next.next.score

下一个例子,演示嵌套循环。

    var SIZE = 9    ;            
        document.write("<table border='1' cellspacing='0'>")
        for (var i=1;i<=SIZE;i++) {
            document.write("<tr>")
            for (var j=1;j<=SIZE;j++) {
                document.write("<td>"+i*j+"</td>");
            }
            document.write("<tr>")    
        }
        document.write("</table>")

图片描述

练习

  • 编写一个for语句,显示10,然后显示9,以此类推,知道最后显示0。

       for (var i=10;i>=0;i--) {
             console.log(i);
       }
    
        var i = 10;
        while (i!==-1){
            console.log(i);
            i--;
        }
    
  • 编写一个小脚本,计算从1至20(含)的整数乘积值。使用for语句。

            for (var i=1,n=1;i<21;i++) {
            n *= i;
            console.log(n);
            };    
                        

4.4.3 for-in语句

JavaScript包含一个迭代对象属性名的语句。这门语言将这一操作称为属性名的枚举。

    var dog = {
            name:"Lisichka",
            breed:"G-SHEP",
            birthday:"2011-12-01"
        };
        for (var p in dog) {
            alert(p)
        }
     // name breed birthday

这个脚本将生成三条提示:一个给出name,一个给出breed,一个给出birthday。这些属性的出现顺序是任意的。试试另一个对象:

        var colors = ["red","amber","green"];    
        for (var c in colors) {
            alert(c)
        }
        // 0 1 2

枚举是对属性名进行的,不是针对值。变量colors引用的对象的属性名是0、1、2和length。但是,在运行这一代码时,你只会看到显示了0,1,2。为什么没有length?
其实,一个对象的每个属性,除了拥有值以外,还有几个特性(attribute)。

        // 特性                      在为真时的含义
        
        writable                    属性的值可以修改
        enumerable                  这个属性将出现在属性的for-in枚举中
        configurable                可以从其对象中删除这个属性,它的特性值是可以改变的

凑巧,数组的length属性的enumerable特性被设置为false。由前面的例子知道,它的writable特性为true。

练习

  • 修改与狗有关的小脚本,提示以下内容:必须用for-in语句生成这些提示,并用object[property]符号提示属性值。

        var dog = {name:"Lisichka",breed:"G-SHEP",birthday:"2011-12-01"};
        for (var i in dog) {
            document.write("The dog's "+i+" is "+dog[i]+"<br />")
        }
        // dog[property]注意property是字符串类型,而不是变量。
    

4.5中断

通常,语句都是按顺序一次执行一条。条件语句和迭代语句会稍微偏离这种有时被称作直线式代码的形式,但这种偏离是语句结构本身决定的,很容易识别。但还四种语句的结构化不是这么强,他们对控制流的效果是中断性的。有这些:

  • break,立即放弃执行当前正在执行的switch或迭代语句;

  • continue,立即放弃一个迭代语句中当前迭代的剩余部分;

  • return,立即放弃当前执行的函数;

  • thorw,将在后面介绍;

4.5.1 breakcontinue

break语句将立即终止整个循环。在搜索数组(或对象链),这一语句特别有用,它会在你找到正在查找的内容之后立即终止搜索。

        // 查找第一个偶数的索引位置
        for (var i=0;i<array.length;i++) {
            if (array[i]%2===0) {
                alert("Even number found at position "+i);
                break;
            }                
        }

continue语句立即开始循环的下一次迭代,而不在完成当前迭代。当循环中的一些(而非全部)迭代生成有用信息时,这一语句非常有用。continue语句就是说“嗨,这次迭代里没有什么要做的事情了,我马上要开始下一次迭代了”。

        // 计算一个数组中所有正数值之和
        var array = [-1,3,12,3,-23,-23,-2,0,1,-12];    
            if (array[i]<=0) {
                continue;    //    跳过非正数
            }
            sum += array[i];
        }
        alert("Sumof positives is "+sum)

这里接一个链接开源中国问题,是一个社友提的一个问题。

图片描述

刚好我在MDN里找到了一些文档MDN-JS-break语句

图片描述

那位社友的问题在于,在while循环中,当i=4时,就continue跳过了自增操作,按照MDN的说法,它将回到条件继续执行,这便成了一个死循环。而在for循环就直接each更新表达式了。

接下来写一个脚本:用户输入一个数字,判断是否为质数。

        var SmallLest = 2;
        var BigGest = 9E15;
        var n = prompt("输入一个数字");
        
        var condition = isNaN(n) || n % 1 !== 0 || n < SmallLest || n > BigGest;
        if (condition) {
            alert("只能判断2~9e15之间的整数");
        } else {
            var foundDivisor = "是";     // default,是质数
            for (var k = 2,last = Math.sqrt(n);k <= last;k++) {
                if (n%k === 0) {        // 2 ~ Math.sqrt(n)之间任何一个数可以整除n,则不是质数
                    foundDivisor = "不是";    
                    break;
                }
            }
        }
        alert(n+" 是否为质数? "+" : "+foundDivisor);

最后一个关于break和continue的示例要研究一个问题:如何中断外层循环?一种方法是对循环进行标记,然后在break语句中提及这个标记。假定有一个对象纪录了一组人选择的彩票信息。

        var picks = {
            Alice:[4,52,9,1,30,2],
            Boris:[14,9,3,6,22,40],
            Chi:[51,53,48,21,17,8],
            Dinh:[1,2,3,4,5,6],
        };

另外,假定我们希望知道是否有人选择了数字53。可以依次查看每个人选择的数字,但只要找到53,就希望停止整个搜索,而不只是终止对当前人员选择数字的扫描。

        var picks = {
            Alice:[4,52,9,1,30,2],
            Boris:[14,9,3,6,22,40],
            Chi:[51,53,48,21,17,8],
            Dinh:[1,2,3,4,5,6],
        };
        var found = false;
        Search:for (var person in picks) {
            var choices = picks[person];
            for (var i=0;i<choices.length;i++) {
                if (choices[i]===53) {
                    found = true;
                    break Search;
                }
            }
        }

练习

  • 重写计算数组例子,改为使用break语句。

        // continue重写:计算一个数组所有正数值之和
        var num = [1,3,4,-2,-12,-3,0,-3,-1];
        var sum = 0;
        for (var i=0;i<num.length;i++) {
            if (num[i]<0) {
                break;    // break后,回到条件更新表达式,这里是自增
            };
            sum += num[i];
        }
        alert(sum)
    

4.5.2 异常

有时,运行脚本会出现一些问题,可能是因为编码错误导致,如希望用乘法时却编写了加法。在这些情况下,脚本会一直运行,但可能会给出错误的结果。我们说这种脚本带有bug。如果幸运(居然叫幸运 - -)的话,还是能看到输出结果,并会想到“这个结果不可能正确”,然后再去复查脚本,纠正错误。

但有时运行脚本,js遇到一个不能执行的语句,或者不能求值的表达式。这是,脚本就不能继续运行了。引擎会抛出一个异常。如果没有捕获这个异常,脚本会立即停止运行。我们称之为崩溃

        alert("Welcome to my script");
        var message = printer+1;
        alert("The script is now ending")

在脚本运行时,出现第一次提示,但由于第二条语句需要一个未声明变量printer的值,所以引擎会抛出异常。因为这一异常未被捕获,所以整个脚本都将被放弃,最后一条提示永远不会被执行。
除了使用未声明变量这种情况外,还有哪些错误会被看作错误,并导致JavaScript抛出异常呢?

  • 将数组长度设置为负值(RangeError

  • null值中读取属性(TypeError),因为只有对象拥有属性,JavaScript不能将null转换为对象

  • 执行不合乎JavaScript语法的代码,或对其求值(SyntaxError

        var a = [10,20,30];
        a.length = -5;            // "Uncaught RangeError: Invalid array length"
        
        var a = null;
        alert(a[3]);              // "Uncaught TypeError: Cannot read property '3' of null"

        alert(3/-)                // "Uncaught SyntaxError: Unexpected token )"

我们可以使用JavaScript的throw语句在自己的代码中显式的抛出异常。可以抛出自己想要的任何值;在下面的例子中,将抛出一个字符串:

        alert("Welcome to my script")
        throw "Ha ha ha";                // "Uncaught Ha ha ha"
        alert("You will never see this message")

我们提到,未捕获的异常会导致脚本崩溃。要捕获异常,可以使用try-catch语句。

        try {
            // 这是一个人为设计的示例,只说明了一个点
            alert("Welcome to my script");
            throw "Ha ha ha";
            alert("You will never see this script");
        } catch(e) {        
            alert("Caught : "+e);
        }

catch子句将一个变量(在本例中是e,这是一个相当常见的选择)初始化(或理解为将抛出的值赋值给e?)为被抛出的值。在实践中,许多JavaScript程序员都需要抛出带有各种属性的对象,用以提供一些信息,用以提供一些信息,来为其描述抛出此异常的问题。例如:

        throw {reason:"class full",limit:20,date:"2012-12-22"}    
        // "Uncaught #<Object>"

异常为特定的编程问题提供了非常自然的解决方案。只要你意识到,利用你当前拥有的数据,一个计算不能正常进行,那就应该抛出异常。
和大多数程序设计特性一样,异常也可能被滥用。下面这个脚本要求用户从三扇分别标有1,2,3的门中选择一扇,并赢得藏在这扇门之后的奖励。

        // 如果没有异常这将是更好的一个脚本
        try{
            var PRIZES = ["a new car","a broken stapler","a refrigerator"];
            var door = prompt("Enter a door number(1,2,or3)");
            var prize = PRIZES[door-1];
            alert("You have won "+prize.toUpperCase()+"!!");
        }catch(e){
            alert("Sorry, no such door.")
        }

如果用户输入除1,2,3之外的任何值,prize的值都将是undefined,对undefined调用toUpperCase将会抛出一个异常。这个异常被捕获,并报告一条错误。这一点很难通过研究代码来发现,因此,我们说这一脚本的逻辑有些费解。他依赖于我们调用toUpperCase这一事实,它与输入无效门牌号的"问题"没有什么关系!我们最后用一个简单的if语句立即核实输入。

练习

  • 在JavaScript中除以零是否会抛出异常?

    1. 正数除0结果为Infinity,负数除0为-Infinity,0除以0为NaN。

  • 重写关于三个奖励的例子,不使用异常

        var PRIZES = ["a new car","a broken stapler","a refrigerator"];
        var door = prompt("Enter a door number(1,2,or3)");
        if (door==1 || door==2 || door==3) {
            alert("You have won "+PRIZES[door-1]+"!!");
        } else {
            alert("Please Input number 1,2or3")
        }
        

4.6 应该避免的编码风格

我们已经给出了以下一般形式的ifwhiledo-whileforfor-in语句

        if (test) { stmts }
        if (test) { stmts } else { stmts }
        if (test) { stmts } else if (test) { stmts }
        if (test) { stmts } else if (test) { stmts } else { stmts }
        
        while (test) { stmts }
        do { stmts } while (test);
        for (init;test;each) { stmts }
        for (var variable in object) { stmts } 

但事实上,上面使用语句序列(放在大括号)的位置,JavaScript都允许使用单个语句,下面这种写法完全合法;

        if (count === 0) break;            
        
        // 或者
        
        if (count === 0)
            break;
            
        // 但不一定是最好的
        if (count === 0) {
            break;
        }

技术角度来说,任何一个放在大括号中的语句序列,其本身就是单条语句,成为块语句。因此,在任何需要使用单条语句的地方都可以使用块语句,但在实践中,如果只是为了使用块语句而使用块语句,看起来会很傻。下面的脚本虽然有点傻,确实合法的;

        {{{{alert("Hello");}}}}

强烈建议遵循现在编码约定,仅在if语句和迭代语句中使用块语句我们还强烈建议,应当始终使用块语句来构建这两种语句,哪怕简短形式可以减少键入工作。主要理由如下:

  • 如果代码中有些语句使用大括号,有些不使用,视觉上还会显得有些不协调。当在同一条if语句中,有些候选项带有大括号,而另一些没有时,看起来尤其糟糕。缺乏一致性会让代码显得不平衡、不整洁、需要花费大量不必要的精力来领会其意图。

  • 如果缺少大括号,在修改代码时更容易引入错误。这里有一个示范。

        // 下面的脚本显示数字0至9的平方
        for (var i=0;i<=10;i++) {
            alert(i+" squared is "+(i*i))
        };

程序员决定向循环体中添加一条语句,定义一个新变量,用来保存计算所得的平方,但却忘了添加大括号

        for (var i=0;i<10;i++)
            var square = i * i;
            alert(i+" squared is "+square);

因为for循环体总是跟在控制表达式(放在小括号)之后的单条语句,所以这个脚本声明了变量square,并重复为它赋值,最后一次为81.在for语句完成之后将出现提示。这时,i的值为10,所以整个脚本给出单条提示:10 squared is 81,如果养成了符合语句中使用大括号的习惯,就永远不会犯这样的错误。

  • 仅在if语句和迭代语句中使用块语句,而且在这两种语句中也总要使用块语句


4.6.2 隐式分号

官方的JavaScript定义声明,以下语句应当以分号(;)结束:变量声明语句、表达式语句(包括赋值)、do-while语句、continue语句、break、return语句和throw语句。
但是这门语言的设计者允许程序员根据自己的一元省略语句末尾的分号,依靠JavaScript引擎来指出哪个地方应当有分号。遗憾的是,这一规则降低了我们将长语句分跨在多行代码的灵活性。关于处理缺失分号的技巧细节,可在官文中找到。

4.6.3 隐式声明

当你尝试使用一个尚未声明的变量时,JavaScript引擎会抛出一个异常。但是,如果尝试为一个未声明的变量赋值,JavaScript会自动为你声明这个变量。

        // 假定变量next_song从来没有声明过
        next_song = "Purple Haze";

这个脚本不会抛出异常!许多人可能会说它应该抛出异常的,许多专家认为这个隐式声明是语言设计缺陷。在赋值中意外地错误拼写一个变量名,会导致一个新变量的声明,它不同与你本来想要赋值的变量。这个脚本将一直运行,只到发生了某些“跑偏道路”的事情,是错误很难查找。如果引擎在赋值时抛出异常,哪会更好一些,因为这个错误的侦测就很容易了。
千万不要依赖这一“功能“,JSLint很明智的将它的应用报告为错误。


4.6.4 递增和递减运算符

这里可能用到优先级表文档:MDNJavaScript运算符优先级

        var x = 5;
        x++;                // x:6
        var y = x++;        // y:6 x:7    等号右结合
        var z = ++x;        // x:8 z:8
        var w = ++y + z++;    // y:7,w:15,z:9
  • 等号结合性为右结合,故对=右侧开始计算,最后赋值给变量xx:5

  • 后置递增,先计算x(使用x),最后再进行自增。x:6

  • 出现的符号:=后置++,后置自增无结合性,则先赋值(使用x)y:6,后自增x:7

  • 出现的符号:=++前置,两者结合性都是右结合,则先自增x:8,后赋值z:8

  • 出现的符号:=前置+++后置++,加括号应该是:(var w = ((++y) + z))++,先对前置自增运算y:7,然先使用zy相加w:15,赋值给变量w,最后z才自增。所以y:7,w:15,z:9


Queen
139 声望20 粉丝