《Eloquent JavaScript》· 02-程序结构

发布时间:2026/8/20 10:32:21
《Eloquent JavaScript》· 02-程序结构 《Eloquent JavaScript》 · 02-程序结构[2026-08-18 19:37:30 星期二] - [2026-08-19 21:21:21 星期三] 跟着书整理到第 2 章。在线阅读《Eloquent JavaScript》 · 02-程序结构“到目前为止我们一直在使用相对简单的语言构造……本章开始把这些构造组合起来形成基本的程序。”上一章学了值、类型和运算符——语言的「名词和句子片段」。本章把它们串起来用绑定保存值、用控制流组织执行、用函数封装复用。程序是从上到下执行的语句列表控制流语句改变这个默认顺序。本章开始做一些真正称之为编程的东西超越我们迄今为止所看到的名词和句子片段最终能够表达有意义的散文。幽默的引言“我的心脏在我薄薄的半透明皮肤下发出鲜红的亮光他们必须给我注射 10 毫升的 JavaScript 才能让我恢复过来。我对血液中的毒素反应良好。我的天那玩意会让你从腮帮子里吐出桃子”——_why, Why’s (Poignant) Guide to Ruby一、表达式与语句上章节讲了值是什么以及我们可以应用运算符产生新值以旧创新。表达式是产生值的代码片段。每个能直接写出的值如22或psychoanalysis都是表达式括号内表达式、二元运算、一元运算也构成表达式可嵌套类似子句嵌套【说白了这是个递归的定义】。这展示了基于语言的界面的部分美丽。表达式可以包含其他表达式类似于人类语言中子句的嵌套方式——子句可以包含自己的子句依此类推。这使我们能够构建表达式来描述任意复杂的计算。语句对应完整句子程序是语句的列表。【如果表达式对应于句子的片段则JavaScript语句对应于完整句子而程序是语句的列表。】程序 语句 表达式// 最简单的语句类型是「在其后面带分号的表达式」1;// 表达式语句但无副作用无意义!false;// 同上// 这也是一个程序。不过这是一个无用的程序。表达式可以满足于仅仅产生一个值然后该值可以被包含它的代码使用。然而语句是独立存在的因此如果它不影响外部世界它就毫无用处。前面的示例中的语句只是产生值 1 和 true然后立即将其丢弃。这根本没有对外部世界留下任何印象。当你运行这个程序时没有任何可观察到的现象发生。它可能会在屏幕上显示一些东西就像 console.log 一样或者以一种会影响之后语句的方式改变机器的状态。这些变化被称为副作用。前面的示例中的语句只是产生值 1 和 true然后立即将其丢弃。这根本没有对外部世界留下任何印象。当你运行这个程序时没有任何可观察到的现象发生。在某些情况下JavaScript 允许你在语句末尾省略分号。在其他情况下它必须存在否则下一行将被视为同一语句的一部分。安全省略它的规则有些复杂并且容易出错。因此在这本书中每个需要分号的语句都将始终带有分号。我建议你也这样做至少在你了解更多关于省略分号的细微之处之前。补充JavaScript有自动分号插入ASI机制省略分号有时也能跑但在以[、(开头的行前可能踩坑。养成显式加分号的习惯更安全。二、绑定变量let / const / var绑定用于「捕获和保存值」。书中用触手比喻——绑定不「包含」值而是「抓住」值两个绑定可引用同一个值。为了捕获和保存值JavaScript提供了一个名为绑定或变量的东西。let定义了一个绑定它后面跟着绑定名称如果我们想立即给它一个值则后面跟着一个 运算符和一个表达式【此时叫初始化】。定义绑定后其名称可以用作表达式。此表达式的值为绑定当前保存的值。当一个绑定指向一个值时这并不意味着它永远绑定到那个值。 运算符可以随时用于现有绑定使其断开与当前值的连接并使其指向一个新的值【这也称为赋值】。应该将绑定想象成触手而不是盒子。它们并不包含值它们抓住它们——两个绑定可以引用同一个值。程序只能访问它仍然具有引用的值。当你需要记住某件事时你要么长出一条触手抓住它要么重新连接你现有的触手之一到它。【言外之意要么用新的变量去初始化要么用旧的变量去重新赋值】当你定义一个绑定而不给它一个值时触手就没有东西可以抓住所以它会以虚无的方式结束。如果你询问一个空绑定的值你将得到值undefined。单个 let 语句可以定义多个绑定。定义必须用逗号隔开。词语 var 和 const 也可以用来创建绑定类似于 let。a. 第一个var“变量”的缩写是 2015 年之前的 JavaScript 中声明绑定的方式当时 let 还不存在。下一章 中回到它与 let 的精确区别。现在请记住它基本上做了同样的事情但我们在这本书中很少使用它因为它在某些情况下行为异常。b. 词语const代表常量。它定义了一个常量绑定该绑定在它生存的整个时间段内指向同一个值。这对于仅为值提供名称的绑定很有用以便你可以在以后轻松地引用它。// 1. 演示let声明变量初始化letcaught5*5;// 这个示例创建一个名为 caught 的绑定并使用它来抓住 5 乘以 5 产生的数字。// 2. 演示变量的使用letten10;console.log(ten*ten);// → 100// 3. 演示变量的重新赋值letmoodlight;// 初始化console.log(mood);// 使用// → lightmooddark;// 赋值console.log(mood);// 再次使用// → dark// 4. 单个 let 语句可以定义多个绑定。定义必须用逗号隔开letone1,two2;console.log(onetwo);// → 3// 5. 词语 var 和 const 也可以用来创建绑定类似于 let。varnameAyda;constgreetingHello ;console.log(greetingname);// → Hello Ayda关键字作用域重新赋值备注let块级可以现代 JS 默认用它const块级不可以优先用表达「不可变绑定」意图var函数级可以有提升、可重复声明少用实战中能用const就用const需要重新赋值才用let。这样代码意图更清晰也避免误改。var除非维护老代码否则别碰。绑定名称规则可含字母、数字、$、_不能以数字开头不能包含其他标点符号或特殊字符。关键字【具有特殊含义的词】let、const、if等和保留字保留供给将来版本的JavaScript使用不能用作名称。关键字和保留字的完整列表break case catch class const continue debugger defaultdelete do else enum export extends false finally forfunction if implements import interface in instanceof letnew package private protected public return static superswitch this throw true try typeof var void while with yield// 不用担心记住这个列表。当创建绑定产生意外的语法错误时请检查是否正在尝试定义一个保留字。【况且 IDE 也会提示你帮助你】多词命名使用camelCase如fuzzyLittleTurtle。构造函数首字母大写如Number、Math第 6 章详述。环境「在给定时间存在的绑定及其值的集合称为环境」。程序启动时环境非空含语言标准绑定如Math、console和系统交互绑定。当程序启动时此环境并不为空。它总是包含语言标准的一部分的绑定并且大多数情况下它也包含提供与周围系统交互方式的绑定。例如在浏览器中存在与当前加载的网站交互以及读取鼠标和键盘输入的函数。三、函数与返回值默认环境中提供的许多值都是函数类型函数是「包装在值中的程序片段」。执行函数称为调用、执行或应用它可以通过在产生函数值的表达式后面加上括号来调用函数我们直接使用保存函数的绑定的名称。括号之间的值参数被传递给函数内部的程序// prompt 函数使用我们给它的字符串作为在对话框中显示的文本。// prompt 函数在现代 Web 编程中使用不多主要是因为你无法控制生成的对话框的外观但它在玩具程序和实验中很有用。prompt(Enter passcode);// 对话框要求用户输入 浏览器弹窗node不行// console.log函数将其参数写入某些文本输出设备。浏览器或node输出// 在浏览器中输出将出现在 JavaScript 控制台中。浏览器界面的这一部分默认情况下是隐藏的但大多数浏览器在按下 F12 或在 Mac 上按下 command-option-I 时会打开它。如果这不起作用请在菜单中搜索名为开发者工具或类似工具的项目。letx30;console.log(the value of x is,x);// → the value of x is 30// 虽然绑定名称不能包含句点字符但 console.log 确实包含一个。这是因为 console.log 不是一个简单的绑定而是一个表达式它从 console 绑定中保存的值中检索 log 属性。我们将在 第 4 章 中找出这到底是什么意思。显示对话框或在屏幕上写入文本是副作用。许多函数之所以有用是因为它们产生的副作用。函数也可以生成值在这种情况下它们不需要产生副作用才能有用。// 函数Math.max接受任意数量的数字参数并返回最大的值。console.log(Math.max(2,4));// → 4在 JavaScript 中任何产生值的都是表达式这意味着函数调用可以在更大的表达式中使用。// 对Math.min与Math.max相反的调用用作加号表达式的部分console.log(Math.min(2,4)100);// → 102补充console.log和Math.max是全局环境预置的绑定。console是个对象log是它的方法第 6 章详述Math是命名空间容器含max/min/sqrt/random等。四、控制流默认程序从上往下执行。控制流语句改变这个顺序。条件执行 if / elselettheNumberNumber(prompt(Pick a number));// prompt的结果是一个字符串值// Number.isNaN 函数是标准 JavaScript 函数仅当提供的参数为NaN 时返回true。if(!Number.isNaN(theNumber)){console.log(Your number is the square root of theNumber*theNumber);}else{console.log(Hey. Why didnt you give me a number?);}String(prompt(Pick a number))prompt(Pick a number)Boolean(prompt(Pick a number))Number.isNaN仅当参数为NaN时返回true比全局isNaN更严格。{}把多条语句分组为块。在上面的示例中if 之后的语句用大括号{ 和 }包裹。大括号可以用来将任意数量的语句分组到单个语句中称为块。您也可以在这种情况下省略它们因为它们只包含单个语句但为了避免思考是否需要它们大多数 JavaScript 程序员在所有类似的包装语句中使用它们。其实我一般也只在力扣 为了一些代码好看方便阅读的适合故意这样写在工程里面一定加// 这里借用k神力扣的一段美丽的java代码classSolution{publicListIntegerspiralOrder(int[][]matrix){if(matrix.length0)returnnewArrayListInteger();intl0,rmatrix[0].length-1,t0,bmatrix.length-1,x0;Integer[]resnewInteger[(r1)*(b1)];while(true){for(intil;ir;i)res[x]matrix[t][i];// left to rightif(tb)break;for(intit;ib;i)res[x]matrix[i][r];// top to bottomif(l--r)break;for(intir;il;i--)res[x]matrix[b][i];// right to leftif(t--b)break;for(intib;it;i--)res[x]matrix[i][l];// bottom to topif(lr)break;}returnArrays.asList(res);}}if/else if/else链做多路分支。letnumNumber(prompt(Pick a number));if(num10){console.log(Small);}elseif(num100){console.log(Medium);}else{console.log(Large);}while 循环但编写程序的目的是使工作减少而不是增加。所以引入了循环减少一些重复的工作。我们需要的是一种方法来运行一段代码多次。这种形式的控制流称为循环。循环控制流允许我们回到程序中之前的位置并重复执行它并使用当前的程序状态。while只要表达式产生的值在转换为布尔值时给出true循环就会一直进入该语句。// 将其与计数绑定结合起来可以形成下面的程序编写一个程序来计算并显示 2102 的 10 次方的值。letresult1;letcounter0;// 计数器也可以从1 开始并检查 10// 以关键字while 开头的语句创建一个循环。只要表达式产生的值在转换为布尔值时给出true循环就会一直进入该语句。// counter绑定演示了绑定如何跟踪程序的进度while(counter10){// 两个绑定一个跟踪我们的结果另一个计算我们已经将该结果乘以 2 的次数。循环测试第二个绑定是否已达到 10如果尚未达到则更新两个绑定。resultresult*2;countercounter1;}console.log(result);// → 1024// 请注意JavaScript 还有一个用于求幂的运算符 (2 ** 10)您将在实际代码中使用它来计算这个值但这会破坏示例。console.log(2**10);do 循环do 循环是一种与while 循环类似的控制结构。它仅在一点上有所不同**do 循环始终至少执行一次其主体并且它只在第一次执行后开始测试是否应该停止。**为了反映这一点测试出现在循环主体的后面。while在do后面// 此程序将强制您输入姓名。它将反复询问直到获得不是空字符串的名称。letyourName;do{yourNameprompt(Who are you?);}while(!yourName);// 除了 之外的所有字符串都将转换为falseconsole.log(Hello yourName);书中这里的描述有点问题歧义用! 运算符将在否定之前将值转换为布尔类型除了 之外的所有字符串都将转换为true。,应该是false补充do...while适合「先做一次再判断」的场景如先读输入再决定是否重复。实际开发中比while少用但某些场景更自然。关于缩紧缩进在块中的作用是使代码的结构突出显示给人类读者。在代码中新的块在其他块内打开可能难以看到一个块的结束和另一个块的开始。通过适当的缩进程序的视觉形状与内部块的形状相对应。作者喜欢为每个打开的块使用两个空格但口味不同——有些人使用四个空格有些人使用制表符。重要的是每个新块添加相同数量的空格。大多数代码编辑器程序包括这本书中的代码编辑器将通过自动将新行缩进适当数量来提供帮助。for 循环许多循环遵循while 示例中所示的模式首先创建一个“计数器”绑定来跟踪循环的进度然后是一个while 循环通常带有一个测试表达式用于检查计数器是否已达到其结束值。在循环主体结束时计数器更新以跟踪进度。由于此模式非常常见JavaScript 和类似语言提供了稍微更短且更全面的形式即for 循环把初始化、条件检查、状态更新集中在一起。for和while的最大区别将所有与循环“状态”相关的语句分组到for 之后。for(letnumber0;number12;numbernumber2){console.log(number);}// → 0// → 2// … etcetera// 使用for 而不是while 计算 210 的代码letresult1;for(letcounter0;counter10;countercounter1){resultresult*2;}console.log(result);// → 1024注意点括号内两分号分隔三部分初始化继续条件每次迭代后的更新。for 关键字后面的括号必须包含两个分号。第一个分号之前的部分初始化循环通常是通过定义绑定。第二部分是表达式用于检查循环是否必须继续。最后部分在每次迭代后更新循环的状态。在大多数情况下这比while 结构更短更清晰。let number只在循环内可见块级作用域。跳出循环break 和 继续循环continue循环条件产生false并非循环结束的唯一方式。break语句具有立即跳出封闭循环的效果。break立即跳出循环。continue跳过当前迭代进下一轮。// 找到第一个大于或等于 20 且可被 7 整除的数字for(letcurrent20;;currentcurrent1){// 使用余数 (%) 运算符是测试一个数字是否可以被另一个数字整除的简单方法。如果是则它们除法的余数为零。if(current%70){console.log(current);break;}}// → 21死循环无限循环如果你要删除该break语句或者你意外地写了一个总是产生true的结束条件你的程序就会陷入无限循环。陷入无限循环的程序永远不会结束运行这通常是一件坏事。如果你在这些页面上的示例之一中创建了一个无限循环通常会问你是否要在几秒钟后停止脚本。如果失败你将不得不关闭正在使用的选项卡才能恢复。continue关键字类似于break因为它影响循环的进度。当在循环体中遇到continue时控制权将跳出循环体并继续进行循环的下一个迭代。补充break还可带标签跳出多层嵌套循环break labelName但少见。continue用多了会让控制流难追谨慎。简洁地更新绑定循环时经常需要“更新”一个绑定以根据该绑定的先前值来保存一个值。countercounter1;// JavaScript 为此提供了一个快捷方式counter1;// * / -// 还有更短的等效项counter;counter;counter--;--counter;switch 调度switch引入是因为遇到了下面这种if else if else的结构if(xvalue1)action1();elseif(xvalue2)action2();elseif(xvalue3)action3();elsedefaultAction();有一个名为switch的结构旨在以更直接的方式表达这种“调度”。不幸的是JavaScript 用于此的语法它从 C/Java 编程语言系列继承而来有点笨拙——一系列if语句可能看起来更好。switch(prompt(What is the weather like?)){caserainy:console.log(Remember to bring an umbrella.);break;casesunny:console.log(Dress lightly.);casecloudy:console.log(Go outside.);break;default:console.log(Unknown weather type!);break;}可以在switch打开的块中放置任意数量的case标签。程序将从与switch给定的值相对应的标签开始执行或者在没有找到匹配值的情况下从default开始执行。它将继续执行即使跨越其他标签直到它到达break语句。在某些情况下例如示例中的sunny 案例这可以用来在案例之间共享一些代码它建议在晴朗和阴天都到户外活动。【缺少break会「穿透」到下一个case如sunny跳到cloudy的代码穿透可用来共享代码但极易出错。】但要小心——很容易忘记这样的break这将导致程序执行你不想执行的代码。多数情况建议显式写break需要穿透时加注释说明否则后人维护时容易踩坑。简洁更新绑定counter1;// 等同 counter counter 1result*2;// 等同 result result * 2counter;// 等同 counter 1counter--;// 等同 counter - 1补充的前置x和后置x返回值不同前置返回新值后置返回旧值但在语句里单独用无差别。别在复杂表达式里混用可读性差。五、缩进与代码风格缩进非必需计算机不需要但对人类阅读至关重要。「程序的视觉形状与内部块的形状相对应」。推荐每层 2 空格也有人用 4 空格或制表符关键是保持一致。多数编辑器自动缩进。if(false!true){console.log(That makes sense.);if(12){console.log(No surprise there.);}}六、注释通常原始代码无法传达你希望程序传达给人类读者的所有信息或者它以一种难以理解的方式传达信息以至于人们可能无法理解它。在其他时候你可能只是想在你的程序中包含一些相关的想法。这就是注释的作用。注释是程序的一部分文本但计算机完全忽略它。// 单行注释使用两个斜杠字符 (//)然后是它后面的注释文本letaccountBalancecalculateBalance(account);// Its a green hollow where a river singsaccountBalance.adjust();// Madly catching white tatters in the grass.letreportnewReport();// Where the sun on the proud mountain rings:addToReport(accountBalance,report);// Its a little valley, foaming like light in a glass.// 注释只到行尾。/* 和*/ 之间的文本部分将被完全忽略无论它是否包含换行符。这对于添加有关文件或程序块的信息块很有用/* 多行注释 可跨越多行 *//* I first found this number scrawled on the back of an old notebook. Since then, it has often dropped by, showing up in phone numbers and the serial numbers of products that Ive bought. It obviously likes me, so Ive decided to keep it. */constmyNumber11213;补充注释讲「为什么」别翻译代码。好的代码自解释注释补充意图、取舍、坑。七、练习这是检验学习的关键部分。动手写别只看。可以在网上找到练习的完整解决方案网址为完整解决方案。循环三角形/* 编写一个循环对console.log进行七次调用以输出以下三角形 # ## ### #### ##### ###### ####### */// 注意.length和的使用for(letline#;line.length7;line#){console.log(line);}FizzBuzz经典题遍历 1-100被 3 整除打印 Fizz被 5 整除打印 Buzz同时被 3 和 5 整除打印 FizzBuzz。/* 编写一个使用console.log打印 1 到 100 的所有数字的程序有两个例外。对于可以被 3 整除的数字打印Fizz 而不是数字对于可以被 5但不能被 3整除的数字打印Buzz 而不是数字。 当你完成上述工作后修改你的程序对于可以被 3 和 5 整除的数字打印FizzBuzz对于只可以被其中一个整除的数字仍然打印Fizz 或Buzz。 这实际上是一个面试问题据称它剔除了很大一部分程序员候选人。所以如果你解决了它你的劳动力市场价值就提高了。 */for(letn1;n100;n){letoutput;if(n%30)outputFizz;if(n%50)outputBuzz;console.log(output||n);// 巧用短路}这里用output || n很巧——output为空字符串假值时回退到n否则打印output。这正是上一章||默认值用法的实战。棋盘构建 8×8 网格空格和#交替用\n分隔行。核心思路外层循环行内层循环列用行列计数器之和的奇偶决定字符。/* 编写一个程序使用换行符分隔行创建一个表示 8×8 网格的字符串。网格的每个位置要么是空格要么是“#”字符。这些字符应该形成一个棋盘。 将此字符串传递给console.log应该显示类似以下内容 # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 当你有一个生成这种模式的程序时定义一个绑定size 8并更改程序使其适用于任何size输出给定宽度和高度的网格。 */letsize8,board;// 为了处理二维你需要在一个循环内使用另一个循环。用大括号括住两个循环的循环体以便轻松查看它们从哪里开始和结束。尝试适当地缩进这些循环体。循环的顺序必须遵循我们构建字符串的顺序一行一行从左到右从上到下。因此外层循环处理行内层循环处理一行上的字符。for(lety0;ysize;y){for(letx0;xsize;x){board((xy)%20)? :#;}// 在构建完一行后必须通过添加换行符来终止一行所以在内循环之后但在外循环内完成此操作。board\n;}console.log(board);输出形如# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #(x y) % 2是关键——行列之和的奇偶决定黑白格这思路在很多二维网格题里通用。八、本章速查程序是由语句构成的语句本身有时包含更多语句。语句往往包含表达式表达式本身可以由更小的表达式构成。将语句一个接一个地放在一起就会得到一个从上到下执行的程序。你可以使用条件 (if、else 和switch) 和循环 (while、do 和for) 语句来引入控制流中的干扰。绑定可以用来将数据片段存储在一个名称下它们对于跟踪程序中的状态很有用。环境是定义的一组绑定。JavaScript 系统总是将一些有用的标准绑定放到你的环境中。函数是封装程序片段的特殊值。你可以通过编写functionName(argument1, argument2)来调用它们。这样的函数调用是一个表达式它可能会产生一个值。构造用途关键点let/const/var声明绑定const不可重新赋值var行为异常少用if/else if/else条件分支{}分组语句块while/do...while循环do至少执行一次for循环三段初始化条件更新break/continue控制循环break跳出continue跳过本轮switch多路分支缺break会穿透等简洁更新x等同x 1程序的本质是「用绑定保存值、用控制流组织执行」剩下的都是在这两个支柱上加细节。下一章把这些语句封装成函数。

相关新闻