JavaScript高级程序设计(第4版)读书笔记:七

JavaScript语句与函数详解

JavaScript程序设计

2.5 语句

ECMA-262 描述了一些语句(也称为流控制语句),而 ECMAScript 中的大部分语法都体现在语句中。语句通常使用一或多个关键字完成既定的任务。语句可以简单,也可以复杂。简单的如告诉函数退出,复杂的如列出一堆要重复执行的指令。

以下部分除for-in,for-of,标签语句,with语句都和C/C++/Java等主流语言类似。

2.5.1 if 语句

if 语句是使用最频繁的语句之一,语法如下:

if (condition) statement1 else statement2

这里的条件(condition)可以是任何表达式,并且求值结果不一定是布尔值。ECMAScript 会自动调用 Boolean()函数将这个表达式的值转换为布尔值。如果条件求值为 true,则执行语句statement1;如果条件求值为 false,则执行语句 statement2。这里的语句可能是一行代码,也可能是一个代码块(即包含在一对花括号中的多行代码)。来看下面的例子:

if (i > 25) 
   console.log("Greater than 25."); // 只有一行代码的语句
else { 
   console.log("Less than or equal to 25."); // 一个语句块
}

这里的最佳实践是使用语句块,即使只有一行代码要执行也是如此。这是因为语句块可以避免对什么条件下执行什么产生困惑。

可以像这样连续使用多个 if 语句:

if (condition1) statement1 else if (condition2) statement2 else statement3

下面是一个例子:

if (i > 25) { 
   console.log("Greater than 25."); 
} else if (i < 0) { 
   console.log("Less than 0."); 
} else { 
   console.log("Between 0 and 25, inclusive."); 
}
2.5.2 do-while 语句

do-while 语句是一种后测试循环语句,即循环体中的代码执行后才会对退出条件进行求值。换句话说,循环体内的代码至少执行一次。do-while 的语法如下:

do { 
 statement 
} while (expression);

下面是一个例子:

let i = 0; 
do { 
 i += 2; 
} while (i < 10);

在这个例子中,只要 i 小于 10,循环就会重复执行。i 从 0 开始,每次循环递增 2。

Note: 后测试循环经常用于这种情形:循环体内代码在退出前至少要执行一次。

2.5.3 while 语句

while 语句是一种先测试循环语句,即先检测退出条件,再执行循环体内的代码。因此,while 循环体内的代码有可能不会执行。下面是 while 循环的语法:

while(expression) statement

这是一个例子:

let i = 0; 
while (i < 10) { 
   i += 2; 
}

在这个例子中,变量 i 从 0 开始,每次循环递增 2。只要 i 小于 10,循环就会继续。

2.5.4 for 语句

for 语句也是先测试语句,只不过增加了进入循环之前的初始化代码,以及循环执行后要执行的表达式,语法如下:

for (initialization; expression; post-loop-expression) statement

下面是一个用例:

let count = 10; 
for (let i = 0; i < count; i++) { // 使用 let 声明变量 i
 console.log(i); 
}

以上代码在循环开始前定义了变量 i 的初始值为 0。然后求值条件表达式,如果求值结果为 true(i < count),则执行循环体。因此循环体也可能不会被执行。如果循环体被执行了,则循环后表达式也会执行,以便递增变量 i。for 循环跟下面的 while 循环是一样的:

let count = 10; 
let i = 0; 
while (i < count) { 
 console.log(i); 
 i++; 
}

无法通过 while 循环实现的逻辑,同样也无法使用 for 循环实现。因此 for 循环只是将循环相关的代码封装在了一起而已。

在 for 循环的初始化代码中,其实是可以不使用变量声明关键字的。不过,初始化定义的迭代器变量在循环执行完成后几乎不可能再用到了。因此,最清晰的写法是使用 let 声明迭代器变量,这样就可以将这个变量的作用域限定在循环中

初始化、条件表达式和循环后表达式都不是必需的。因此,下面这种写法可以创建一个无穷循环:

for (;;) { // 无穷循环
   doSomething(); 
}

如果只包含条件表达式,那么 for 循环实际上就变成了 while 循环:

let count = 10; 
let i = 0; 
for (; i < count; ) { 
   console.log(i); 
   i++; 
}

这种多功能性使得 for 语句在这门语言中使用非常广泛。

2.5.5 for-in 语句

for-in 语句是一种严格的迭代语句,用于枚举对象中的非符号键属性,语法如下:

for (property in expression) statement

下面是一个例子:

for (const propName in window) { 
   document.write(propName); 
}

这个例子使用 for-in 循环显示了 BOM 对象 window 的所有属性。每次执行循环,都会给变量propName 赋予一个 window 对象的属性作为值,直到 window 的所有属性都被枚举一遍。与 for 循环一样,这里控制语句中的 const 也不是必需的。但为了确保这个局部变量不被修改,推荐使用 const

ECMAScript 中对象的属性是无序的,因此 for-in 语句不能保证返回对象属性的顺序。换句话说,所有可枚举的属性都会返回一次,但返回的顺序可能会因浏览器而异。

如果 for-in 循环要迭代的变量是 null 或 undefined,则不执行循环体。

值得补充的是:

  • for-in: 只遍历可枚举的字符串键属性(包括原型链)
  • Object.keys(): 只返回自身可枚举的字符串键属性
  • Object.getOwnPropertyNames(): 返回自身所有字符串键属性(包括不可枚举)
  • Object.getOwnPropertySymbols(): 返回自身所有符号键属性
  • Reflect.ownKeys(): 返回自身所有键(字符串+符号),最全面的方法

需要注意的是 for-in 语句还会遍历原型链上的可枚举属性(除非用 hasOwnProperty() 过滤):

function Person(name) {
    this.name = name;
}

Person.prototype.sayHello = function() {
    console.log(`Hello, I'm ${this.name}`);
};

const person = new Person('John');

console.log('=== for-in (包含原型链属性) ===');
for (const key in person) {
    console.log(key); 
    // 输出: name, sayHello (原型链上的方法也会被枚举)
}

console.log('=== for-in + hasOwnProperty (过滤原型链) ===');
for (const key in person) {
    if (person.hasOwnProperty(key)) {
        console.log(key); // 输出: name (只包含自身属性)
    }
}

console.log('=== Object.keys (只包含自身可枚举属性) ===');
for (const key of Object.keys(person)) {
    console.log(key); // 输出: name
}
2.5.6 for-of 语句

for-of 语句是一种严格的迭代语句,用于遍历可迭代对象的元素,语法如下:

for (property of expression) statement

下面是示例:

for (const el of [2,4,6,8]) { 
   document.write(el); 
}

在这个例子中,我们使用 for-of 语句显示了一个包含 4 个元素的数组中的所有元素。循环会一直持续到将所有元素都迭代完。与 for 循环一样,这里控制语句中的 const 也不是必需的。但为了确保这个局部变量不被修改,推荐使用 const

for-of 循环会按照可迭代对象(即实现了前文提到的[Symbol.iterator]方法的对象)的 next()方法产生值的顺序迭代元素。如果尝试迭代的变量不支持迭代,则 for-of 语句会抛出错误。

Note: ES2018 对 for-of 语句进行了扩展,增加了 for-await-of 循环,以支持生成期约(promise)的异步可迭代对象(即实现了前文提到的[Symbol.asyncIterator]方法的对象)。

下面这个例子展示了 for-in 与 for-of 的区别:

const arr = ['a', 'b', 'c'];

console.log('=== for-in (遍历索引) ===');
for (const key in arr) {
    console.log(`key: ${key}, type: ${typeof key}, value: ${arr[key]}`);
    // 输出: key: 0, type: string, value: a
    //       key: 1, type: string, value: b  
    //       key: 2, type: string, value: c
}

console.log('=== for-of (遍历值) ==='); // 只能用于可迭代对象(或手动实现[Symbol.iterator]方法)
for (const value of arr) {
    console.log(`value: ${value}, type: ${typeof value}`);
    // 输出: value: a, type: string
    //       value: b, type: string
    //       value: c, type: string
}
2.5.7 标签语句

标签语句用于给语句加标签,语法如下:

label: statement

下面是一个例子:

start: for (let i = 0; i < count; i++) { 
   console.log(i); 
}

在这个例子中,start 是一个标签,可以在后面通过 break 或 continue 语句引用。标签语句的典型应用场景是嵌套循环,下面是一个例子:

// 使用标签的情况 - 找到朋友就回家(一次性跳出多层循环)
search: for (let floor = 1; floor <= 3; floor++) {
    for (let room = 1; room <= 5; room++) {
        console.log(`正在搜索 ${floor}${room}号房间`);
        if (floor === 2 && room === 3) {
            console.log("找到朋友了!回家!");
            break search; // 直接停止整个搜索,不会再搜索第3楼
        }
    }
}
2.5.8 break 和 continue 语句

break 和 continue 语句为执行循环代码提供了更严格的控制手段。其中,break 语句用于立即退出循环,强制执行循环后的下一条语句。而 continue 语句也用于立即退出循环,但会再次从循环顶部开始执行。下面看一个例子:

let num = 0; 
for (let i = 1; i < 10; i++) { 
   if (i % 5 == 0) { 
      break;
   } 
   num++; 
} 
console.log(num); // 4

在上面的代码中,for 循环会将变量 i 由 1 递增到 10。而在循环体内,有一个 if 语句用于检查 i能否被 5 整除(使用取模操作符)。如果是,则执行 break 语句,退出循环。变量 num 的初始值为 0,表示循环在退出前执行了多少次。当 break 语句执行后,下一行执行的代码是 console.log(num),显示 4。之所以循环执行了 4 次,是因为当 i 等于 5 时,break 语句会导致循环退出,该次循环不会执行递增 num 的代码。如果将 break 换成 continue,则会出现不同的效果:

let num = 0; 
for (let i = 1; i < 10; i++) { 
   if (i % 5 == 0) { 
     continue; 
   } 
   num++; 
} 
console.log(num); // 8

这一次,console.log 显示 8,即循环被完整执行了 8 次。当 i 等于 5 时,循环会在递增 num 之前退出,但会执行下一次迭代,此时 i 是 6。然后,循环会一直执行到自然结束,即 i 等于 10。最终num 的值是 8 而不是 9,是因为 continue 语句导致它少递增了一次。

break 和 continue 都可以与标签语句一起使用,返回代码中特定的位置。这通常是在嵌套循环中,如下面的例子所示:

let num = 0; 
outermost: 
for (let i = 0; i < 10; i++) { 
   for (let j = 0; j < 10; j++) { 
       if (i == 5 && j == 5) { 
           break outermost; 
       } 
       num++; 
   } 
} 
console.log(num); // 55

在这个例子中,outermost 标签标识的是第一个 for 语句。正常情况下,每个循环执行 10 次,意味着 num++语句会执行 100 次,而循环结束时 console.log 的结果应该是 100。但是,break 语句带来了一个变数,即要退出到的标签。添加标签不仅让 break 退出(使用变量 j 的)内部循环,也会退出(使用变量 i 的)外部循环。当执行到 i 和 j 都等于 5 时,循环停止执行,此时 num 的值是 55。continue语句也可以使用标签,如下面的例子所示:

let num = 0; 
outermost: 
for (let i = 0; i < 10; i++) { 
   for (let j = 0; j < 10; j++) {
       if (i == 5 && j == 5) { 
           continue outermost; 
       } 
       num++; 
   } 
} 
console.log(num); // 95

这一次,continue 语句会强制循环继续执行,但不是继续执行内部循环,而是继续执行外部循环。当 i 和 j 都等于 5 时,会执行 continue,跳到外部循环继续执行,从而导致内部循环少执行 5 次,结果 num 等于 95。

组合使用标签语句和 break、continue 能实现复杂的逻辑,但也容易出错。注意标签要使用描述性强的文本,而嵌套也不要太深。

2.5.9 with 语句

由于 with 语句影响性能且难于调试其中的代码,通常不推荐在产品代码中使用 with 语句。

with 语句的用途是将代码作用域设置为特定的对象,其语法是:

with (expression) statement;

使用 with 语句的主要场景是针对一个对象反复操作,这时候将代码作用域设置为该对象能提供便利,如下面的例子所示:

let qs = location.search.substring(1); 
let hostName = location.hostname; 
let url = location.href;

上面代码中的每一行都用到了 location 对象。如果使用 with 语句,就可以少写一些代码:

with(location) { 
   let qs = search.substring(1); 
   let hostName = hostname; 
   let url = href; 
}

这里,with 语句用于连接 location 对象。这意味着在这个语句内部,每个变量首先会被认为是一个局部变量。如果没有找到该局部变量,则会搜索 location 对象,看它是否有一个同名的属性。如果有,则该变量会被求值为 location 对象的属性。

严格模式不允许使用 with 语句,否则会抛出错误

2.5.10 switch 语句

switch 语句是与 if 语句紧密相关的一种流控制语句,从其他语言借鉴而来。ECMAScript中 switch 语句跟 C 语言中 switch 语句的语法非常相似,如下所示:

switch (expression) { 
   case value1: 
       statement
       break; 
   case value2: 
       statement 
       break;
   default: 
       statement 
}

这里的每个 case(条件/分支)相当于:“如果表达式等于后面的值,则执行下面的语句。”break关键字会导致代码执行跳出 switch 语句。如果没有 break,则代码会继续匹配下一个条件。default关键字用于在任何条件都没有满足时指定默认执行的语句(相当于 else 语句)。

为避免不必要的条件判断,最好给每个条件后面都加上 break 语句。如果确实需要连续匹配几个条件,那么推荐写个注释表明是故意忽略了 break

虽然 switch 语句是从其他语言借鉴过来的,但 ECMAScript 为它赋予了一些独有的特性。首先,switch 语句可以用于所有数据类型(在很多语言中,它只能用于数值),因此可以使用字符串甚至对象。其次,条件的值不需要是常量,也可以是变量或表达式。看下面的例子:

switch ("hello world") { 
   case "hello" + " world": 
       console.log("Greeting was found."); 
       break; 
   case "goodbye": 
       console.log("Closing was found."); 
       break; 
   default: 
       console.log("Unexpected message was found."); 
}
// 输出:Greeting was found.

这个例子在 switch 语句中使用了字符串。第一个条件实际上使用的是表达式,求值为两个字符串拼接后的结果。因为拼接后的结果等于 switch 的参数,所以 console.log 会输出"Greeting was found."。能够在条件判断中使用表达式,就可以在判断中加入更多逻辑:

let num = 25; 
switch (true) { 
   case num < 0: 
       console.log("Less than 0."); 
       break; 
   case num >= 0 && num <= 10: 
       console.log("Between 0 and 10."); 
       break; 
   case num > 10 && num <= 20: 
       console.log("Between 10 and 20."); 
       break; 
   default: 
       console.log("More than 20."); 
}

上面的代码首先在外部定义了变量 num,而传给 switch 语句的参数之所以是 true,就是因为每个条件的表达式都会返回布尔值。条件的表达式分别被求值,直到有表达式返回 true;否则,就会一直跳到 default 语句(这个例子正是如此)。

Note: switch 语句在比较每个条件的值时会使用全等操作符,因此不会强制转换数据类型(比如,字符串"10"不等于数值 10)。

2.6 函数

函数对任何语言来说都是核心组件,因为它们可以封装语句,然后在任何地方、任何时间执行。ECMAScript 中的函数使用 function 关键字声明,后跟一组参数,然后是函数体

以下是函数的基本语法:

function functionName(arg0, arg1,...,argN) { 
   statements 
}

下面是一个例子:

function sayHi(name, message) { 
   console.log("Hello " + name + ", " + message); 
}

可以通过函数名来调用函数,要传给函数的参数放在括号里(如果有多个参数,则用逗号隔开)。下面是调用函数 sayHi()的示例:

sayHi("Nicholas", "how are you today?");

调用这个函数的输出结果是"Hello Nicholas, how are you today?"。参数 name 和 message 在函数内部作为字符串被拼接在了一起,最终通过 console.log 输出到控制台。

ECMAScript 中的函数不需要指定是否返回值。任何函数在任何时间都可以使用 return 语句来返回函数的值,用法是后跟要返回的值。比如:

function sum(num1, num2) { 
   return num1 + num2; 
}

函数 sum()会将两个值相加并返回结果。注意,除了 return 语句之外没有任何特殊声明表明该函数有返回值。然后就可以这样调用它:

const result = sum(5, 10);

要注意的是,只要碰到 return 语句,函数就会立即停止执行并退出。因此,return 语句后面的代码不会被执行。比如:

function sum(num1, num2) { 
   return num1 + num2; 
   console.log("Hello world"); // 不会执行
}

在这个例子中,console.log 不会执行,因为它在 return 语句后面。一个函数里也可以有多个 return 语句,像这样:

function diff(num1, num2) { 
   if (num1 < num2) { 
       return num2 - num1; 
   } else { 
       return num1 - num2; 
   } 
}

这个 diff()函数用于计算两个数值的差。如果第一个数值小于第二个,则用第二个减第一个;否则,就用第一个减第二个。代码中每个分支都有自己的 return 语句,返回正确的差值。

return 语句也可以不带返回值。这时候,函数会立即停止执行并返回 undefined。这种用法最常用于提前终止函数执行,并不是为了返回值。比如在下面的例子中,console.log 不会执行:

function sayHi(name, message) { 
   return; 
   console.log("Hello " + name + ", " + message); // 不会执行
}

Note: 最佳实践是函数要么返回值,要么不返回值。只在某个条件下返回值的函数会带来麻烦,尤其是调试时。

严格模式对函数也有一些限制:

  • 函数不能以 eval 或 arguments 作为名称
  • 函数的参数不能叫 eval 或 arguments
  • 两个命名参数不能拥有同一个名称

如果违反上述规则,则会导致语法错误,代码也不会执行。

2.7 小结

JavaScript 的核心语言特性在 ECMA-262 中以伪语言 ECMAScript 的形式来定义。ECMAScript 包含所有基本语法、操作符、数据类型和对象,能完成基本的计算任务,但没有提供获得输入和产生输出的机制。理解 ECMAScript 及其复杂的细节是完全理解浏览器中 JavaScript 的关键。下面总结一下ECMAScript 中的基本元素。

  • ECMAScript 中的基本数据类型包括 Undefined、Null、Boolean、Number、String 和 Symbol。
  • 与其他语言不同,ECMAScript 不区分整数和浮点值,只有 Number 一种数值数据类型。
  • Object 是一种复杂数据类型,它是这门语言中所有对象的基类。
  • 严格模式为这门语言中某些容易出错的部分施加了限制。
  • ECMAScript 提供了 C 语言和类 C 语言中常见的很多基本操作符,包括数学操作符、布尔操作符、关系操作符、相等操作符和赋值操作符等。
  • 这门语言中的流控制语句大多是从其他语言中借鉴而来的,比如 if 语句、for 语句和 switch 语句等。

ECMAScript 中的函数与其他语言中的函数不一样。

  • 不需要指定函数的返回值,因为任何函数可以在任何时候返回任何值。
  • 不指定返回值的函数实际上会返回特殊值 undefined
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值