流程控制


逻辑分支

现在有一个新情况,学校期末考试,60 分及以上为及格,尝试编写一个函数,输入成绩判断是否及格。

此时遇到了一个流程分支的情况,当分数大于等于 60 的时候,我们提示及格,小于的时候,提示不及格。遇到这种情况,我们需要使用逻辑分支语句 if-else

function foo(score) {
  if (score >= 60) {
    console.log("及格");
  } else {
    console.log("不及格");
  }
}

foo(100);
foo(10);

上诉代码会依次打印 及格不及格,通过 if-else 语句,我们改变了程序的走向,上面代码中, >= 是一个关系运算符,表示大于等于,它用来判断两个数据的大小关系。score >= 60 是一个关系表达式,如果关系成立,那么关系表达式的值是布尔值 true,反之是布尔值 false

falsy 值

falsy 值代表假值,当一个判断的结果是 falsy 值时,判断不成立,它用于 if-else 语句的判断部分。falsy 值有以下几个:

描述
false布尔值 false
0数字 0,包括负零 -0
空字符串空字符串
nullNull 字面量 null
undefinedUndefined 字面量 undefined
NaN数字字面量 NaN

也就是说,如果 if-else 语句判断部分的表达式结果是以上几个值,那么就执行 else 后面的代码。

// 更换 0、""、null、undefined、NaN 表达式都不会成立
if (false) {
  console.log("成立");
} else {
  console.log("不成立", false);
}

if-else

if-else 语句的写法如下:

if (expression)
    // code 1
else
    // cdoe 2

圆括号内的 expression 是一个表达式,if-else 语句的判断部分能填写任何表达式:

  • 如果这个表达式的结果不是一个 falsy 值,那么执行 if 后面的代码。
  • 如果表达式的结果是一个 falsy 值,那么执行 else 后的代码。

如果条件中只有一条语句,那么花括号可以被省略,如:

var a = 10;
if (a > 0) console.log("a 大于 0");
else console.log("a 小于等于 0");

if-else 语句执行结束后,会按顺序执行后续的代码,不会结束执行:

function foo(score) {
  if (score >= 60) {
    console.log("及格");
  } else {
    console.log("不及格");
  }
  console.log("判断结束,后续代码会执行");
}

if-else 语句中,else 是可以被省略的,比如:

function foo(x) {
  if (x > 0) {
    console.log("x 大于 0");
  }
  console.log("end");
}
foo(1);
foo(0);

else-if

某些情况下,判断存在多种不同的情况,会产生多种不同的分支,比如考试成绩被分为不及格、及格、良、优等四个阶段,分别对应分数 60 分以下、60 到 80、80 到 90、90 到 100。针对这种情况,我们可以使用 if-else 嵌套:

function foo(x) {
  if (x < 60) {
    console.log("不及格");
  } else {
    if (x < 80) {
      console.log("及格");
    } else {
      if (x < 90) {
        console.log("良");
      } else {
        console.log("优");
      }
    }
  }
}

针对这种情况,可以使用 else-if 语句来让逻辑并行,而不是像上面一样串行:

function foo(x) {
  if (x < 60) {
    console.log("不及格");
  } else if (x < 80) {
    console.log("及格");
  } else if (x < 90) {
    console.log("良");
  } else {
    console.log("优");
  }
}

foo(50); // 不及格
foo(70); // 及格
foo(80); // 良
foo(90); // 优

使用 else-if 会基于前一次判断不成立的情况,再提起一次逻辑判断。其末尾的 else 同样可以删除。

开发中应尽量把 if-else 逻辑嵌套的层数降低,以提高代码可读性。

关系运算符

关系运算符用来进行关系比较,关系运算符组成的表达式称作 关系表达式,关系表达式的值是布尔值。

常见关系运算符如下:

运算符描述
>大于
<小于
>=大于等于
<=小于等于
==等于
!=不等于
===全等于
!==不全等于

当一个关系表达式成立时,值为 true,反之为 false

var foo = 1;
var bar = 2;
console.log(foo > bar, foo < bar); // false true

等于和全等于的区别

等于运算符在进行比较时,较为松懈,比如:

var num = 1;
var str = "1";
console.log(num == str); // true

数字 1 和字符 '1' 使用等于运算符被判定为相等,如果这个你觉得这么比较很合理,那么下面判断你就会觉得很离谱:

var arr = [1];
var num = "1";
var bool = true;
console.log(arr == num, arr == bool, num == bool); // true true true

使用等于运算符比较数组 [1]、字符 '1'、布尔 true 时,表达式的结果都是 true。这些特殊的规律很难总结和记忆,并且容易造成程序出错,所以 开发时一般不使用等于运算符,使用全等于运算符

全等于运算符会严格判断两个数据,必须完全相等表达式才会成立。

var num = 1;
var str = "1";
console.log(num === str); // false

数字 1 和字符 '1' 因为类型不同,全等于关系不成立。

不等于 != 和不全等于 !== 的区别同上述一样,这里不再赘述。

有一个很特殊的情况, NaN 字面量无法通过关系运算符比较 ,只能使用内置函数 isNaN 来判断:

var n = NaN;
n === NaN; // false
isNaN(n); // true

所以判断一个变量是不是 NaN 的时候,不要与 NaN 作比较,两个 NaN 不相等。需要使用 isNaN 函数判断。

逻辑运算符

JS 的关系运算符不支持并且或者逻辑,比如想表示一个数字大于 2 小于 5,不能使用以下表达式:

var x = 3;
if (2 < x < 5) {
  // do something
}

上面的程序实际上会先运算表达式 2 < x 的结果得到一个 true,然后用 true 再与 5 作比较,虽然程序不会报错,但是比较的结果会让人出乎预料。

要实现一个数字同时大于 2 并且小于 5,应该使用逻辑运算符。

可以使用以下代码表示与逻辑:

if (2 < x && x < 5) {
  // do something
}

&& 是一个逻辑运算符,它表示多个条件必须同时成立,被称作逻辑与。常用逻辑运算符如下:

逻辑运算符描述补充
&&与,多个条件同时成立时,命题才成立。对应数学中的并且
\|\|或,其中一个条件成立时,命题就成立。对应数学中的或者
!非,把命题的成立条件颠倒。对应数学中的反命题

与或逻辑比较简单,它们就是数学思维中常说的并且或者。

比如判断一个数字是否是 2 和 3 的倍数,就是并且关系,该数字需要是 2 的倍数,并且是 3 的倍数,它的代码如下:

function foo(x) {
  if (x % 2 === 0 && x % 3 === 0) {
    console.log("是");
  } else {
    console.log("不是");
  }
}
foo(9); // 不是
foo(12); // 是

【代码解释】

如果把上面的题目改成,判断一个数字是 2 或 3 的倍数,就使用或逻辑:

function foo(x) {
  if (x % 2 === 0 || x % 3 === 0) {
    console.log("是");
  } else {
    console.log("不是");
  }
}
foo(9); // 是
foo(12); // 是
foo(11); // 不是

或逻辑只要一个条件成立,命题就成立。所以参数 x 只要是 2 或者 3 其中一个的倍数,表达式就成立了。

非逻辑表示否定,代表原命题的反命题。比如一个命题是:一个数大于 1,其反命题就是:一个数小于等于 1:

var x = 0;

x < 1; // true
!(x < 1); // false

非逻辑的计算结果是一个布尔值,反命题成立,结果为 true,反之为 false。JS 中。

// falsy 值
!0; // true
!undefined; // true

// 不是 falsy 值
!100; // false
![1, 3, 4]; // false

逻辑转换

不同的思维方向会列出不同的逻辑表达式。比如下面两个命题:

  • x 不是 1 并且也不是 2。
  • x 不是 1 或者 2。

上面两个命题的意义相同,但是思维方式不同,按照不同逻辑编写的代码也会不同:

//  x 不是 1 并且也不是 2。
if (val !== 1 && val !== 2) {
  console.log("这个数不是 1 并且不是 2");
}

// x 不是 1 或者 2。
if (!(val === 1 || val === 2)) {
  console.log("这个数不是 1 或 2");
}

上面两个判断的意思是一样的,只是思维方式不一样。所以逻辑语句之间是存在转换规则的,逻辑表达式的转换规则如下:

原表达式等同于
A && B!(!A \|\| !B)
A \|\| B!(!A && !B)

逻辑非还能配合关系运算符转换:

原表达式等同于
A <= B!(A > B)
A < B!(A >= B)
A >= B!(A < B)
A > B!(A <= B)
A != B!(A == B)
A !== B!(A === B)

优先级

意外的是,与运算符比或运算符的优先级要高,所以与逻辑会被先运算。

function foo(x) {
  if (x &gt; 0 &amp;&amp; x &lt; 10 || x &gt; 20 &amp;&amp; x &lt; 30) {
    console.log(&quot;x 在区间 (0,10) 或者 (20,30) 内&quot;);
  }
}

【代码解释】

如果你记不住这点问题,vocode 的 prettire 扩展会在格式化保存时自动填充圆括号提高可读性,让你一眼就知道那里先被执行,但是还是需要同学们记住,编辑器只是辅助,你不一定所有代码都使用 vscode 编写。

逻辑非的优先级极高,大部分情况下逻辑非都会先执行。比如:

!x === false;

绝对会先计算 !A,而不是先计算 x === false。这点从语义上就可以理解。

逻辑表达式与短路运算

与或运算符组成的表达式称作 逻辑表达式,逻辑表达式的值不是 true 或者 false,分析以下代码:

与或逻辑成立或者不成立时,它们的值不一定是 true 或者 false

var foo1 = 0 && 1;
var foo2 = 1 && 2;
var foo3 = 0 || 1;
var foo4 = 1 || 2;

console.log(foo1, foo2, foo3, foo4); // 0 2 1 1

上面的代码不会输出 truefalse,而是进行了短路运算。

程序中不管是与运算还是或运算,都是从左到右计算。

在与运算中:只要有一个不成立,那么逻辑表达式就不成立,所以当程序计算到第一个不成立的表达式后,就会停止后续计算,并把这个不成立的表达式作为逻辑表达式的值,如果全部表达式都成立,那么就使用最后一个值作为逻辑表达式的值。

在或运算中相反:只要有一个成立,那么逻辑表达式就成立,所以当程序计算到第一个成立的表达式后,就会停止后续计算,并把这个成立的表达式作为逻辑表达式的值,如果全部表达式都成立,那么就使用最后一个值作为逻辑表达式的值。

上述这种停止计算的行为,被叫做 短路运算。比如:

var foo = 10 > 1 && true && null && 10;
var bar = 1 > 10 || undefined || "string" || 10;
console.log(foo, bar); // null "string"

【代码解释】

观察下列代码:

var foo, bar;

foo = 0 && (bar = 1);
console.log(foo, bar); // 0 undefined

foo = 10 && (bar = 100);
console.log(foo, bar); // 100 100

foo = 0 && (bar = 1) 中产生了短路运算, && 符后面的语句没有执行,foo 最终的值是 0

而在 foo = 10 && (bar = 100) 中没有产生短路运算,bar = 100 被执行,并且表达式的值是 100,此时 foobar 都是 100;

短路运算常常用来设置默认值或者试探访问,比如:

var foo = {
  name: null,
};

function getNameLength(obj) {
  var len = (obj && obj.name && obj.name.length) || 0;
  console.log(len);
}

getNameLength(); // 0
getNameLength(foo); // 0
getNameLength({ name: "text" }); // 4

试探访问和设置默认值都是提高代码的健壮性,让代码不容易出错,比如上述代码中,如果不使用试探访问与默认值设置,程序就会产生错误,导致无法运行。

小练习,判断一下程序的输出结果:

var A = 1,
  B = 2,
  C = 3,
  D = 4,
  E = 5;
console.log((!A && B) || !C || (D && !E));

三目运算符

开发中常常会遇到 if-else 内只有一行简单代码的情况,这种时候,可以使用三目运算符来简化代码:

function foo(score) {
  if (score >= 60) {
    console.log("及格");
  } else {
    console.log("不及格");
  }
}

function bar(score) {
  var text = score > 60 ? "及格" : "不及格";
  console.log(text);
}

上诉代码中,foo 函数和 bar 函数是等价的。

三目运算符由问号和冒号 ?: 组成,共有三个操作数,格式如下:

命题 ? 成立值 : 不成立值

如果命题成立,那么表达式的值是第二个操作数,否则是第三个操作数。

使用短路运算也可以模拟三目运算符:

function baz() {
  var text = (score > 60 && "及格") || "不及格";
  console.log(text);
}

【代码解释】

逻辑练习

  • 编写一个函数 foo,有一个参数 x,判断 x 是否是一个正数并且是一个双数。

  • 编写一个函数,输入三个不相等的数字参数 x,y,z,从小到大输出这三个数。

function foo(x) {
  if (x > 0 && x % 2 === 0) {
    console.log("是");
  } else {
    console.log("不是");
  }
}
// 解题思维,先判断 x 和 y 的大小,然后把 z 插入 x 和 y。
function bar(x, y, z) {
  if (x > y) {
    if (z > x) {
      console.log(z, x, y);
    } else if (z > y) {
      console.log(x, z, y);
    } else {
      console.log(x, y, z);
    }
  } else {
    if (z > y) {
      console.log(z, y, x);
    } else if (z > x) {
      console.log(y, z, x);
    } else {
      console.log(y, x, z);
    }
  }
}

逻辑分支 switch

switch 是另一种逻辑分支,它的写法如下:

function foo(x) {
  switch (x) {
    case 1:
      console.log("number is 1");
      console.log("x is 1");
      break;
    case 2:
      console.log("number is 2");
      console.log("x is 2");
      break;
    case 3:
      console.log("number is 3");
      console.log("x is 3");
      break;
    default:
      console.log("default");
  }
}

foo(1);

switch 语句接受一个确定的值,并且判断自己的案例列表中是否有这个值,当匹配成功后,执行案例内的代码;

switch 规则:

  • 匹配成功时,会执行匹配成功的 case 后续的所有代码,不过可以使用 break 关键字跳出 switch 语句
  • break 只是跳出 switch 语句,不是跳出程序。
  • 匹配不成功时,会执行 default 后面的代码。
  • case 的匹配值一般是一个字面量或者变量,也可以是复杂的表达式,但是出于程序设计角度来说,使用太复杂的表达式请使用 if-else 语句。

switch 语句和 if-else 语句都是处理逻辑分支的语句,你可以随意使用。

switch 语句的一旦查找到了对应的案例,会执行案例后的所有代码,如果不使用 break 跳出,那么后续所有案例的代码都会执行,比如:

function foo(x) {
  switch (x) {
    case 1:
      console.log("number is 1");
      console.log("x is 1");
    case 2:
      console.log("number is 2");
      console.log("x is 2");
    case 3:
      console.log("number is 3");
      console.log("x is 3");
      break;
    default:
      console.log("default");
  }
}

foo(2); // 会执行case 2 3 中的代码,如果删除 case 3 中的 break,那么默认案例中的代码也会执行

循环分支 for

程序中还存在一种循环分支的流程控制,它被用来处理重复的事情,比如汽车制造商制造一个轮胎,我们把制造轮胎的代码封装进一个函数,不可能今天生成 100 个,我们就写 100 次的执行代码。

循环分支的实现如下:

function foo() {
  for (var i = 0; i < 10; ++i) {
    console.log("第", i, "次执行");
  }
}
foo();

上边这行代码会执行 10 次,打印的数据会填满你的控制台。

通过 for 关键字创建一个循环语句,for 语句的循环控制部分有三个操作数,之间使用分号 ; 号分割。

  • 第一个操作数常常用来初始化循环语句需要的变量
  • 第二个操作数被用来判断循环是否继续执行,如果是一个 falsy 值,那么循环停止。
  • 第三个操作数会在每次循环结束后执行。

循环体用一对花括号 {} 表示,如果只有一条语句,那么可以省略花括号。

JS 中所有逻辑分支语句都不会生成函数作用域,所以我们在上面代码中写的 var i = 0; 实际上把变量 i 声明到了 foo 的函数作用域中。

尝试在循环之后打印变量 i,你会发现它等于 10,因为当变量 i 自增到 10 的时候,第二个操作数为 10 < 10,不成立,循环停止。

和循环关系密切的数据类型是数组,尝试编写一个函数,计算一个数字数组内数据的总和:

function sum(arr) {
  var re = 0;
  for (var i = 0; i < arr.length; i++) {
    re += arr[i];
  }
  return re;
}
var foo = sum([1, 2, 3, 4]);
console.log(foo);

【代码讲解】

小练习 1: 编写一个函数,传入一个数字参数 n,返回 1 - 2 + 4 - 8 + 16... n 的值。n 从 1 开始计数

小练习 2: 编写一个函数,传入一个数字参数 n,返回菲波那切数列的第 n 项。n 从 0 开始计数。

// 小练习 1
function foo(n) {
  var re = 0;
  var item = 1;
  for (var i = 1; i <= n; i++) {
    if (i % 2) {
      re += item;
    } else {
      re -= item;
    }
    item *= 2;
  }
  return re;
}

// 小练习 2
function fib(n) {
  var start = 0;
  var middle = 1;
  var end;

  if (n <= 1) {
    return n;
  } else {
    for (var i = 1; i < n; i++) {
      end = start + middle;
      start = middle;
      middle = end;
    }

    return end;
  }
}

嵌套循环

循环之间可以嵌套,用来处理更复杂的逻辑。小学课程中有一个经典的题目叫做鸡兔同笼,尝试用程序处理鸡兔同笼的问题。

我们可以是用程序暴力破解这个题,使用多重循环撞击所有的可能性,直到找出答案:

function foo(head, foot) {
  if (head * 4 < foot || head * 2 > foot) {
    console.log("头脚不合,有缺胳膊少腿的?");
    return;
  }

  for (var chicken = 0; chicken < head; chicken++) {
    for (var rabbit = 0; rabbit < head; rabbit++) {
      if (chicken + rabbit === head && chicken * 2 + rabbit * 4 === foot) {
        console.log("鸡 : " + chicken, "兔 : " + rabbit);
        return;
      }
    }
  }
}

foo(100, 280);

上述代码中,我们就使用了二次循环暴力破解此题,尽管答案是对的。

现在改变一下思路:

function foo(head, foot) {
  if (head * 4 < foot || head * 2 > foot) {
    console.log("头脚不合,有缺胳膊少腿的?");
    return;
  }

  for (var chicken = 0; chicken < head; chicken++) {
    var rabbit = head - chicken;
    if (chicken * 2 + rabbit * 4 === foot) {
      console.log("鸡 : " + chicken, "兔 : " + rabbit);
      return;
    }
  }
}

foo(100, 280);

我们减少了一层循环体,让代码不论从速度还是可读性上,都获得了提升。

然而实际上,这个题的最优解应该是这样:

function foo(head, foot) {
  if (head * 4 < foot || head * 2 > foot) {
    console.log("头脚不合,有缺胳膊少腿的?");
    return;
  }

  var rabbit = (foot - 2 * head) / 2;
  var chicken = head - rabbit;

  console.log("鸡 : " + chicken, "兔 : " + rabbit);
}

只要有对应的公式,逻辑上的嵌套是可以解开的,类似上面鸡兔同笼的问题一样,不过如果是循环数据嵌套,就无法解开,尝试遍历一个三维数组:

var foo = [
  [
    [1, 2, 3],
    [4, 5, 6],
  ],
  [
    ["a", "b", "c"],
    ["q", "w", "e"],
  ],
];
for (var i = 0; i < foo.length; i++) {
  for (var j = 0; j < foo[i].length; j++) {
    for (var k = 0; k < foo[i][j].length; k++) {
      console.log(foo[i][j][k]);
    }
  }
}

【代码解释】

跳出循环

在程序中,会遇到程序中途跳出循环的情况。跳出循环有三个关键字:

  • break:跳出一层循环体
  • continue:跳出本次循环,然后执行下一次循环。
  • return:停止函数内代码的执行,循环也会被终止

观察下列代码:

for (var i = 0; i < 10; i++) {
  if (i % 2 === 0) {
    continue;
  }

  console.log(i);
}

上述代码每次循环的时候,都会判断 i 是不是一个偶数,是偶数就使用 continue 关键字跳出本次循环,执行下次循环。

如果你不是很理解,我给你提出一个新的非专业术语,你把每次循环读作帧,那么 continue 关键字就是跳出当前帧,执行下一帧。上面的循环一共有 10 帧,当 i 为偶数时,continue 会结束本帧的执行,执行下一帧。

break 语句会跳出一层循环语句,也就是当前循环的所有帧,比如:

for (var i = 0; i < 5; i++) {
  for (var j = 0; j < 3; j++) {
    console.log("j = ", j);
    break;
  }
  console.log("i = ", i);
}

break 无法连续跳出多层循环,如果你想连续跳出多层循环,你可以在循环中设置一个标记,比如:

var flag = false;
for (var i = 0; i < 5 && flag; i++) {
  for (var j = 0; j < 3; j++) {
    console.log("j = ", j);
    if (i > 3) {
      flag = true;
    }
    break;
  }
  console.log("i = ", i);
}

上面代码中,我们定义了一个标记 flag 来决定了外层循环的条件,然后在内层可以随时更改这个条件来让外层循环结束。

label 语句

警告:本小节的内容不要在开发中使用,如果你需要用到本节的内容,那么你应该思考重构你的代码。

我们可以为每个循环命名,这样使用 continuebreak 就可以指定循环体跳出,比如上一节的代码,我们可以直接跳到最外层:

loop1: for (var i = 0; i < 5; i++) {
  for (var j = 0; j < 3; j++) {
    console.log("j = ", j);
    break loop1;
  }
  console.log("i = ", i);
}

上面代码中,我们给 for i 循环体命名为 loop1,就像变量名一样,不论在内层的任何位置,都可以通过 break 或者 continue 语句,来指定跳出循环的位置。

这种写法不被推荐,不要在开发中使用。

循环分支 for-in

for 循环还有另一种变体 for-in 循环,它被用来遍历一个引用类型内可以枚举的属性:

var foo = {
  name: "foo",
  bar: "bar value",
  baz: function () {},
};

for (var key in foo) {
  console.log(key, foo[key]);
}

for in 循环用来遍历一个对象的属性列表,遍历的值是对象的属性名称列表,同普通的 for 语句类似,for in 语句也可以使用 break 等关键字跳出循环,或者和其他循环体互相嵌套。

for in 循环没有循环控制的操作数,需要跳出循环,只能通过 break 或者 continue 等关键字。

使用 for in 循环只能遍历出可以枚举的属性列表,数组也是引用类型,你可以使用 for in 来遍历数组的下标:

var foo = ["a", "b", "c"];

for (var key in foo) {
  console.log(key);
}

上面代码会遍历出数组的下标,但是不会遍历出数组的内置属性 length,因为这个属性是不可枚举的。

循环分支 do-while

还有另一种循环分支叫做 while,相比 for 循环,while 循环更加简洁,它只有一个操作数:

var i = 0;

while (i++ < 10) {
  console.log(i);
}

while 语句仅有的操作数作为循环控制,虽然更加简洁,但是它的使用场景比 for 循环更少。

不论是 forfor in 或者 while 语句,都是先进行循环控制的判断,再执行循环,在 while 循环中,有一种变体,可以先循环一次,再判断是否执行下次循环。它是 do-while 循环,它的编写方式如下:

var i = 0;

do {
  console.log(i);
} while (i++ < 10);

do-while 语句会先执行一次循环体内的代码,然后再进行判断是否进行下次循环。

练习

注意:练习只针对于零基础的同学,如果你有其他编程语言基础,可以无视以下练习。做每道题之前不要看解答,做完后再看解答。如果你是零基础,下列题目并不一定能全部完成,不要灰心,你并没有编程经验,这是正常情况,无法完成的题目在听完讲解后,可以再重新编做一次。

  • 题 1:编写一个函数,传入一个一维数字数组参数 a,输出 a 中的最大值。

  • 题 2:编写一个函数,接收一个行数与列数相同的二维数组,从数组的左上角到右下角作一条直线,把数组的数据沿这条直线翻面,如:

    [
      [1, 2, 3],
      [4, 5, 6],
      [7, 8, 9],
    ];
    

    转化为:

    [
      [1, 4, 7],
      [2, 5, 8],
      [3, 6, 9],
    ];
    
  • 题 3:编写一个函数,传入一个 一维正整数数组,判断数组内是否有由一个数字组成的数,比如 1、2、3、111、222、99999、3333333 这样的数字,如果有打印第一个这样的数字,如果没有打印 '该数组内没有找到符合题意的数字' 字符串。提示:这个题你需要使用算术运算对数字进行分割。

  • 题 4:编写一个函数,传入一个正整数 n,在控制台打印出以下图形:

      *
     ***
    *****
     ***
      *

    其中,第一行的中央有一个 * 号,每行递增两个 * 号,第 n 行以后每行递减两个 * 号,直至没有 * 号。提示:你可以把图形分成几部分处理。提示:你可能会使用绝对值计算,那么你应该多编写一个绝对值函数。

题 1:

function foo(a) {
  var max = a[0];
  for (var i = 1; i < a.length; i++) {
    if (max < a[i]) {
      max = a[i];
    }
  }
  return max;
}

题 2:

观察可知,沿着左上与右下对角线做出分割线,分割线两边的数据项在转化后改变了行号和列号进行了对调,所以只需要把不是对角线上的数据进行行号和列号的对调即可,对角线上的数据行号和列号相同:

function foo(a) {
  var size = a.length;

  // row 遍历到 size - 1,col 从 row + 1 开始,只遍历右上部分
  for (var row = 0; row < size - 1; row++) {
    for (var col = row + 1; col < size; col++) {
      var item = a[row][col];
      a[row][col] = a[col][row];
      a[col][row] = item;
    }
  }
  console.log(a);
}

题 3:

这个题中,我们需要对数字的位数进行拆分。

  • 从末位开始拆分,通过对 10 取模,可以得到个位数字。
  • 然后把数字缩小 10 倍,让十位上的数字变到个位。
  • 重复上述计算,拆分一个数字,每次都需要判断相邻的两个数字是否相同,如果不同,那么说明这个数不止一种数字,停止拆分。
  • 如果数字被拆分结束后,都没有发现该数由多种数字组成,那么就是满足题意的数字。
function foo(a) {
  for (var i = 0; i < a.length; i++) {
    var num = a[i];
    var item;
    do {
      item = num % 10;
      num = (num - item) / 10;
    } while (num % 10 === item);

    if (num === 0) {
      console.log(a[i]);
      return;
    }
  }
  console.log("该数组内没有找到符合题意的数字");
}

题 4:

解答:设每行的行标为 ii 从 1 到 n,输入数据为 n

不难发现新,第 i 行的开头有 |n - i| 个空格作为前缀。记作:条件 A。

行与行之间,每少一个空格,就会多两个 * 号,每行 * 号数量是少去空格数乘以 2 加 1,1 是因为首行自带一个 * 号。另外,首行的空格数量是 n - 1,配合条件 A,可得每行 * 号数量为:1 + (n - 1 - |n - i| ) * 2.

那么程序可以这么写:

// 绝对值函数
function pos(x) {
  return x >= 0 ? x : -x;
}

function foo(n) {
  var rowStr = "";
  for (var i = 1; i < 2 * n; i++) {
    for (var j = 0; j < pos(n - i); j++) {
      rowStr += " ";
    }

    rowStr += "*";
    for (var j = 0; j < n - 1 - pos(i - n); j++) {
      rowStr += "**";
    }

    rowStr += "\n";
  }
  console.log(rowStr);
}

此题解法不一,也可以分成上下部分单独处理。