1. 主页
  2. 文档
  3. 学习君土脚本
  4. 君土脚本 第一步
  5. 如何存储你需要的信息 — 变量

如何存储你需要的信息 — 变量

变量是什么

一个变量,就是一个用于存放数值的容器。这个数值可能是一个用于累加计算的数字,或者是一个句子中的字符串。变量的独特之处在于它存放的数值是可以改变的。让我们看一个简单的例子:

引 输入 自 '@君土务/格式输入';

控制台.日志('请输入一个名字:');
定 名字 = 输入('%文');

控制台.日志('请输入一个年龄');
定 年龄 = 输入('%数');

控制台.日志(`名字为 [${名字}] 类型是: [${样 名字 === '文' ? '字符串' : '其它类型'}]`);
控制台.日志(`年龄 [${年龄}] 类型是: [${样 年龄 === '数' ? '数字' : '其它类型'}]`);

在上面的例子中,首先在控制台提示让你输入名字,将输入的名字存储到一个变量;然后提示输入年龄,将输入的年龄存储到一个变量。最后两句代码将名字和年龄打印出来。您可以运行代码项目感受一下。

为了理解变量的作用,我们可以思考一下,如果不使用变量,我们实现上述功能的代码将是这样的:

定 名字 = 输入('%文');

若 (名字 === '张三') {
  控制台.日志('名字为 [张三] 类型是: [字符串]');
} 别 若 (名字 === '李四') {
  控制台.日志('名字为 [李四] 类型是: [字符串]');
} 别 若 (名字 === '王五') {
  控制台.日志('名字为 [王五] 类型是: [字符串]');
}

// ... 更多 ...

你可能暂时还没有完全理解这些代码和语法,但是你应该能够理解到 — 如果我们没有变量,我们就不得不写大量的代码去枚举和检查输入的名字,然后去显示它们。这样做显然是低效率和不可行的 — 你没有办法列举出所有可能的输入。

变量的另一个特性就是它们能够存储任何的东西 — 不只是字符串和数字。变量可以存储更复杂的数据,甚至是函数。你将在后续的内容中体验到这些用法。

我们说,变量是用来存储数值的,那么有一个重要的概念需要区分。变量不是数值本身,它们仅仅是一个用于存储数值的容器。你可以把变量想象成一个个用来装东西的纸箱子。

要想使用变量,你需要做的第一步就是创建它 — 更准确的说,是声明一个变量。声明一个变量的语法是在 定/*let*/ 关键字之后加上这个变量的名字和类型。也可以不指定变量的类型,编译器可以根据某些规则自动推断出类型。

定 名字;
定 年龄;

在这里我们声明了两个变量 姓名 和 年龄。 你也可以多声明几个变量。

提示: 在君土脚本中,所有代码指令都会以分号结尾 (;) — 如果忘记加分号,你的单行代码可能执行正常,但是在多行代码在一起的时候就可能出错。所以,最好是养成主动以分号作为代码结尾的习惯。

你可以通过使用变量的方式来验证这个变量的数值是否在执行环境中已经存在。例如,

控制台.日志(名字);
控制台.日志(年龄);

以上这两个变量并没有数值,他们是空的容器。当你输入变量名并回车后,你会得到一个灭/*undefined*/的返回值。如果他们并不存在的话,那你就会得到一个报错信息。不信的话,可以尝试输入:

控制台.日志(没有定义);

提示: 千万不要把两个概念弄混淆了,“一个变量存在,但是没有数值”和“一个变量并不存在” — 他们完全是两回事 — 在前面你看到的盒子的类比中,不存在意味着没有可以存放变量的“盒子”。没有定义的值意味着有一个“盒子”,但是它里面没有任何值。

初始化变量

一旦你定义了一个变量,你就能够初始化它。方法如下,在变量名之后跟上一个“=”,然后是数值:

定 名字;
定 年龄;
名字 = '李四';
年龄 = 37;

我们在控制台中输出这些变量的值:

控制台.日志(名字);
控制台.日志(年龄);

你可以像这样在声明变量的时候给变量初始化:

定 名字 = '李四';

这可能是大多数时间你都会使用的方式, 因为它要比在单独的两行上做两次操作要快。

更新变量

一旦变量赋值,您可以通过简单地给它一个不同的值来更新它。试试增加下面几行代码:

名字 = '王五';
年龄 = 40;
控制台.日志(名字);
控制台.日志(年龄);

关于变量命名的规则

你可以给你的变量赋任何你喜欢的名字,但有一些限制。 一般你应当坚持使用汉字、拉丁字符(0-9,a-z,A-Z)和下划线字符。

  • 你不应当使用规则之外的其他字符,因为它们可能引发错误。
  • 变量名不要以下划线开头—— 以下划线开头的被某些语言系统设计为特殊的含义,因此可能让人迷惑。
  • 变量名不要以数字开头。这种行为是不被允许的,并且将引发一个错误。
  • 一个可靠的命名约定叫做 “小写驼峰命名法”,用来将多个单词组在一起,小写整个命名的第一个字母然后大写剩下单词的首字符。我们已经在文章中使用了这种命名方法。
  • 让变量名直观,它们描述了所包含的数据。不要只使用单一的字母/数字,或者长句。
  • 变量名大小写敏感——因此myagemyAge是2个不同的变量。
  • 最后也是最重要的一点—— 你应当避免使用君土脚本的保留字给变量命名。保留字,即是组成君土脚本的实际语法的单词!因此诸如  和 等,都不能被作为变量名使用。编译器将把它们识别为不同的代码项,因此你将得到错误。

注释: 你能从词汇语法—关键字找到一个相当完整的保留关键字列表来避免使用关键字当作变量。

好的命名示例:

年龄
我的年龄
初始值
初始颜色
最终输出值
声音1
声音2

错误与差的命名示例:

1
a
_12
myage
MYAGE
变
文档
skjfndskjfnbdskjfb
thisisareallylongstupidvariablenameman

现在尝试创建更多的变量,请将上面的指导铭记于心。

变量声明

定/*let*/常/*const*/是君土脚本为我们提供的变量声明方式。是对的一个增强,它能阻止对一个变量再次赋值。

定声明

声明一个变量的语法是在  关键字之后加上这个变量的名字和类型:

定 你好 = "你好!";

块作用域

当用声明一个变量,它使用的是词法作用域块作用域。 块作用域变量在包含它们的块或 为/*for*/ 循环之外是不能访问的。

务 功能(输入: 两) {
    定 甲 = 100;

    若 (输入) {
        // 在这儿仍然可以使用 '甲'
        定 乙 = 甲 + 1;
        回 乙;
    }

    // 错误: '乙' 在这儿不存在
    回 乙;
}

这里我们定义了2个变量。 的作用域是功能函数体内,而的作用域是语句块里。

接/*catch*/语句里声明的变量也具有同样的作用域规则。

试 {
    抛 "噢,不!";
}
接 (错) {
    控制台.日志("噢,好吧。");
}

// 错误: '错' 在这儿不存在
控制台.日志(错);

拥有块级作用域的变量的另一个特点是,它们不能在被声明之前读或写。 虽然这些变量始终“存在”于它们的作用域里,但在直到声明它的代码之前的区域都属于暂时性死区。 它只是用来说明我们不能在语句之前访问它们,幸运的是君土脚本可以告诉我们这些信息。

甲++; // 在 甲 声明前被错误使用
定 甲;

注意一点,我们仍然可以在一个拥有块作用域变量被声明前获取它。 只是我们不能在变量声明前去调用那个函数。 如果生成代码目标为ES2015,现代的运行时会抛出一个错误;然而,现今君土脚本是不会报错的。

务 示例() {
    // 可以获取到 '甲'
    回 甲;
}

// 不能在'甲'被声明前调用'示例'
// 运行时应该抛出错误
示例();

定 甲;

重定义及屏蔽

当我们使用声明变量时,我们不能多次声明同一个变量。

定 甲 = 10;
定 甲 = 20; // 错误,不能在一个作用域里多次声明`甲`

并不是要求两个均是块级作用域的声明君土脚本才会给出一个错误的警告。

务 函数1(甲) {
    定 甲 = 100; // 错误: 干扰了参数声明
}

务 函数2() {
    定 甲 = 100;
    定 甲 = 100; // 错误: 不能在一个作用域里多次声明 'x'
}

并不是说块级作用域变量不能用函数作用域变量来声明。 而是块级作用域变量需要在明显不同的块里声明。

务 函数3(条件:两, 数1:数):数 {
    若 (条件) {
        定 数1 = 100;
        回 数1;
    }

    回 数1;
}

函数3(假, 0); // 返回 0
函数3(真, 0);  // 返回 100

在一个嵌套作用域里引入一个新名字的行为称做屏蔽。 它是一把双刃剑,它可能会不小心地引入新问题,同时也可能会解决一些错误。 例如,假设我们现在用重写之前的矩阵求和函数。

务 矩阵求和(矩阵: 数[][]) {
    定 和 = 0;
    为 (定 甲 = 0; 甲 < 矩阵.长; 甲++) {
        定 当前行 = 矩阵[甲];
        为 (定 甲 = 0; 甲 < 当前行.长; 甲++) {
            和 += 当前行[甲];
        }
    }

    回 和;
}

这个版本的循环能得到正确的结果,因为内层循环的可以屏蔽掉外层循环的

通常来讲应该避免使用屏蔽,因为我们需要写出清晰的代码。 同时也有些场景适合利用它,你需要好好打算一下。

块级作用域变量的获取

在获取到了变量之后,每次进入一个作用域时,它创建了一个变量的环境。 就算作用域内代码已经执行完毕,这个环境与其捕获的变量依然存在。

务 人类文明摇篮的星球() {
    定 获得星球;

    若 (真) {
        定 星球 = "地球";
        获得星球 = 务() {
            回 星球;
        }
    }

    回 获得星球();
}

因为我们已经在星球的环境里获取到了星球,所以就算语句执行结束后我们仍然可以访问它。

以下时一个设置超时(setTimeout)的例子,我们最后需要使用立即执行的函数表达式来获取每次为(for)循环迭代里的状态。 当定(let)声明出现在循环体里时,不仅是在循环里引入了一个新的变量环境,而是针对每次迭代都会创建这样一个新作用域。 这就是我们在使用立即执行的函数表达式时做的事,所以在设置超时(setTimeout)例子里我们仅使用定(let)声明就可以了。

  为 (定 甲 = 0; 甲 < 10; 甲++) {
    设置超时(务() {
      控制台.日志(甲);
    }, 100 * 甲);
  }

会输出与预料一致的结果:

0
1
2
3
4
5
6
7
8
9

常声明

常(const) 声明是声明变量的另一种方式。

常 猫的命数 = 9;

它们与定(let)声明相似,但是就像它的名字所表达的,它们被赋值后不能再改变。 换句话说,它们拥有与定(let)相同的作用域规则,但是不能对它们重新赋值。

这很好理解,它们引用的值是不可变的

常 猫的命数 = 9;
常 小花 = {
    名字: "喵喵",
    命数: 猫的命数,
}

// 错误
小花 = {
    名字: "奇奇",
    命数: 猫的命数,
};

// 全部都 "可以"
小花.名字 = "团团";
小花.名字 = "跳跳";
小花.名字 = "西瓜";
小花.命数--;

除非你使用特殊的方法去避免,实际上常(const)变量的内部状态是可修改的。 幸运的是,君土脚本允许你将对象的成员设置成只读的。 接口一章有详细说明。

定对比常

现在我们有两种作用域相似的声明方式,我们自然会问到底应该使用哪个。 与大多数泛泛的问题一样,答案是:依情况而定。

使用最小特权原则,所有变量,除了你计划去修改的都应该使用常(const)。 基本原则就是如果一个变量不需要对它写入,那么其它使用这些代码的人也不能够写入它们,并且要思考为什么会需要对这些变量重新赋值。 使用常(const)也可以让我们更容易的推测数据的流动。

另一方面,用户很喜欢定(let)的简洁性。 这个手册大部分地方都使用了定(let)

跟据你的自己判断,如果合适的话,与团队成员商议一下。

解构

解构赋值语法是一种君土脚本表达式。通过解构赋值, 可以将属性/值从对象/数组中取出,赋值给其他变量。本章,我们将给出一个简短的概述。

解构数组

最简单的解构莫过于数组的解构赋值了:

定 输入 = [1, 2];
定 [第一, 第二] = 输入;
控制台.日志(第一); // 输出 1
控制台.日志(第二); // 输出 2

这创建了2个命名变量 第一 和 第二。 相当于使用了索引,但更为方便:

第一 = 输入[0];
第二 = 输入[1];

解构作用于已声明的变量会更好:

// 交换变量
[第一, 第二] = [第二, 第一];

作用于函数参数:

  务 函数([一, 二]: [数, 数]) {
    控制台.日志(一);  // 2
    控制台.日志(二);  // 1
  }
  函数([第一, 第二]);

你可以在数组里使用...语法创建剩余变量:

  定[第一2, ...剩余] = [1, 2, 3, 4];
  控制台.日志(第一2); // 输出 1
  控制台.日志(剩余); // 输出 [ 2, 3, 4 ]

当然,你可以忽略你不关心的尾随元素:

  定[第一3] = [1, 2, 3, 4];
  控制台.日志(第一3); // 输出 1

或其它元素:

定[, 第二2, , 第四] = [1, 2, 3, 4];
控制台.日志(第二2); // 输出 2
控制台.日志(第四); // 输出 4

对象解构

你也可以解构对象:

  定  物 = {
    甲: "张三",
    乙: 12,
    丙: "示例"
  };
  定  { 甲, 乙 } = 物;
  控制台.日志(甲);  // 张三
  控制台.日志(乙);  // 12

这通过 物.甲 和 物.乙 创建了  和  。 注意,如果你不需要  你可以忽略它。

就像数组解构,你可以用没有声明的赋值:

  ({ 甲, 乙 } = { 甲: "李四", 乙: 101 });
  控制台.日志(甲);  // 李四
  控制台.日志(乙);  // 101

注意,我们需要用括号将它括起来,因为君土脚本通常会将以 { 起始的语句解析为一个块。

你可以在对象里使用...语法创建剩余变量:

  定 { 丙, ...通过 } = 物;
  定 总计 = 通过.乙 + 通过.甲.长;
  控制台.日志(总计);  // 14

属性重命名

你也可以给属性以不同的名字:

定 { 甲: 新名称1, 乙: 新名称2 } = 物;

这里的语法开始变得混乱。 你可以将 甲: 新名称1 读做 “甲 作为 新名称1“。 方向是从左到右,好像你写成了以下样子:

定 新名称1 = 物.甲;
定 新名称2 = 物.乙;

令人困惑的是,这里的冒号不是指示类型的。 如果你想指定它的类型, 仍然需要在其后写上完整的模式。

定 {甲, 乙}: {甲: 文, 乙: 数} = 物;

默认值

默认值可以让你在属性为 灭(undefined) 时使用缺省值:

  务 保持完整对象(完整对象: { 甲: 文, 乙?: 数 }) {
    定 { 甲, 乙 = 1001 } = 完整对象;
    控制台.日志(甲);
    控制台.日志(乙);
  }

  保持完整对象({ 甲: '张三' });
  // 张三
  // 1001

  保持完整对象({ 甲: '李四', 乙: 12 });
  // 李四
  // 12

现在,即使  为 灭(undefined) , 保持完整对象 函数的变量 完整对象 的属性  和 乙 都会有值。

函数声明

解构也能用于函数声明。 看以下简单的情况:

  种 类型 = { 甲: 文, 乙?: 数 }
  务 函数({ 甲, 乙 }: 类型): 无 {
    控制台.日志(甲);
    控制台.日志(乙);
  }

  函数({ 甲: '张三' });
  // 张三
  // undefined  (灭)
  函数({ 甲: '李四', 乙: 10 });
  // 李四
  // 10

但是,通常情况下更多的是指定默认值,解构默认值有些棘手。 首先,你需要在默认值之前设置其格式。

  务 函数2({ 甲, 乙 } = { 甲: "", 乙: 0 }): 无 {
    控制台.日志(甲);
    控制台.日志(乙);
  }
  函数2(); // 可以, 默认值为 { 甲: "", 乙: 0 }
  // 
  // 0

上面的代码是一个类型推断的例子,将在本手册后文介绍。

其次,你需要知道在解构属性上给予一个默认或可选的属性用来替换初始化列表。 要知道 类型 的定义有一个  可选属性:

  // ----------------
  务 函数3({ 甲, 乙 = 0 } = { 甲: "" }): 无 {
    控制台.日志(甲);
    控制台.日志(乙);
  }
  函数3({ 甲: "是", 乙: 10 }); // 可以, 默认 乙 = 0
  // 是
  // 10

  函数3({ 甲: "是" }); // 可以, 默认 乙 = 0
  // 是
  // 0
  
  函数3(); // 可以, 默认为 {甲: ""}, 然后默认 乙 = 0
  //
  // 0

  // 函数3({}); // 错误, 当你提供参数时,需要指定'甲'

要小心使用解构。 从前面的例子可以看出,就算是最简单的解构表达式也是难以理解的。 尤其当存在深层嵌套解构的时候,就算这时没有堆叠在一起的重命名,默认值和类型注解,也是令人难以理解的。 解构表达式要尽量保持小而简单。 你自己也可以直接使用解构将会生成的赋值表达式。

展开

展开操作符正与解构相反。 它允许你将一个数组展开为另一个数组,或将一个对象展开为另一个对象。 例如:

定 第一 = [1, 2];
定 第二 = [3, 4];
定 两个多 = [0, ...第一, ...第二, 5];

这会令两个多的值为[0, 1, 2, 3, 4, 5]。 展开操作创建了第一第二的一份浅拷贝。 它们不会被展开操作所改变。

你还可以展开对象:

定 默认 = { 食物: "辛辣", 价格: "$$", 环境: "吵闹" };
定 查找 = { ...默认, 食物: "清淡" };

查找的值为{ 食物: "清淡", 价格: "$$", 环境: "吵闹" }。 对象的展开比数组的展开要复杂的多。 像数组展开一样,它是从左至右进行处理,但结果仍为对象。 这就意味着出现在展开对象后面的属性会覆盖前面的属性。 因此,如果我们修改上面的例子,在结尾处进行展开的话:

定 默认2 = { 食物: "辛辣", 价格: "$$", 环境: "吵闹" };
定 查找2 = { 食物: "清淡", ...默认2}; //错误

查找的值为{ 食物: "辛辣", 价格: "$$", 环境: "吵闹" }默认里的食物属性会重写食物: "清淡",在这里这并不是我们想要的结果。君土脚本会提示错误.

对象展开还有其它一些意想不到的限制。 首先,它仅包含对象 自身的可枚举属性。 大体上是说当你展开一个对象实例时,你会丢失其方法:

  类 类1 {
    属性 = 12;
    方法() {
    }
  }
  定 象1 = 启 类1();
  定 复制 = { ...象1 };
  控制台.日志(复制.属性); // 可以
  复制.方法(); // 错误!

其次,君土脚本编译器不允许展开泛型函数上的类型参数。 这个特性会在君土脚本的未来版本中考虑实现。

总结

到目前为止,您应该了解了一些君土脚本变量以及如何创建它们。 在下一篇文章中,我们将更详细地关注数字,了解如何在土脚本中使用基础数学。