【Javascript基础】--零基础--超详细且简洁的Javascript笔记--代码质量(03)

在浏览器中调试

在编写代码前看看调试。

调试是指在一个脚本中找出并修复错误的过程。

在这里我们将会使用 Chrome(谷歌浏览器),因为它拥有足够多的功能,其他大部分浏览器的功能也与之类似。

“资源(Sources)”面板

你的 Chrome 版本可能看起来有一点不同,但是它应该还是处于很明显的位置。

  • 在 Chrome 中打开一个页面。
  • 使用快捷键 F12 打开开发者工具。
  • 选择 Sources(资源) 面板。

如果你是第一次这么做,那你应该会看到下面这个样子:
在这里插入图片描述

切换按钮会打开文件列表的选项卡。

资源(Sources)面板包含三个部分:

  1. 文件导航(File Navigator) 区域列出了 HTML、JavaScript、CSS 和包括图片在内的其他依附于此页面的文件。Chrome 扩展程序也会显示在这。
  2. 代码编辑(Code Editor) 区域展示源码。
  3. JavaScript 调试(JavaScript Debugging) 区域是用于调试的,我们很快就会来探索它。

现在你可以再次点击切换按钮 隐藏资源列表来给代码腾出一些空间。

控制台(Console)

如果我们按下 Esc,下面会出现一个控制台,我们可以输入一些命令然后按下 Enter 来执行。

语句执行完毕之后,其执行结果会显示在下面。

例如,1+2 将会返回 3,而 hello("debugger") 函数调用什么也没返回,所以结果是 undefined
在这里插入图片描述

断点(Breakpoints)

断点 是调试器会自动暂停 JavaScript 执行的地方。

当代码被暂停时,我们可以检查当前的变量,在控制台执行命令等等。换句话说,我们可以调试它。

我们总是可以在右侧的面板中找到断点的列表。当我们在数个文件中有许多断点时,这是非常有用的。它允许我们:

  • 快速跳转至代码中的断点(通过点击右侧面板中的对应的断点)。
  • 通过取消选中断点来临时禁用对应的断点。
  • 通过右键单击并选择移除来删除一个断点。
  • ……等等。

条件断点

在行号上 右键单击 允许你创建一个 条件 断点。只有当给定的表达式(你创建条件断点时提供的表达式)为真时才会被触发。

当我们需要在特定的变量值或参数的情况下暂停程序执行时,这种调试方法就很有用了。

“debugger” 命令

我们也可以使用 debugger 命令来暂停代码,像这样:

function hello(name) {
  let phrase = `Hello, ${name}!`;

  debugger;  // <-- 调试器会在这停止

  say(phrase);
}

这样的命令只有在开发者工具打开时才有效,否则浏览器会忽略它。

暂停并查看

请打开右侧的信息下拉列表(箭头指示出的地方)。这里允许你查看当前的代码状态:

  1. 察看(Watch) —— 显示任意表达式的当前值。

    你可以点击加号 + 然后输入一个表达式。调试器将显示它的值,并在执行过程中自动重新计算该表达式。

  2. 调用栈(Call Stack) —— 显示嵌套的调用链。

    此时,调试器正在 hello() 的调用链中,被 index.html 中的一个脚本调用(这里没有函数,因此显示 “anonymous”)

    如果你点击了一个堆栈项,调试器将跳到对应的代码处,并且还可以查看其所有变量。

  3. 作用域(Scope) —— 显示当前的变量。

    Local 显示当前函数中的变量,你还可以在源代码中看到它们的值高亮显示了出来。

    Global 显示全局变量(不在任何函数中)。

    这里还有一个 this 关键字,目前我们还没有学到它,不过我们很快就会学习它了。

跟踪执行

现在是 跟踪 脚本的时候了。

在右侧面板的顶部是一些关于跟踪脚本的按钮。让我们来使用它们吧。
在这里插入图片描述

  • “恢复(Resume)”:继续执行,快捷键 F8。

继续执行。如果没有其他的断点,那么程序就会继续执行,并且调试器不会再控制程序。

  • “下一步(Step)”:运行下一条(即当前行)指令,快捷键 F9。

    运行下一条语句。

    一次接一次地点击此按钮,整个脚本的所有语句会被逐个执行。

  • “跨步(Step over)”:运行下一条(即当前行)指令,但 不会进入到一个函数中,快捷键 F10。

    跟上一条命令“下一步(Step)”类似,但如果下一条语句是函数调用则表现不同。这里的函数指的是:不是内建的如 alert 函数等,而是我们自己写的函数。

    如果我们对比一下,“下一步(Step)”命令会进入嵌套函数调用并在其第一行暂停执行,而“跨步(Step over)”对我们不可见地执行嵌套函数调用,跳过了函数内部。执行会在该函数调用后立即暂停。

    如果我们对该函数的内部执行不感兴趣,这命令会很有用。

  • “步入(Step into)”,快捷键 F11。

    和“下一步(Step)”类似,但在异步函数调用情况下表现不同。

    至于之后,只需要记住“下一步(Step)”命令会忽略异步行为,例如 setTimeout(计划的函数调用),它会过一段时间再执行。而“步入(Step into)”会进入到代码中并等待(如果需要)。

  • “步出(Step out)”:继续执行到当前函数的末尾,快捷键 Shift+F11。

    继续执行当前函数内的剩余代码,并暂停在调用当前函数的下一行代码处。当我们使用 偶然地进入到一个嵌套调用,但是我们又对这个函数不感兴趣时,我们想要尽可能的继续执行到最后的时候是非常方便的。

  • 启用/禁用所有的断点。

    这个按钮不会影响程序的执行。只是一个批量操作断点的开/关。

  • 启用/禁用出现错误时自动暂停脚本执行。

    当启动此功能,如果开发者工具是打开着的时候,任何脚本执行错误都会导致该脚本执行自动暂停。然后我们可以在调试器中分析变量来看一下什么出错了。因此如果我们的脚本因为错误挂掉的时候,我们可以打开调试器,启用这个选项然后重载页面,查看一下哪里导致它挂掉了和当时的上下文是什么。

Continue to here

在代码中的某一行上右键,在显示的关联菜单(context menu)中点击一个非常有用的名为 “Continue to here” 的选项。

当你想要向前移动很多步到某一行为止,但是又懒得设置一个断点时非常的方便。

日志记录

想要输出一些东西到控制台上?console.log 函数可以满足你。

例如:将从 04 的值输出到控制台上:

// 打开控制台来查看
for (let i = 0; i < 5; i++) {
  console.log("value", i);
}

普通用户看不到这个输出,它是在控制台里面的。要想看到它 —— 要么打开开发者工具中的 Console(控制台)选项卡,要么在一个其他的选项卡中按下 Esc:这会在下方打开一个控制台。

如果我们在代码中有足够的日志记录,那么我们可以从记录中看到刚刚发生了什么,而不需要借助调试器。

小结

我们可以看到,这里有 3 种方式来暂停一个脚本:

  1. 断点。
  2. debugger 语句。
  3. error(如果开发者工具是打开状态,并且按钮 是开启的状态)。

当脚本执行暂停时,我们就可以进行调试:检查变量,跟踪代码来查看执行出错的位置。

代码风格

我们的代码必须尽可能的清晰和易读。

语法

下面是一个备忘单,其中列出了一些建议的规则:
在这里插入图片描述

下面详细讨论一下这些规则和它们的原因吧。

注意:这些所谓风格只是推荐写法,你可以有自己喜欢的方式去书写漂亮的代码

花括号

在大多数的 JavaScript 项目中,花括号以 “Egyptian” 风格书写

左花括号与相应的关键词在同一行上

而不是新起一行。左括号前还应该有一个空格,如下所示:

if (condition) {
  // do this
  // ...and that
  // ...and that
}

单行构造(如 if (condition) doSomething())也是一个重要的用例。我们是否应该使用花括号?如果是,那么在哪里?

下面是这几种情况的注释,你可以自己判断一下它们的可读性:

  1. 😠 初学者常这样写。非常不好!这里不需要花括号:

    if (n < 0) {alert(`Power ${n} is not supported`);}
    
  2. 😠 拆分为单独的行,不带花括号。永远不要这样做,添加新行很容易出错:

    if (n < 0)
      alert(`Power ${n} is not supported`);
    
  3. 😏 写成一行,不带花括号 —— 如果短的话,也是可以的:

    if (n < 0) alert(`Power ${n} is not supported`);
    
  4. 😃 最好的方式:

    if (n < 0) {
      alert(`Power ${n} is not supported`);
    }
    

对于很短的代码,写成一行是可以接受的:例如 if (cond) return null。但是代码块(最后一个示例)通常更具可读性。

行的长度

没有人喜欢读一长串代码,最好将代码分割一下。

例如:

// 回勾引号 ` 允许将字符串拆分为多行
let str = `
  ECMA International's TC39 is a group of JavaScript developers,
  implementers, academics, and more, collaborating with the community
  to maintain and evolve the definition of JavaScript.
`;

对于 if 语句:

if (
  id === 123 &&
  moonPhase === 'Waning Gibbous' &&
  zodiacSign === 'Libra'
) {
  letTheSorceryBegin();
}

一行代码的最大长度应该在团队层面上达成一致。通常是 80 或 120 个字符。

缩进

有两种类型的缩进:

  • 水平方向上的缩进:2 或 4 个空格。

    一个水平缩进通常由 2 或 4 个空格或者 “Tab” 制表符(Tab 键)构成。

    选择空格而不是 tabs 的优点之一是,这允许你做出比 “Tab” 制表符更加灵活的缩进配置。

    例如,我们可以将参数与左括号对齐,像下面这样:

    show(parameters,
         aligned, // 左边有 5 个空格
         one,
         after,
         another
      ) {
      // ...
    }
    
  • 垂直方向上的缩进:用于将代码拆分成逻辑块的空行。

    即使是单个函数通常也被分割为数个逻辑块。在下面的示例中,初始化的变量、主循环结构和返回值都被垂直分割了:

    function pow(x, n) {
      let result = 1;
      //              <--
      for (let i = 0; i < n; i++) {
        result *= x;
      }
      //              <--
      return result;
    }
    

    插入一个额外的空行有助于使代码更具可读性。写代码时,不应该出现连续超过 9 行都没有被垂直分割的代码。

分号

每一个语句后面都应该有一个分号。即使它可以被跳过。

在 JavaScript 中,极少数情况下,换行符有时不会被解释为分号,这时代码就容易出错。

嵌套的层级

尽量避免代码嵌套层级过深。

例如,在循环中,有时候使用 continue 指令以避免额外的嵌套是一个好主意。

例如,不应该像下面这样添加嵌套的 if 条件:

for (let i = 0; i < 10; i++) {
  if (cond) {
    ... // <- 又一层嵌套
  }
}

我们可以这样写:

for (let i = 0; i < 10; i++) {
  if (!cond) continue;
  ...  // <- 没有额外的嵌套
}

使用 if/elsereturn 也可以做类似的事情。

例如,下面的两个结构是相同的。

第一个:

function pow(x, n) {
  if (n < 0) {
    alert("Negative 'n' not supported");
  } else {
    let result = 1;

    for (let i = 0; i < n; i++) {
      result *= x;
    }

    return result;
  }
}

第二个:

function pow(x, n) {
  if (n < 0) {
    alert("Negative 'n' not supported");
    return;
  }

  let result = 1;

  for (let i = 0; i < n; i++) {
    result *= x;
  }

  return result;
}

但是第二个更具可读性,因为 n < 0 这个“特殊情况”在一开始就被处理了。一旦条件通过检查,代码执行就可以进入到“主”代码流,而不需要额外的嵌套。

函数位置

如果你正在写几个“辅助”函数和一些使用它们的代码,那么有三种方式来组织这些函数。

  1. 在调用这些函数的代码的 上方 声明这些函数:

    // 函数声明
    function createElement() {
      ...
    }
    
    function setHandler(elem) {
      ...
    }
    
    function walkAround() {
      ...
    }
    
    // 调用函数的代码
    let elem = createElement();
    setHandler(elem);
    walkAround();
    
  2. 先写调用代码,再写函数

    // 调用函数的代码
    let elem = createElement();
    setHandler(elem);
    walkAround();
    
    // --- 辅助函数 ---
    function createElement() {
      ...
    }
    
    function setHandler(elem) {
      ...
    }
    
    function walkAround() {
      ...
    }
    
  3. 混合:在第一次使用一个函数时,对该函数进行声明。

大多数情况下,第二种方式更好。

这是因为阅读代码时,我们首先想要知道的是“它做了什么”。如果代码先行,那么在整个程序的最开始就展示出了这些信息。之后,可能我们就不需要阅读这些函数了,尤其是它们的名字清晰地展示出了它们的功能的时候。

自动检查器

检查器是可以自动检查代码样式,并提出改进建议的工具。

它们的妙处在于进行代码风格检查时,还可以发现一些代码错误,例如变量或函数名中的错别字。因此,即使你不想坚持某一种特定的代码风格,我也建议你安装一个检查器。

我使用的是 ESLint

大多数检查器都可以与编辑器集成在一起:只需在编辑器中启用插件并配置代码风格即可。

例如,要使用 ESLint 你应该这样做:

  1. 安装 Node.JS。
  2. 使用 npm install -g eslint 命令(npm 是一个 JavaScript 包安装工具)安装 ESLint。
  3. 在你的 JavaScript 项目的根目录(包含该项目的所有文件的那个文件夹)创建一个名为 .eslintrc 的配置文件。
  4. 在集成了 ESLint 的编辑器中安装/启用插件。大多数编辑器都有这个选项。

下面是一个 .eslintrc 文件的例子:

{
  "extends": "eslint:recommended",
  "env": {
    "browser": true,
    "node": true,
    "es6": true
  },
  "rules": {
    "no-console": 0,
    "indent": 2
  }
}

这里的 "extends" 指令表示我们是基于 “eslint:recommended” 的设置项而进行设置的。之后,我们制定我们自己的规则。

此外,某些 IDE 有内建的检查器,这非常方便,但是不像 ESLint 那样可自定义。

注释

正如我们在代码结构所了解到的那样,注释可以是以 // 开始的单行注释,也可以是 /* ... */ 结构的多行注释。

我们通常通过注释来描述代码怎样工作和为什么这样工作。

糟糕的注释

新手倾向于使用注释来解释“代码中发生了什么”。就像这样:

// 这里的代码会先做这件事(……)然后做那件事(……)
// ……谁知道还有什么……
very;
complex;
code;

但在好的代码中,这种“解释性”注释的数量应该是最少的。严格地说,就算没有它们,代码也应该很容易理解。

关于这一点有一个很棒的原则:“如果代码不够清晰以至于需要一个注释,那么或许它应该被重写。”

配方:分解函数

有时候,用一个函数来代替一个代码片段是更好的,就像这样:

function showPrimes(n) {
  nextPrime:
  for (let i = 2; i < n; i++) {

    // 检测 i 是否是一个质数(素数)
    for (let j = 2; j < i; j++) {
      if (i % j == 0) continue nextPrime;
    }

    alert(i);
  }
}

更好的变体,使用一个分解出来的函数 isPrime

function showPrimes(n) {

  for (let i = 2; i < n; i++) {
    if (!isPrime(i)) continue;

    alert(i);
  }
}

function isPrime(n) {
  for (let i = 2; i < n; i++) {
    if (n % i == 0) return false;
  }

  return true;
}

现在我们可以很容易地理解代码了。函数自己就变成了一个注释。这种代码被称为 自描述型 代码。

配方:创建函数

如果我们有一个像下面这样很长的代码块:

// 在这里我们添加威士忌(译注:国外的一种酒)
for(let i = 0; i < 10; i++) {
  let drop = getWhiskey();
  smell(drop);
  add(drop, glass);
}

// 在这里我们添加果汁
for(let t = 0; t < 3; t++) {
  let tomato = getTomato();
  examine(tomato);
  let juice = press(tomato);
  add(juice, glass);
}

// ...

我们像下面这样,将上面的代码重构为函数,可能会是一个更好的变体:

addWhiskey(glass);
addJuice(glass);

function addWhiskey(container) {
  for(let i = 0; i < 10; i++) {
    let drop = getWhiskey();
    //...
  }
}

function addJuice(container) {
  for(let t = 0; t < 3; t++) {
    let tomato = getTomato();
    //...
  }
}

同样,函数本身就可以告诉我们发生了什么。没有什么地方需要注释。并且分割之后代码的结构也更好了。每一个函数做什么、需要什么和返回什么都非常地清晰。

实际上,我们不能完全避免“解释型”注释。例如在一些复杂的算法中,会有一些出于优化的目的而做的一些巧妙的“调整”。但是通常情况下,我们应该尽可能地保持代码的简单和“自我描述”性。

好的注释

所以,解释性注释通常来说都是不好的。那么哪一种注释才是好的呢?

  • 描述架构

    对组件进行高层次的整体概括,它们如何相互作用、各种情况下的控制流程是什么样的……简而言之 —— 代码的鸟瞰图。

  • 记录函数的参数和用法

例如:

/**
 * 返回 x 的 n 次幂的值。
 *
 * @param {number} x 要改变的值。
 * @param {number} n 幂数,必须是一个自然数。
 * @return {number} x 的 n 次幂的值。
 */
function pow(x, n) {
  ...
}

这种注释可以帮助我们理解函数的目的,并且不需要研究其内部的实现代码,就可以直接正确地使用它。

小结

一个好的开发者的标志之一就是他的注释:它们的存在甚至它们的缺席

好的注释可以使我们更好地维护代码,一段时间之后依然可以更高效地回到代码高效开发。

注释这些内容:

  • 整体架构,高层次的观点。
  • 函数的用法。
  • 重要的解决方案,特别是在不是很明显时。

Polyfill 和转译器

JavaScript 语言在稳步发展。也会定期出现一些对语言的新提议,它们会被分析讨论,如果认为有价值,就会被加入到 https://tc39.github.io/ecma262/ 的列表中,然后被加到规范中。

因此,一个 JavaScript 引擎只能实现标准中的一部分是很常见的情况。

作为程序员,我们希望使用最新的特性。好东西越多越好!

另一方面,如何让我们现代的代码在还不支持最新特性的旧引擎上工作?

有两个工作可以做到这一点:

  1. 转译器(Transpilers)。
  2. 垫片(Polyfills)。

下面一起了解它们的工作原理以及它们在 Web 开发中的位置。

转译器(Transpilers)

转译器是一种可以将源码转译成另一种源码的特殊的软件。它可以解析(“阅读和理解”)现代代码,并使用旧的语法结构对其进行重写,进而使其也可以在旧的引擎中工作。

例如,在 ES2020 之前没有“空值合并运算符” ??。所以,如果访问者使用过时了的浏览器访问我们的网页,那么该浏览器可能就不明白 height = height ?? 100 这段代码的含义。

转译器会分析我们的代码,并将 height ?? 100 重写为 (height !== undefined && height !== null) ? height : 100

// 在运行转译器之前
height = height ?? 100;

// 在运行转译器之后
height = (height !== undefined && height !== null) ? height : 100;

现在,重写了的代码适用于更旧版本的 JavaScript 引擎。

通常,开发者会在自己的计算机上运行转译器,然后将转译后的代码部署到服务器。

说到名字,Babel是最著名的转译器之一。

现代项目构建系统,例如 Webpack,提供了在每次代码更改时自动运行转译器的方法,因此很容易将代码转译集成到开发过程中。

垫片(Polyfills)

新的语言特性可能不仅包括语法结构和运算符,还可能包括内建函数。

例如,Math.trunc(n) 是一个“截断”数字小数部分的函数,例如 Math.trunc(1.23) 返回 1

在一些(非常过时的)JavaScript 引擎中没有 Math.trunc 函数,所以这样的代码会执行失败。

由于我们谈论的是新函数,而不是语法更改,因此无需在此处转译任何内容。我们只需要声明缺失的函数。

更新或添加这些新函数的JS脚本被称为“polyfill”。它“填补”了空白并添加了缺失的实现。

对于这种特殊情况,Math.trunc 的 polyfill 是一个实现它的脚本,如下所示:

if (!Math.trunc) { // 如果没有这个函数
  // 实现它
  Math.trunc = function(number) {
    // Math.ceil 和 Math.floor 甚至存在于上古年代的 JavaScript 引擎中
    // 在本教程的后续章节中会讲到它们
    return number < 0 ? Math.ceil(number) : Math.floor(number);
  };
}

JavaScript 是一种高度动态的语言。脚本可以添加或修改任何函数,甚至包括内建函数。

两个有趣的 polyfill 库:

  • core js支持了很多特性,允许只包含需要的特性。
  • polyfill.io提供带有 polyfill 的脚本的服务,具体取决于特性和用户的浏览器。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值