总结初学JavaScript的基础

初识JavaScript

JavaScript 最初被创建的目的是“使网页更生动”。

这种编程语言写出来的程序被称为 脚本。它们可以被直接写在网页的 HTML 中,在页面加载的时候自动执行。

脚本被以纯文本的形式提供和执行。它们不需要特殊的准备或编译即可运行。

如今,JavaScript 不仅可以在浏览器中执行,也可以在服务端执行,甚至可以在任意搭载了 JavaScript 引擎 的设备中执行。

浏览器中嵌入了 JavaScript 引擎,有时也称作“JavaScript 虚拟机”。

不同的引擎有不同的“代号”,例如:

  • V8) —— Chrome 和 Opera 中的 JavaScript 引擎。
  • SpiderMonkey —— Firefox 中的 JavaScript 引擎。
  • ……还有其他一些代号,像 “Chakra” 用于 IE,“ChakraCore” 用于 Microsoft Edge,“Nitro” 和 “SquirrelFish” 用于 Safari,等等。

上面这些术语很容易记住,因为它们经常出现在开发者的文章中。我们也会用到这些术语。例如,如果“V8 支持某个功能”,那么我们可以认为这个功能大概能在 Chrome 和 Opera 中正常运行。

JavaScript 作用

现代的 JavaScript 是一种“安全的”编程语言。它不提供对内存或 CPU 的底层访问,因为它最初是为浏览器创建的,不需要这些功能。

JavaScript 的能力很大程度上取决于它运行的环境。例如,Node.js 支持允许 JavaScript 读取/写入任意文件,执行网络请求等的函数。

浏览器中的 JavaScript 可以做与网页操作、用户交互和 Web 服务器相关的所有事情。

例如,浏览器中的 JavaScript 可以做下面这些事:

  • 在网页中添加新的 HTML,修改网页已有内容和网页的样式。
  • 响应用户的行为,响应鼠标的点击,指针的移动,按键的按动。
  • 向远程服务器发送网络请求,下载和上传文件(所谓的 AJAX) 和 COMET) 技术)。
  • 获取或设置 cookie,向访问者提出问题或发送消息。
  • 记住客户端的数据(“本地存储”)。

JavaScript的组成

  1. ECMAScript (标准:ECMA-262) :基础语言部分 (基础、面向对象 等)
  2. DOM (标准:W3C ) : 节点操作
  3. BOM (无标准) :浏览器的操作

JavaScript 的使用

  • 使用 <script> 标签 , 注意 <script> 标签可以在网页代码中的任意地方, 因为JS是同步执行的,但为了避免出现JS堵塞,最好些在 <body>
  • <a> 标签中使用JavaScript代码 , 但是会影响性能
    1. <a href="javascript: alert("大家好")" > 大家好 </a>
  • 外部引入JS脚本
    1. <script src="01_test.js" type="text/javascript" async="async" ></script>
    2. <!--
    3. src: 表示引入的外部JS文件的路径和文件名(只能用于引入JS脚本)
    4. async : 异步加载JS代码, 在加载DOM元素的同时,可以同时运行js代码
    5. -->

JavaScript 中的标识符

标识符,就是指变量、函数、属性的名字、或者函数的参数 ;

标识符定义的规则

  • 第一个字符必须是一个字母、下划线、或一个美元符号
  • 其他字符可以是字母、下划线、美元符号、或数字
  • 不能把关键字、保留字、true、false、null、作为标识符

JavaScript 中的注释

  1. // 定义变量
  2. var a = 10; // 单行注释, 一般写在代码后面,表示代码的意思
  3. var b = 20;
  4. console.log(a + b);
  5. // 多行注释
  6. /*
  7. 使用 prompt()函数
  8. 输出到浏览器中
  9. */
  10. var user = prompt("请输入你的姓名: ");
  11. document.write("你的名字是:" + user + "!");

JavaScript 中的变量

var

  1. var a, b, c;
  2. // 赋值
  3. var test = true;

let

  1. let message ; // 定义变量

在 let 中 声明两次会触发 error

一个变量应该只被声明一次。

对同一个变量进行重复声明会触发 error

const

声明一个常数(不变)变量,可以使用 const 而非 let

  1. const myBirthday = '18.21';
  2. // 使用 const 声明的变量称为“常量”。它们不能被修改,如果你尝试修改就会发现报错:
  3. myBirthday = '01.01.2001'; // 错误,不能对常量重新赋值

当程序员能确定这个变量永远不会改变的时候,就可以使用 const 来确保这种行为,并且清楚地向别人传递这一事实。

我们可以使用 varletconst 声明变量来存储数据。

  • let — 现代的变量声明方式。
  • var — 老旧的变量声明方式。一般情况下,我们不会再使用它。但是,我们会在 旧时的 “var” 章节介绍 varlet 的微妙差别,以防你需要它们。
  • const — 类似于 let,但是变量的值无法被修改。

变量应当以一种容易理解变量内部是什么的方式进行命名。

JavaScript 的数据类型

字符串 描述
Undefined 未定义
Boolean 布尔值
String 字符串
Number 数值
Object 对象
BigInt 大数字类型
Symbol 唯一标识符
Function 函数
  1. typeof undefined // "undefined"
  2. typeof 0 // "number"
  3. typeof 10n // "bigint"
  4. typeof true // "boolean"
  5. typeof "foo" // "string"
  6. typeof Symbol("id") // "symbol"
  7. typeof Math // "object" (1)
  8. typeof null // "object" (2)
  9. typeof alert // "function" (3)

Number类型

在现代 JavaScript 中,数字(number)有两种类型:

  1. JavaScript 中的常规数字以 64 位的格式 IEEE-754 存储,也被称为“双精度浮点数”。这是我们大多数时候所使用的数字,我们将在本章中学习它们。
  2. BigInt 数字,用于表示任意长度的整数。有时会需要它们,因为常规数字不能超过 253 或小于 -253。由于仅在少数特殊领域才会用到 BigInt,因此我们在特殊的章节 BigInt 中对其进行了介绍。

加字母 e

在 JavaScript 中,我们通过在数字后附加字母 “e”,并指定零的数量来缩短数字:

  1. let billion = 1e9; // 10亿, 字面意思:数字 1 后面跟 9 个 0
  2. console.log(7.3e9); // 73 亿(7,300,000,000)

换句话说,"e" 把数字乘以 1 后面跟着给定数量的 0 的数字。

  1. 1e3 = 1 * 1000
  2. 1.23e6 = 1.23 * 1000000;

进制数字

  1. // 0x 表示十六进制
  2. console.log(0xff); // 255
  3. console.log(0xFF); // 255(一样,大小写没影响)
  4. // 0b
  5. let a = 0b11111111; // 二进制形式的 255
  6. // 0o
  7. let b = 0o377; // 八进制形式的 255
  8. alert( a == b ); // true,两边是相同的数字,都是 255

toString(base) 【 进制转换方法】

toString(base) 将数字转为多少进制, num.toString(base) 返回在给定 base 进制数字系统中 num 的字符串表示形式。base 的范围可以从 236。默认情况下是 10

  1. // toString() 将数字转为 多少进制
  2. let num = 255;
  3. console.log(num.toString(2));
  4. console.log(num.toString(8));
  5. console.log(num.toString(16));
  6. console.log(num.toString(36));

Math.floo() 【向上舍入】

Math.floo() 向下舍入:3.1 变成 3-1.1 变成 -2

  1. let number = 3.2;
  2. // Math.floor() : 向下舍入:3.1 变成 3,-1.1 变成 -2。
  3. console.log(Math.floor(number)); // 3

Math.ceil() 【向下舍入】

Math.ceil() 向上舍入:3.1 变成 4-1.1 变成 -1

  1. let number = 3.2;
  2. // Math.ceil() : 向上舍入:3.1 变成 4,-1.1 变成 -1。
  3. console.log(Math.ceil(number)); // 4

Math.round() 【四舍五入】

Math.round() 向最近的整数舍入:3.1 变成 33.6 变成 4-1.1 变成 -1

  1. let number = 3.2;
  2. // Math.round() : 向最近的整数舍入:3.1 变成 3,3.6 变成 4,-1.1 变成 -1。
  3. console.log(Math.round(number)); // 3
  4. console.log(Math.round(3.6)); // 4
  5. // 四舍五入

toFixed() 【保留n位小数】

将数字舍入到小数点后 n 位,并以字符串形式返回结果

舍入小数点后n位

  1. 乘除法 : 例如,要将数字舍入到小数点后两位,我们可以将数字乘以 100(或更大的 10 的整数次幂),调用舍入函数,然后再将其除回。
    1. let number1 = 1.23456;
    2. console.log(Math.floor(number1 * 100) / 100); // 1.23456 -> 123.345 -> 123 > 1.23
  1. 使用toFixed(n) 将数字舍入到小数点后 n 位,并以字符串形式返回结果
    1. // 另一种方法 : 使用函数 toFixed(n) 将数字舍入小数点后 n 位, 返回的是 字符串 类型 ,
    2. // 但是 我们可以使用一元加号或 Number() 调用,将其转换为数字:+ num.toFixed(5)。
    3. console.log(number1.toFixed(2)); // "1.23"
    4. console.log(12.34.toFixed(5)); // "12.34000" 不够添加0
  1. toFixed(n) 返回的字符串类型转为 Number 类型
    1. // toFixed 返回的字符串 转为 number 类型
    2. console.log(+number1.toFixed(2)); // 使用 + 号将数字的字符串转为 数字类型
    3. console.log(Number(number1.toFixed(3))); // Number() : str --> num

Math.random() 【一个随机数】

Math.random()返回一个从 0 到 1 的随机数(不包括 1)

  1. // Math.random() 返回一个从 0 到 1 的随机数(不包括 1)
  2. console.log(Math.random());
  3. console.log(Math.random());
  4. console.log(Math.random());
  5. // 获取随机数字的方法
  6. console.log(Math.round(Math.random() * 100)); // 0.9230314578103669 -> 92.30314578103669 --> 92
  7. console.log(Math.round(Math.random() * 10)); // 0.9230314578103669 -> 9.230314578103669 -> 9

Math.max(a, b, c…)

Math.min(a, b, c…)

从任意数量的参数中返回最大/最小值。

  1. // Math.max(a,b,c) / Math.min(a,b,c) 从任意数量的参数中返回最大/最小值。
  2. console.log(Math.max(12, 4123, 5, 123));
  3. console.log(Math.min(12, 4123, 5, 123));

Math.pow(n, power) 【n次幂】

  1. // Math.pow(n, power) 返回 n 的给定(power)次幂
  2. console.log(Math.pow(2, 3)); // 2 * 2 * 2

Math 对象中还有更多函数和常量,包括三角函数,你可以在 Math 对象文档 中找到这些内容。

isNaN 【判断数字】

isNaN(value) 将其参数转换为数字,然后测试它是否为 NaN

  1. console.log(isNaN(NaN)); // true
  2. console.log(isNaN("str")); // true

但是我们需要这个函数吗?我们不能只使用 === NaN 比较吗?不好意思,这不行。值 “NaN” 是独一无二的,它不等于任何东西,包括它自身:

  1. console.log(NaN === NaN); // false

isFinite 【判断数字】

isFinite(value) 将其参数转换为数字,如果是常规数字,则返回 true,而不是 NaN/Infinity/-Infinity

  1. alert( isFinite("15") ); // true
  2. alert( isFinite("str") ); // false,因为是一个特殊的值:NaN
  3. alert( isFinite(Infinity) ); // false,因为是一个特殊的值:Infinity

不精确计算

如果一个数字太大,则会溢出 64 位存储,并可能会导致无穷大:

  1. console.log(1e500); // Infinty 无穷大

这可能不那么明显,但经常会发生的是,精度的损失。

注意 0.1 + 0.2 的问题

  1. console.log(Boolean(0.1 + 0.2 == 0.3)) // flase
  2. console.log(0.1 + 0.2); // 0.30000000000000004
  3. /**
  4. 原因:
  5. 一个数字以其二进制的形式存储在内存中,一个 1 和 0 的序列。但是在十进制数字系统中看起来很简单的 `0.1`,`0.2` 这样的小数,实际上在二进制形式中是无限循环小数。
  6. 换句话说,什么是 `0.1`?`0.1` 就是 `1` 除以 `10`,`1/10`,即十分之一。在十进制数字系统中,这样的数字表示起来很容易。将其与三分之一进行比较:`1/3`。三分之一变成了无限循环小数 `0.33333(3)`。
  7. 在十进制数字系统中,可以保证以 `10` 的整数次幂作为除数能够正常工作,但是以 `3` 作为除数则不能。也是同样的原因,在二进制数字系统中,可以保证以 `2` 的整数次幂作为除数时能够正常工作,但 `1/10` 就变成了一个无限循环的二进制小数。
  8. 使用二进制数字系统无法 **精确** 存储 *0.1* 或 *0.2*,就像没有办法将三分之一存储为十进制小数一样。
  9. IEEE-754 数字格式通过将数字舍入到最接近的可能数字来解决此问题。这些舍入规则通常不允许我们看到“极小的精度损失”,但是它确实存在。
  10. */
  11. // 0.1 + 0.2 == 0.3 // false
  12. console.log(0.1 + 0.2); // 0.30000000000000004
  13. console.log(Boolean(0.1 + 0.2 == 0.3)); // false
  14. console.log(0.1.toFixed(20)); // 0.10000000000000000555
  15. console.log(0.2.toFixed(20)); // 0.20000000000000001110

常见的Number类型算法

这是一个无限循环。它永远不会结束。为什么?

  1. let i = 0;
  2. while (i != 10) {
  3. i += 0.2;
  4. }

解决方案

那是因为 i 永远不会等于 10

运行下面这段代码来查看 i实际 值:

  1. let i = 0;
  2. while (i < 11) {
  3. i += 0.2;
  4. if (i > 9.8 && i < 10.2) alert( i );
  5. }

它们中没有一个恰好是 10

之所以发生这种情况,是因为对 0.2 这样的小数时进行加法运算时出现了精度损失。

结论:在处理小数时避免相等性检查。

编写一个 random(min, max) 函数,用以生成一个在 minmax 之间的随机浮点数(不包括 max))。

  1. /**
  2. * 我们需要将区间 0…1 中的所有值“映射”为范围在 min 到 max 中的值。
  3. 这可以分两个阶段完成:
  4. 如果我们将 0…1 的随机数乘以 max-min,则随机数的范围将从 0…1 增加到 0..max-min。
  5. 现在,如果我们将随机数与 min 相加,则随机数的范围将为 min 到 max。
  6. */
  7. let random = (min, max) => min + Math.random() * (max - min);
  8. console.log(random(1, 5));

创建一个函数 randomInteger(min, max),该函数会生成一个范围在 minmax 中的随机整数,包括 minmax

这个题目有很多正确的解决方案。其中之一是调整取值范围的边界。为了确保相同的取值范围,我们可以生成从 0.5 到 3.5 的值,从而将所需的概率添加到取值范围的边界:

  1. function randomInteger(min, max) {
  2. // 现在范围是从 (min-0.5) 到 (max+0.5)
  3. let rand = min - 0.5 + Math.random() * (max - min + 1);
  4. return Math.round(rand);
  5. }
  6. console.log(randomInteger(1,3));

String 类型

字符串是不可变的

在 JavaScript 中,字符串不可更改。改变字符是不可能的。

  1. // 字符串不可变
  2. let str = 'H1';
  3. str[0] = "O"; // error
  4. console.log(str);

通常的解决方法是创建一个新的字符串,并将其分配给 str 而不是以前的字符串。

  1. let str = 'Hi';
  2. str = 'h' + str[1]; // 替换字符串
  3. alert( str ); // hi

引号

  1. let single = 'single-quoted';
  2. let double = "double-quoted";
  3. let backticks = `backticks`;

引号和双引号基本相同。但是,反引号允许我们通过 ${…} 将任何表达式嵌入到字符串中:

  1. function sum(a, b) return a + b;
  2. console.log(`1 + 2 = ${sum(1, 2)}.`);

使用反引号的另一个优点是它们允许字符串跨行:

  1. let guestList = `Guests:
  2. * John
  3. * Pete
  4. * Mary
  5. `;
  6. alert(guestList); // 客人清单,多行

特殊字符

  1. let guestList = "Guests:\n * John\n * Pete\n * Mary";
  2. alert(guestList); // 一个多行的客人列表
字符 描述
\\n 换行
\\r 回车:不单独使用。Windows 文本文件使用两个字符 \\r\\n
的组合来表示换行。
\\'
, \\"
引号
\\\\ 反斜线
\\t 制表符
\\b
, \\f
, \\v
退格,换页,垂直标签 —— 为了兼容性,现在已经不使用了。
\\xXX 具有给定十六进制 Unicode XX
的 Unicode 字符,例如:'\\x7A'
'z'
相同。
\\uXXXX 以 UTF-16 编码的十六进制代码 XXXX
的 unicode 字符,例如 \\u00A9
—— 是版权符号 ©
的 unicode。它必须正好是 4 个十六进制数字。
\\u{X…XXXXXX}
(1 到 6 个十六进制字符)
具有给定 UTF-32 编码的 unicode 符号。一些罕见的字符用两个 unicode 符号编码,占用 4 个字节。这样我们就可以插入长代码了。

unicode 示例:

  1. // unicode 字符
  2. console.log("\u00A9"); // ©
  3. console.log("\u{20331}"); //𠌱
  4. console.log("\u{1F60D}"); // 😍

length 【字符串长度】

length 属性表示字符串长度:

  1. console.log(`My\n`.lenght); // 3

**length** 是一个属性

掌握其他编程语言的人,有时会错误地调用 str.length() 而不是 str.length。这是行不通的。

charAt(pos) 【访问字符】

要获取在 pos 位置的一个字符,可以使用方括号 [pos] 或者调用 str.charAt(pos) 方法。第一个字符从零位置开始:

  1. let str = `Hello`;
  2. console.log(str[0]); // 一般用 [] 的方法
  3. console.log(str.charAt(0));
  4. console.log(str.charAt(str.length - 1));
  5. console.log(str[str.length - 1]);

方括号是获取字符的一种现代化方法,而 charAt 是历史原因才存在的。

它们之间的唯一区别是,如果没有找到字符,[] 返回 undefined,而 charAt 返回一个空字符串:

  1. let str = `Hello`;
  2. alert( str[1000] ); // undefined
  3. alert( str.charAt(1000) ); // ''(空字符串)

我们也可以使用 for..of 遍历字符:

  1. for (let char of "Hello") {
  2. alert(char); // H,e,l,l,o(char 变为 "H",然后是 "e",然后是 "l" 等)
  3. }

toLowerCase() 【字符串小写】

  1. console.log('Interface'.toLowerCase()); // 变为小写 interface

toUpperCase() 【字符串大写】

  1. console.log('Interface'.toUpperCase()); // 变为大写 INTERFACE

indexOf(substr, pos) 【查找字符串】

它从给定位置 pos 开始,在 str 中查找 substr,如果没有找到,则返回 -1,否则返回匹配成功的位置。

  1. let str = 'Widget with id';
  2. alert( str.indexOf('Widget') ); // 0,因为 'Widget' 一开始就被找到
  3. alert( str.indexOf('widget') ); // -1,没有找到,检索是大小写敏感的
  4. alert( str.indexOf("id") ); // 1,"id" 在位置 1 处(……idget 和 id)

可选的第二个参数允许我们从给定的起始位置开始检索。

例如,"id" 第一次出现的位置是 1。查询下一个存在位置时,我们从 2 开始检索:

  1. let str = "Widget with id ";
  2. console.log(str.indexOf('id',2)); // 12

如果我们对所有存在位置都感兴趣,可以在一个循环中使用 indexOf。每一次新的调用都发生在上一匹配位置之后:

  1. let str = 'As sly as a fox, as strong as an ox';
  2. let target = 'as'; // 这是我们要查找的目标
  3. let pos = 0;
  4. while (true) {
  5. let foundPos = str.indexOf(target, pos);
  6. if (foundPos == -1) break;
  7. alert( `Found at ${foundPos}` );
  8. pos = foundPos + 1; // 继续从下一个位置查找
  9. }
  10. /// 相同的算法可以简写:
  11. let str = "As sly as a fox, as strong as an ox";
  12. let target = "as";
  13. let pos = -1;
  14. while ((pos = str.indexOf(target, pos + 1)) != -1) {
  15. alert( pos );
  16. }

lastIndexOf(substr, pot)

还有一个类似的方法 str.lastIndexOf(substr, position),它从字符串的末尾开始搜索到开头。

它会以相反的顺序列出这些事件。

  1. let str = "As sly as a fox, as strong as an ox";
  2. console.log(str.lastIndexOf("as")); // 27

includes(substr, pos) 【判断字符串是否存在】

跟据 str 中是否包含 substr 来返回 true/false

  1. alert( "Widget with id".includes("Widget") ); // true
  2. alert( "Hello".includes("Bye") ); // false

str.includes 的第二个可选参数是开始搜索的起始位置:

  1. alert( "Midget".includes("id") ); // true
  2. alert( "Midget".includes("id", 3) ); // false, 从位置 3 开始没有 "id"

startWith() 和 endsWith()

方法 str.startsWithstr.endsWith 的功能与其名称所表示的意思相同:

  1. alert( "Widget".startsWith("Wid") ); // true,"Widget" 以 "Wid" 开始
  2. alert( "Widget".endsWith("get") ); // true,"Widget" 以 "get" 结束

获取字符串

str.slice(start ,end) 【获取字符串】

str.slice(start end)返回字符串从 start 到(但不包括)end 的部分

  1. let str = "stringify";
  2. console.log(str.slice(0,5)); // strin [0,5)
  1. // slice(,end)
  2. let str_1 = "stringify";
  3. console.log(str_1.slice(0, 5)); // strin
  4. console.log(str_1.slice(0, 1)); // s
  5. // 如果没有第二个参数,slice 会一直运行到字符串末尾:
  6. console.log(str_1.slice(2)); // 表示从第2个字符开始取, 一直到最后结束
  7. //start/end 也有可能是负值。它们的意思是起始位置从字符串结尾计算:
  8. console.log(str_1.slice(0, -1));

str.substring(start ,end) 【获取字符串】

返回字符串在 startend 之间 的部分。

这与 slice 几乎相同,但它允许 start 大于 end。不支持负参数(不像 slice),它们被视为 0

  1. let str = "stringify";
  2. // 这些对于 substring 是相同的
  3. alert( str.substring(2, 6) ); // "ring"
  4. alert( str.substring(6, 2) ); // "ring"
  5. // ……但对 slice 是不同的:
  6. alert( str.slice(2, 6) ); // "ring"(一样)
  7. alert( str.slice(6, 2) ); // ""(空字符串)
  8. console.log(str_1.substring(str_1.length - 1, 0));

str.substr(start , length)

返回字符串从 start 开始的给定 length 的部分。与以前的方法相比,这个允许我们指定 length 而不是结束位置:

  1. let str = "stringify";
  2. alert( str.substr(2, 4) ); // 'ring',从位置 2 开始,获取 4 个字符

第一个参数可能是负数,从结尾算起:

  1. let str = "stringify";
  2. alert( str.substr(-4, 2) ); // 'gi',从第 4 位获取 2 个字符
方法 选择方式…… 负值参数
slice(start, end) start
end
(不含 end
允许
substring(start, end) start
end
之间(包括 start
,但不包括 end
负值代表 0
substr(start, length) start
开始获取长为 length
的字符串
允许 start
为负数

比较字符串

  1. 小写字母总是大于大写字母:
    1. alert( 'a' > 'Z' ); // true
  1. 带变音符号的字母存在“乱序”的情况:
    1. alert( 'Österreich' > 'Zealand' ); // true

    如果我们对这些国家名进行排序,可能会导致奇怪的结果。通常,人们会期望 Zealand 在名单中的 Österreich 之后出现。

str.codePointAt(pos) 【字符串编码】

返回在 pos 位置的字符代码

  1. console.log("z".codePoinAt(0)); // 122
  2. console.log("z".codePoinAt(0)); // 90

str.fromCodePoint(code) 【编码字符串】

通过数字 code 创建字符

  1. console.log(String.fromCodePoint(90)); // Z

我们还可以用 \u 后跟十六进制代码,通过这些代码添加 unicode 字符:

  1. // 在十六进制系统中 90 为 5a
  2. alert( '\u005a' ); // Z

str.localeCompare(str2) 【比较字符串】

会根据语言规则返回一个整数,这个整数能指示字符串 str 在排序顺序中排在字符串 str2 前面、后面、还是相同

  • 如果 str 排在 str2 前面,则返回负数。
  • 如果 str 排在 str2 后面,则返回正数。
  • 如果它们在相同位置,则返回 0
  1. let str_1 = "yellowsea";
  2. console.log("yellowsea".localeCompare(str_1)); // 0
  3. console.log("test".localeCompare(str_1)); // -1
  4. console.log("yellowsea0644".localeCompare(str_1)); // 1

str.trim() 【删除字符串前后空格】

  1. // str.trim() 删除字符串前后的空格
  2. let str_tab = " yellowsea ";
  3. console.log(str_tab.trim()); // yellow

str.repeat(n) 【重复字符串n次】

  1. // str.repeat() 重复字符串
  2. console.log(str_tab.repeat(2)); // yellowsea yellowsea

常见字符串操作方法

  1. // 搜字母大写
  2. function ucFirst(str) {
  3. if (!str) return str;
  4. return str[0].toUpperCase() + str.slice(1); // slice(1) 从1到最后
  5. }
  6. alert( ucFirst("john") ); // John

数组类型

声明数组

  1. let arr = new Array();
  2. let arr = [];

绝大多数情况下使用的都是第二种语法。我们可以在方括号中添加初始元素:

  1. let fruits = ['Apple', "Orange", 'Plum'];

我们可以通过方括号中的数字获取元素:

  1. let fruits = ["Apple", "Orange", "Plum"];
  2. alert( fruits[0] ); // Apple
  3. alert( fruits[1] ); // Orange
  4. alert( fruits[2] ); // Plum

可以替换元素:

  1. fruits[2] = 'Pear'; // 现在变成了 ["Apple", "Orange", "Pear"]

……或者向数组新加一个元素:

  1. fruits[3] = 'Lemon'; // 现在变成 ["Apple", "Orange", "Pear", "Lemon"]

length

属性的值是数组中元素的总个数:

  1. let fruits = ["Apple", "Orange", "Plum"];
  2. console.log(fruits.length); // 3

输出整个数组

  1. let fruits = ["Apple", "Orange", "Plum"];
  2. console.log(fruits);

数组可以存储任何类型的元素。

  1. // 混合值
  2. let arr = [ 'Apple', { name: 'John' }, true, function() { alert('hello'); } ];
  3. // 获取索引为 1 的对象然后显示它的 name
  4. alert( arr[1].name ); // John
  5. // 获取索引为 3 的函数并执行
  6. arr[3](); // hello

队列和栈

队列(queue))是最常见的使用数组的方法之一。在计算机科学中,这表示支持两个操作的一个有序元素的集合:

  • push 在末端添加一个元素.
  • shift 取出队列首端的一个元素,整个队列往前移,这样原先排第二的元素现在排在了第一。

JavaScript基础 - 图1

这两种操作数组都支持。

队列的应用在实践中经常会碰到。例如需要在屏幕上显示消息队列。

数组还有另一个用例,就是数据结构 )。

它支持两种操作:

  • push 在末端添加一个元素.
  • pop 从末端取出一个元素.

所以新元素的添加和取出都是从“末端”开始的。

栈通常被被形容成一叠卡片:要么在最上面添加卡片,要么从最上面拿走卡片:

JavaScript基础 - 图2

对于栈来说,

最后放进去的内容是最先接收的,也叫做 LIFO(Last-In-First-Out),

即后进先出法则。而与队列相对应的叫做 FIFO(First-In-First-Out),即先进先出。

pop() 【末端取出元素】

pop : 取出并且返回数组的最后一个元素

  1. //`pop` : 取出并且返回数组的最后一个元素
  2. let fruits = ["Apple", "Orange", "Pear"];
  3. console.log(fruits.pop()); // 移除 Pear 然后 打印出来
  4. console.log(fruits); // ["Apple", "Orange"]

push() 【数组末端添加元素】

push 在数组末端添加元素

  1. // push , 在数组末端添加元素 ,不会返回 添加的元素
  2. // console.log(fruits.push("Pear")); // 3 返回元素个数
  3. let fruits = ["Apple", "Orange"];
  4. fruits.push("Pear")
  5. console.log(fruits);
  6. //调用 fruits.push(...) 与 fruits[fruits.length] = ... 是一样的。
  7. fruits[fruits.length - 1] = 'Pear';
  8. console.log(fruits);
  9. // 可以一次添加多个元素
  10. fruits.push("Orange", "Peach");

shift() 【数组首端移除元素】

  1. // shift() 取出数组的第一个元素并返回它:
  2. let fruits = ["Apple", "Orange", "Pear"];
  3. console.log(fruits.shift()); // 移除Apple ,然后打印 Apple
  4. console.log(fruits)

unshift() 【数组首端添加元素】

  1. // unshift 在数组的 首段 添加元素
  2. let fruits = ["Apple", "Orange", "Pear"];
  3. fruits.unshift("Apple");
  4. // console.log(fruits.unshift("Apple")); // 4 数组长度
  5. console.log(fruits);
  6. // 可以一次添加多个元素
  7. fruits.unshift("Pineapple", "Lemon");

数组性能

push/pop 方法运行的比较快,而 shift/unshift 比较慢。

JavaScript基础 - 图3

为什么作用于数组的末端会比首端快呢?让我们看看在执行期间都发生了什么:

  1. fruits.shift(); // 从首端取出一个元素

只获取并移除数字 0 对应的元素是不够的。其它元素也需要被重新编号。

shift 操作必须做三件事:

  1. 移除索引为 0 的元素。
  2. 把所有的元素向左移动,把索引 1 改成 02 改成 1 以此类推,对其重新编号。
  3. 更新 length 属性。

JavaScript基础 - 图4

数组里的元素越多,移动它们就要花越多的时间,也就意味着越多的内存操作。

unshift 也是一样:为了在数组的首端添加元素,我们首先需要将现有的元素向右移动,增加它们的索引值。

push/pop 是什么样的呢?它们不需要移动任何东西。如果从末端移除一个元素,pop 方法只需要清理索引值并缩短 length 就可以了。

pop 操作的行为:

  1. fruits.pop(); // 从末端取走一个元素

JavaScript基础 - 图5

**pop** 方法不需要移动任何东西,因为其它元素都保留了各自的索引。这就是为什么 **pop** 会特别快。

push 方法也是一样的。

数组误用几种方式

  • 添加一个非数字的属性,比如 arr.test = 5
  • 制造空洞,比如:添加 arr[0],然后添加 arr[1000] (它们中间什么都没有)。
  • 以倒序填充数组,比如 arr[1000]arr[999] 等等

数组循环

遍历数组最古老的方式就是 for 循环:

  1. let arr = ["Apple", "Orange", "Pear"];
  2. for (let i = 0; i < arr.length; i++) {
  3. alert( arr[i] );
  4. }

但对于数组来说还有另一种循环方式,for..of

  1. let fruits = ["Apple", "Orange", "Plum"];
  2. // 遍历数组元素
  3. for (let fruit of fruits) {
  4. alert( fruit );
  5. }

for..of 不能获取当前元素的索引,只是获取元素值,但大多数情况是够用的。而且这样写更短。

技术上来讲,因为数组也是对象,所以使用 for..in 也是可以的:

  1. let arr = ["Apple", "Orange", "Pear"];
  2. for (let key in arr) {
  3. alert( arr[key] ); // Apple, Orange, Pear
  4. }

但这其实是一个很不好的想法。会有一些潜在问题存在:

  1. for..in 循环会遍历 所有属性,不仅仅是这些数字属性。
    在浏览器和其它环境中有一种称为“类数组”的对象,它们 看似是数组。也就是说,它们有 length 和索引属性,但是也可能有其它的非数字的属性和方法,这通常是我们不需要的。for..in 循环会把它们都列出来。所以如果我们需要处理类数组对象,这些“额外”的属性就会存在问题。
  2. for..in 循环适用于普通对象,并且做了对应的优化。但是不适用于数组,因此速度要慢 10-100 倍。当然即使是这样也依然非常快。只有在遇到瓶颈时可能会有问题。但是我们仍然应该了解这其中的不同。

通常来说,我们不应该用 for..in 来处理数组。

数组中的 length

当我们修改数组的时候,length 属性会自动更新。准确来说,它实际上不是数组里元素的个数,而是最大的数字索引值加一。

例如,一个数组只有一个元素,但是这个元素的索引值很大,那么这个数组的 length 也会很大:

  1. let fruits = [];
  2. fruits[123] = "Apple";
  3. alert( fruits.length ); // 124

要知道的是我们通常不会这样使用数组。

length 属性的另一个有意思的点是它是可写的。

如果我们手动增加它,则不会发生任何有趣的事儿。但是如果我们减少它,数组就会被截断。该过程是不可逆的,下面是例子:

  1. let arr = [1, 2, 3, 4, 5];
  2. arr.length = 2; // 截断到只剩 2 个元素
  3. alert( arr ); // [1, 2]
  4. arr.length = 5; // 又把 length 加回来
  5. alert( arr[3] ); // undefined:被截断的那些数值并没有回来

所以,清空数组最简单的方法就是:arr.length = 0;

new Array()

这是创建数组的另一种语法:

  1. let arr = new Array("Apple", "Pear", "etc");

它很少被使用,因为方括号 [] 更短更简洁。而且,这种语法还有一个棘手的特性。

如果使用单个参数(即数字)调用 new Array,那么它会创建一个 指定了长度,却没有任何项 的数组。

让我们看看如何搬起石头砸自己的脚:

  1. let arr = new Array(2); // 会创建一个 [2] 的数组吗?
  2. alert( arr[0] ); // undefined!没有元素。
  3. alert( arr.length ); // length 2

在上面的代码中,new Array(number) 创建的数组的所有元素都是 undefined

为了避免这种乌龙事件,我们通常都是使用方括号的,除非我们清楚地知道自己正在做什么。

多维数组

数组里的项也可以是数组。我们可以将其用于多维数组,例如存储矩阵:

  1. let matrix = [
  2. [1, 2, 3],
  3. [4, 5, 6],
  4. [7, 8, 9]
  5. ];
  6. alert( matrix[1][1] ); // 最中间的那个数

数组的比较

JavaScript 中的数组与其它一些编程语言的不同,不应该使用 == 运算符比较 JavaScript 中的数组。

该运算符不会对数组进行特殊处理,它会像处理任意对象那样处理数组。

让我们回顾一下规则:

  • 仅当两个对象引用的是同一个对象时,它们才相等 ==
  • 如果 == 左右两个参数之中有一个参数是对象,另一个参数是原始类型,那么该对象将会被转换为原始类型,转换规则如 对象 — 原始值转换 一章所述。
  • ……nullundefined 相等 ==,且各自不等于任何其他的值。

严格比较 === 更简单,因为它不会进行类型转换。

所以,如果我们使用 == 来比较数组,除非我们比较的是两个引用同一数组的变量,否则它们永远不相等。

  1. alert( [] == [] ); // false
  2. alert( [0] == [0] ); // false

数组常用方法

添加移除元素

我们已经学了从数组的首端或尾端添加和删除元素的方法:

  • arr.push(...items) —— 从尾端添加元素,
  • arr.pop() —— 从尾端提取元素,
  • arr.shift() —— 从首端提取元素,
  • arr.unshift(...items) —— 从首端添加元素。

splic() 【删除,插入数组元素】

数组是对象,所以我们可以尝试使用 delete

  1. let arr = ["I", "go", "home"];
  2. delete arr[1]; // remove "go"
  3. alert( arr[1] ); // undefined
  4. // now arr = ["I", , "home"];
  5. alert( arr.length ); // 3

元素被删除了,但数组仍然有 3 个元素,我们可以看到 arr.length == 3

arr.splice() 添加,删除和插入元素 ,

  1. arr.splice(start[, deleteCount, elem1, ..., elemN]) // start开始下标,删除的元素个数,插入元素的个数

它从索引 start 开始修改 arr:删除 deleteCount 个元素并在当前位置插入 elem1, ..., elemN。最后返回已被删除元素的数组。

  1. let arr = ["I", "study", "JavaScript"];
  2. arr.splice(1, 1); // 从索引 1 开始删除 1 个元素
  3. alert( arr ); // ["I", "JavaScript"]

在下一个例子中,我们删除了 3 个元素,并用另外两个元素替换它们:

  1. let arr = ["I", "study", "JavaScript", "right", "now"];
  2. // remove 3 first elements and replace them with another
  3. // 删除了 3 个元素,并用另外两个元素替换它们
  4. arr.splice(0, 3, "Let's", "dance");
  5. alert( arr ) // now ["Let's", "dance", "right", "now"]

arr.splice() 的返回值 , 返回了已删除元素的数组

  1. let arr = ["I", "study", "JavaScript", "right", "now"];
  2. // 删除前两个元素
  3. let removed = arr.splice(0, 2);
  4. alert( removed ); // "I", "study" <-- 被从数组中删除了的元素

数组中在任意位置插入元素

我们可以将 deleteCount 设置为 0splice 方法就能够插入元素而不用删除任何元素:

  1. let arr = ["I", "study", "JavaScript"];
  2. // 从索引 2 开始
  3. // 删除 0 个元素
  4. // 然后插入 "complex" 和 "language"
  5. arr.splice(2, 0, "complex", "language");
  6. alert( arr ); // "I", "study", "complex", "language", "JavaScript"

允许负向索引

  1. let arr = [1, 2, 5];
  2. // 从索引 -1(尾端前一位)
  3. // 删除 0 个元素,
  4. // 然后插入 3 和 4
  5. arr.splice(-1, 0, 3, 4);
  6. alert( arr ); // 1,2,3,4,5

arr.slice() 【复制数组元素】

它会返回一个新数组,将所有从索引 startend(不包括 end)的数组项复制到一个新的数组。startend 都可以是负数,在这种情况下,从末尾计算索引。

语法

  1. arr.slice([start], [end])
  1. let arr = ["t", "e", "s", "t"];
  2. alert( arr.slice(1, 3) ); // e,s(复制从位置 1 到位置 3 的元素) [1,3)
  3. alert( arr.slice(-2) ); // s,t(复制从位置 -2 到尾端的元素)

我们也可以不带参数地调用它:arr.slice() 会创建一个 arr 的副本。其通常用于获取副本,以进行不影响原始数组的进一步转换。

  1. // arr.slice() 复制数组元素, 并且生成一个新的数组
  2. let arr_1 = ["t", "e", "s", "t"];
  3. console.log(arr_1.slice(0, 2)); // [0, 2) ,[ 't', 'e' ]
  4. console.log(arr_1.slice()); // [ 't', 'e', 's', 't' ] 生成一个新的数组

arr.concat() 【数组合并】

arr.concat() 创建一个新数组,其中包含来自于其他数组和其他项的值。

语法

  1. arr.concat(arg1, arg2...)

它接受任意数量的参数 —— 数组或值都可以。

结果是一个包含来自于 arr,然后是 arg1arg2 的元素的新数组。

如果参数 argN 是一个数组,那么其中的所有元素都会被复制。否则,将复制参数本身。

  1. let arr = [1, 2];
  2. // create an array from: arr and [3,4]
  3. alert( arr.concat([3, 4]) ); // 1,2,3,4
  4. // create an array from: arr and [3,4] and [5,6]
  5. alert( arr.concat([3, 4], [5, 6]) ); // 1,2,3,4,5,6
  6. // create an array from: arr and [3,4], then add values 5 and 6
  7. alert( arr.concat([3, 4], 5, 6) ); // 1,2,3,4,5,6

arr.forEach() 【数组元素都运行一个函数】

arr.forEach() 方法允许为数组的每个元素都运行一个函数。

语法

  1. arr.forEach(
  2. function (item, index, array) {
  3. // ... do something with item
  4. }
  5. );

例如,下面这个程序显示了数组的每个元素:

  1. // 对每个元素调用 alert
  2. ["Bilbo", "Gandalf", "Nazgul"].forEach(alert);

而这段代码更详细地介绍了它们在目标数组中的位置:

  1. ["Bilbo", "Gandalf", "Nazgul"].forEach( (item, index, array) => {
  2. alert(`${item} is at index ${index} in ${array}`);
  3. });
  4. //Bilbo is at index 0 in Bilbo,Gandalf,Nazgul
  5. //Gandalf is at index 1 in Bilbo,Gandalf,Nazgul
  6. //Nazgul is at index 2 in Bilbo,Gandalf,Nazgul
  • item : 元素
  • index : 元素下标
  • array : 整个数组

该函数的结果(如果它有返回)会被抛弃和忽略。

数组中的搜索

arr.indexOf() 【搜索数组中元素】

arr.indexOf(item, from) 从索引 from 开始搜索 item,如果找到则返回索引,否则返回 -1

  1. // arr.IndexOf(item, from)
  2. // item 要搜索的元素, form 开始的下标
  3. let arr_2 = [1, 2, 3, 4, 5, 5];
  4. console.log(arr_2.indexOf(100, 0)); // -1
  5. console.log(arr_2.indexOf(3, 0)); // 2, 找到元素返回下标

arr.lastIndexOf() 【搜索数组中元素】

arr.lastIndexOf(item, from) —— 和上面IndexOf()相同,只是从右向左搜索。

  1. // arr.lastindexOf()
  2. console.log(arr_2.lastIndexOf(3)); //2
  3. console.log(arr_2.lastIndexOf(100)); // -1

arr.includes() 【判断元素是存在数组】

arr.includes(item, from) —— 从索引 from 开始搜索 item,如果找到则返回 true(译注:如果没找到,则返回 false)。

  1. // arr.includes() 判断数组是否存在
  2. console.log(arr_2.includes(100)); // false
  3. console.log(arr_2.includes(3)); // true

includes 的一个非常小的差别是它能正确处理NaN,而不像 indexOf/lastIndexOf

  1. const arr = [NaN];
  2. alert( arr.indexOf(NaN) ); // -1(应该为 0,但是严格相等 === equality 对 NaN 无效)
  3. alert( arr.includes(NaN) );// true(这个结果是对的)

find() 【查找数组对象】

语法 :

  1. let result = arr.find(function(item, index, array) {
  2. // 如果返回 true,则返回 item 并停止迭代
  3. // 对于假值(falsy)的情况,则返回 undefined
  4. });

依次对数组中的每个元素调用该函数:

  • item 是元素。
  • index 是它的索引。
  • array 是数组本身。

如果它返回 true,则搜索停止,并返回 item。如果没有搜索到,则返回 undefined

  1. // 例如,我们有一个存储用户的数组,每个用户都有 `id` 和 `name` 字段。让我们找到 `id == 1` 的那个用户:
  2. // arr.find(item, index, array) 方法
  3. let users = [
  4. // 对象
  5. { id: 1, name: "John" },
  6. { id: 2, name: "Pete" },
  7. { id: 3, name: "Mary" }
  8. ];
  9. console.log(users.find(item => item.id == 1)); // 返回item { id: 1, name: 'John' }
  10. let user = users.find(item => item.id == 2);
  11. console.log(user.name); // Pete

在现实生活中,对象数组是很常见的,所以 find 方法非常有用。

注意在这个例子中,我们传给了 find 一个单参数函数 item => item.id == 1。这很典型,并且 find 方法的其他参数很少使用。

arr.findIndex() 【查找元素的索引】

arr.findIndex() 方法(与 arr.find 方法)基本上是一样的,但它返回找到元素的索引,而不是元素本身。并且在未找到任何内容时返回 -1

  1. // findindex() 方法
  2. let users = [
  3. // 对象
  4. { id: 1, name: "John" },
  5. { id: 2, name: "Pete" },
  6. { id: 3, name: "Mary" }
  7. ];
  8. let user_test = users.findIndex(item => item.id == 2); // 返回索引值
  9. console.log(user_test); // 返回下标 1

arr.filter() 【查找大量元素】

find 方法搜索的是使函数返回 true 的第一个(单个)元素。如果需要匹配的有很多,我们可以使用 arr.filter。

语法与 find 大致相同,但是 filter 返回的是所有匹配元素组成的数组

  1. let results = arr.filter(function(item, index, array) {
  2. // 如果 true item 被 push 到 results,迭代继续
  3. // 如果什么都没找到,则返回空数组
  4. });
  1. // arr.filter() 方法
  2. let users = [
  3. // 对象
  4. { id: 1, name: "John" },
  5. { id: 2, name: "Pete" },
  6. { id: 3, name: "Mary" }
  7. ];
  8. let find_arr = users.filter(item => item.id == 2);
  9. console.log(find_arr); // [ { id: 2, name: 'Pete' } ]

数组的转换

arr.map() 【数组元素的调用】

arr.map()方法是最有用和经常使用的方法之一。 它对数组的每个元素都调用函数,并返回结果数组

  1. // 语法
  2. let result = arr.map(function(item, index, array) {
  3. // 返回新值而不是当前元素
  4. })
  1. let find_New_arr = [1, 2, 3, 3, 0, "test", "test"];
  2. // arr.map() 方法
  3. let result = find_New_arr.map(item => String(item));
  4. console.log(result);
  5. /**
  6. * [
  7. '1', '2',
  8. '3', '3',
  9. '0', 'test',
  10. 'test'
  11. ]
  12. */

arr.soft() 【大小排序数组】

arr.sort()方法对数组进行 原位(in-place) 排序,更改元素的顺序。(译注:原位是指在此数组内,而非生成一个新数组。)

它还返回排序后的数组,但是返回值通常会被忽略,因为修改了 arr 本身。

  1. let arr = [ 1, 2, 15 ];
  2. // 该方法重新排列 arr 的内容
  3. arr.sort();
  4. alert( arr ); // 1, 15, 2

这些元素默认情况下被按字符串进行排序

从字面上看,所有元素都被转换为字符串,然后进行比较。对于字符串,按照词典顺序进行排序,实际上应该是 "2" > "15"

  1. // 例如,按数字进行排序:
  2. function compareNumeric(a, b) {
  3. if (a > b) return 1;
  4. if (a == b) return 0;
  5. if (a < b) return -1;
  6. }
  7. let arr = [ 1, 2, 15 ];
  8. arr.sort(compareNumeric);
  9. alert(arr); // 1, 2, 15

arr.sort(fn) 方法实现了通用的排序算法。我们不需要关心它的内部工作原理(大多数情况下都是经过 快速排序Timsort 算法优化的)。它将遍历数组,使用提供的函数比较其元素并对其重新排序,我们所需要的就是提供执行比较的函数 fn

比较函数可以返回任何数字

实际上,比较函数只需要返回一个正数表示“大于”,一个负数表示“小于”。

通过这个原理我们可以编写更短的函数:

  1. let arr = [ 1, 2, 15 ];
  2. arr.sort(function(a, b) { return a - b; });
  3. alert(arr); // 1, 2, 15

箭头函数最好

你还记得 箭头函数 吗?这里使用箭头函数会更加简洁:

  1. arr.sort( (a, b) => a - b );

这与上面更长的版本完全相同。

arr.reverse() 【颠倒数组元素顺序】

arr.reverse方法用于颠倒 arr 中元素的顺序。

  1. let arr = [1, 2, 3, 4, 5];
  2. arr.reverse();
  3. alert( arr ); // 5,4,3,2,1

split() 【分割字符】

str.split 方法可以做到。它通过给定的分隔符 delim 将字符串分割成一个数组 。

  1. // split() 以什么字符对 字符串 进行划分 , 返回的是一个数组
  2. let names = 'Bilbo, Gandalf, Nazgul'; // 字符串
  3. let Arr = names.split(',');
  4. console.log(Arr); // [ 'Bilbo', ' Gandalf', ' Nazgul' ]
  5. for (let name of Arr) {
  6. console.log(`A message to ${name}.`); // A message to Bilbo(和其他名字)
  7. }

拆分为字母

调用带有空参数 ssplit(s),会将字符串拆分为字母数组:

  1. let str = "test";
  2. alert( str.split('') ); // t,e,s,t

join()

arr.join(glue)split 相反。它会在它们之间创建一串由 glue 粘合的 arr 项。

  1. let arr = ['Bilbo', 'Gandalf', 'Nazgul'];
  2. let str = arr.join(';'); // 使用分号 ; 将数组粘合成字符串
  3. alert( str ); // Bilbo;Gandalf;Nazgul

Array.isArray() 【判断是否是数组】

Array.ifArray()判断是否是数组

数组是基于对象的,不构成单独的语言类型。

所以 typeof 不能帮助从数组中区分出普通对象:

  1. alert(typeof {}); // object
  2. alert(typeof []); // same

但是数组经常被使用,因此有一种特殊的方法用于判断:Array.isArray(value)。如果 value 是一个数组,则返回 true;否则返回 false

  1. // arr.isArray() 判断是否是数组
  2. console.log(Array.isArray([])); // true
  3. console.log(Array.isArray({})); // false

reduce 【数组元素调用并计算结果】

**reduce/reduceRight(func, initial)** —— 通过对每个元素调用 **func** 计算数组上的单个值,并在调用之间传递中间结果。

常见方法总结

数组方法备忘单:

  • 添加/删除元素:
    • push(...items) —— 向尾端添加元素,
    • pop() —— 从尾端提取一个元素,
    • shift() —— 从首端提取一个元素,
    • unshift(...items) —— 向首端添加元素,
    • splice(pos, deleteCount, ...items) —— 从 pos 开始删除 deleteCount 个元素,并插入 items
    • slice(start, end) —— 创建一个新数组,将从索引 start 到索引 end(但不包括 end)的元素复制进去。
    • concat(...items) —— 返回一个新数组:复制当前数组的所有元素,并向其中添加 items。如果 items 中的任意一项是一个数组,那么就取其元素。
  • 搜索元素:
    • indexOf/lastIndexOf(item, pos) —— 从索引 pos 开始搜索 item,搜索到则返回该项的索引,否则返回 -1
    • includes(value) —— 如果数组有 value,则返回 true,否则返回 false
    • find/filter(func) —— 通过 func 过滤元素,返回使 func 返回 true 的第一个值/所有值。
    • findIndexfind 类似,但返回索引而不是值。
  • 遍历元素:
    • forEach(func) —— 对每个元素都调用 func,不返回任何内容。
  • 转换数组:
    • map(func) —— 根据对每个元素调用 func 的结果创建一个新数组。
    • sort(func) —— 对数组进行原位(in-place)排序,然后返回它。比如按照元素大小进行排序
    • reverse() —— 原位(in-place)反转数组,然后返回它。
    • split/join —— 将字符串转换为数组并返回。
    • **reduce/reduceRight(func, initial)** —— 通过对每个元素调用 **func** 计算数组上的单个值,并在调用之间传递中间结果。
  • 其他:
    • Array.isArray(arr) 检查 arr 是否是一个数组。

请注意,sortreversesplice 方法修改的是数组本身。

这些是最常用的方法,它们覆盖 99% 的用例。但是还有其他几个:

  • arr.some(fn)/arr.every(fn) 检查数组。
  • every() 方法测试一个数组内的所有元素是否都能通过某个指定函数的测试。它返回一个布尔值。

map 类似,对数组的每个元素调用函数 fn。如果任何/所有结果为 true,则返回 true,否则返回 false

这两个方法的行为类似于 ||&& 运算符:如果 fn 返回一个真值,arr.some() 立即返回 true 并停止迭代其余数组项;如果 fn 返回一个假值,arr.every() 立即返回 false 并停止对其余数组项的迭代。

我们可以使用 every 来比较数组:

  1. function arraysEqual(arr1, arr2) {
  2. return arr1.length === arr2.length && arr1.every((value, index) => value === arr2[index]);
  3. }
  4. alert( arraysEqual([1, 2], [1, 2])); // true

数组常见的算法

  1. let arr = ["a", "b"];
  2. arr.push(function() {
  3. console.log(this);
  4. });
  5. console.log(arr[2]()); // [ 'a', 'b', [Function (anonymous)] ]
  1. /**
  2. *写出函数 sumInput(),要求如下:
  3. 使用 prompt 向用户索要值,并存在数组中。
  4. 当用户输入了非数字、空字符串或者点击“取消”按钮的时候,问询结束。
  5. 计算并返回数组所有项之和。
  6. P.S. 0 是有效的数字,不要因为是 0 就停止问询。
  7. 解决方案
  8. 请注意这个解决方案的细微但是很重要的细节。我们没有在 prompt 后立即把 value 转换成数字,因为在执行 value = +value 之后,就没办法区分出空字符串(中断标志)和数字 0(合法输入)了,所以要放到后面再处理。
  9. */
  10. function sumInput() {
  11. let numbers = [];
  12. while (true) {
  13. let value = 124;
  14. // 应该结束了吗?
  15. if (value === "" || value === null || !isFinite(value)) break;
  16. numbers.push(+value);
  17. }
  18. let sum = 0;
  19. for (let number of numbers) {
  20. sum += number;
  21. }
  22. return sum;
  23. }
  24. console.log(sumInput());
  1. /*
  2. *最大子数组
  3. 重要程度: 2
  4. 输入是以数字组成的数组,例如 arr = [1, -2, 3, 4, -9, 6].
  5. 任务是:找出所有项的和最大的 arr 数组的连续子数组。
  6. 写出函数 getMaxSubSum(arr),用其找出并返回最大和。
  7. 例如:
  8. getMaxSubSum([-1, 2, 3, -9]) == 5(高亮项的加和)
  9. getMaxSubSum([2, -1, 2, 3, -9]) == 6
  10. getMaxSubSum([-1, 2, 3, -9, 11]) == 11
  11. getMaxSubSum([-2, -1, 1, 2]) == 3
  12. getMaxSubSum([100, -9, 2, -3, 5]) == 100
  13. getMaxSubSum([1, 2, 3]) == 6(所有项的和)
  14. 如果所有项都是负数,那就一个项也不取(子数组是空的),所以返回的是 0:
  15. */
  16. function getMaxSubSum(arr) {
  17. let maxSum = 0; // 如果没有取到任何元素,就返回 0
  18. for (let i = 0; i < arr.length; i++) {
  19. let sumFixedStart = 0;
  20. for (let j = i; j < arr.length; j++) {
  21. sumFixedStart += arr[j];
  22. maxSum = Math.max(maxSum, sumFixedStart);
  23. }
  24. }
  25. return maxSum;
  26. }
  27. alert( getMaxSubSum([-1, 2, 3, -9]) ); // 5
  28. alert( getMaxSubSum([-1, 2, 3, -9, 11]) ); // 11
  29. alert( getMaxSubSum([-2, -1, 1, 2]) ); // 3
  30. alert( getMaxSubSum([1, 2, 3]) ); // 6
  31. alert( getMaxSubSum([100, -9, 2, -3, 5]) ); // 100
  32. /*
  33. 快的解决方案
  34. 让我们遍历数组,将当前局部元素的和保存在变量 s 中。如果 s 在某一点变成负数了,就重新分配 s=0。所有 s 中的最大值就是答案。
  35. 如果文字描述不太好理解,就直接看下面的代码吧,真的很短:
  36. */
  37. function getMaxSubSum(arr) {
  38. let maxSum = 0;
  39. let partialSum = 0;
  40. for (let item of arr) { // arr 中的每个 item
  41. partialSum += item; // 将其加到 partialSum
  42. maxSum = Math.max(maxSum, partialSum); // 记住最大值
  43. if (partialSum < 0) partialSum = 0; // 如果是负数就置为 0
  44. }
  45. return maxSum;
  46. }
  47. alert( getMaxSubSum([-1, 2, 3, -9]) ); // 5
  48. alert( getMaxSubSum([-1, 2, 3, -9, 11]) ); // 11
  49. alert( getMaxSubSum([-2, -1, 1, 2]) ); // 3
  50. alert( getMaxSubSum([100, -9, 2, -3, 5]) ); // 100
  51. alert( getMaxSubSum([1, 2, 3]) ); // 6
  52. alert( getMaxSubSum([-1, -2, -3]) ); // 0
  53. //该算法只需要遍历 1 轮数组,所以时间复杂度是 O(n)。

数组元素重命名

编写函数 camelize(str) 将诸如 “my-short-string” 之类的由短划线分隔的单词变成骆驼式的 “myShortString”。

即:删除所有短横线,并将短横线后的每一个单词的首字母变为大写。

提示:使用 split 将字符串拆分成数组,对其进行转换之后再 join 回来。

  1. // camelize("background-color") == 'backgroundColor';
  2. // camelize("list-style-image") == 'listStyleImage';
  3. // camelize("-webkit-transition") == 'WebkitTransition';
  4. function camelize(str) {
  5. return str.split("-").map((item,index) => index == 0 ? item : item[0].toUpperCase() + item.slice(1)).join('');
  6. }
  7. console.log(camelize("background-color"));
  8. // .map( (item,index,array) => { console.log(item,index,array)} );
  9. // .map( ) map方法
  10. // .map( (item数组元素,index数组下标,array数组本身) => { 类似于函数使用,在这里调用数组元素参数,下标等 }); .map的返回值也是一个数组
  11. function camelize(str) {
  12. return str
  13. .split('-')
  14. .map((item, index) => {
  15. if (index == 0) return item;
  16. else return item[0].toUpperCase() + item.slice(1); // slice(1) 复制数组,1表示从1开始
  17. })
  18. .join(''); // 合并
  19. }
  20. console.log(camelize("background-color"));

数组范围

  1. /**
  2. 写一个函数 filterRange(arr, a, b),该函数获取一个数组 arr,在其中查找数值大于或等于 a,且小于或等于 b 的元素,并将结果以数组的形式返回。
  3. 该函数不应该修改原数组。它应该返回新的数组。
  4. */
  5. let arr_f = [5, 3, 8, 1];
  6. function filterRange(arr, a, b) {
  7. let result = [];
  8. for (let res of arr) {
  9. if (res >= a && res <= b) result.push(res);
  10. }
  11. return result;
  12. }
  13. let filtered = filterRange(arr_f, 1, 4);
  14. console.log(filtered);
  15. // 参考方法
  16. function filterRange(arr, a, b) {
  17. return arr.filter(item => (a <= item && item <= b));
  18. }
  19. let filtered = filterRange(arr_f, 1, 4);
  20. console.log(filtered);

数组范围

  1. /**
  2. 写一个函数 filterRangeInPlace(arr, a, b),该函数获取一个数组 arr,并删除其中介于 a 和 b 区间以外的所有值。检查:a ≤ arr[i] ≤ b。
  3. 该函数应该只修改数组。它不应该返回任何东西。
  4. */
  5. function filterRangeInPlace(arr, a, b) {
  6. for (let i = 0; i < arr.length; i++) {
  7. let val = arr[i];
  8. // 如果超出范围,则删除
  9. if (val < a || val > b) {
  10. arr.splice(i, 1);
  11. i--;
  12. }
  13. }
  14. }
  15. let arr = [5, 3, 8, 1];
  16. filterRangeInPlace(arr, 1, 4); // 删除 1 到 4 范围之外的值
  17. alert( arr ); // [3, 1]

降序排列

  1. /** 降序排列
  2. 重要程度: 4
  3. let arr = [5, 2, 1, -10, 8];
  4. // ……你的代码以降序对其进行排序
  5. alert( arr ); // 8, 5, 2, 1, -10
  6. */
  7. let arr = [5, 2, 1, -10, 8];
  8. arr.sort((a, b) => b - a);
  9. alert( arr );

不创建数组,进行数组排序

我们有一个字符串数组 arr。我们希望有一个排序过的副本,但保持 arr 不变。

创建一个函数 copySorted(arr) 返回这样一个副本。

  1. let arr = ["HTML", "JavaScript", "CSS"];
  2. let sorted = copySorted(arr);
  3. alert( sorted ); // CSS, HTML, JavaScript
  4. alert( arr ); // HTML, JavaScript, CSS (no changes)

解决

  1. //我们可以使用 slice() 来创建一个副本并对其进行排序:
  2. function copySorted(arr) {
  3. return arr.slice().sort();
  4. // slice() 复制全部元素, sort() 对数组进行排序
  5. }
  6. let arr = ["HTML", "JavaScript", "CSS"];
  7. let sorted = copySorted(arr);
  8. alert( sorted );
  9. alert( arr );

降序 排列

  1. // 降序 排列
  2. let arr = [5, 2, 1, -10, 8];
  3. arr.sort((a, b) => b - a); // sort() 降序排列
  4. alert( arr );

复制和排序数组

  1. // 复制和排序数组
  2. // 我们有一个字符串数组 arr。我们希望有一个排序过的副本,但保持 arr 不变。 , 创建一个函数 copySorted(arr) 返回这样一个副本。
  3. function copySorted(arr) {
  4. // .slice()创建一个副本并对其进行排序 sort()
  5. return arr.slice().sort();
  6. }
  7. let arr = ["HTML", "JavaScript", "CSS"];
  8. let sorted = copySorted(arr);
  9. alert( sorted );
  10. alert( arr );

数组去重

  1. // 数组去重
  2. let arr = [5, 2, 1, -10, 8, 2, 2, 3, 3, 4, 4];
  3. // 数组去重
  4. let Rest = (arr) => {
  5. let Arr = [];
  6. for (let i = 0; i < arr.length; i++) {
  7. // 创建一个空的数组 ,在数组使用 indexOf()查找 arr所有的元素,如果没有就添加到Arr , 如果在Arr种有了(重复),这pass
  8. if (Arr.indexOf(arr[i]) == -1) { // indexOf 用来返回指定数据在数组种出现的位置 ,如果没有找到返回-1
  9. Arr.push(arr[i]);
  10. }
  11. }
  12. return Arr;
  13. }
  14. console.log(Rest(arr));

映射到 names

  1. // 映射到 names
  2. // 你有一个 user 对象数组,每个对象都有 user.name。编写将其转换为 names 数组的代码。
  3. // 例如:
  4. let john = { name: "John", age: 25 };
  5. let pete = { name: "Pete", age: 30 };
  6. let mary = { name: "Mary", age: 28 };
  7. let users = [ john, pete, mary ];
  8. let names = /* ... your code */
  9. alert( names ); // John, Pete, Mary

解决

  1. // 映射到 name
  2. let john = { name: "John", age: 25 };
  3. let pete = { name: "Pete", age: 30 };
  4. let mary = { name: "Mary", age: 28 };
  5. let users = [john, pete, mary]; // 数组中存放对象
  6. let names = users.map(item => item.name); // 定义一个map方法
  7. console.log(names); // John, Pete, Mary

映射到对象

  1. let john = { name: "John", surname: "Smith", id: 1 };
  2. let pete = { name: "Pete", surname: "Hunt", id: 2 };
  3. let mary = { name: "Mary", surname: "Key", id: 3 };
  4. let users = [ john, pete, mary ];
  5. let usersMapped = users.map(user => ({
  6. fullName: `${user.name} ${user.surname}`,
  7. id: user.id
  8. }));
  9. /*
  10. usersMapped = [
  11. { fullName: "John Smith", id: 1 },
  12. { fullName: "Pete Hunt", id: 2 },
  13. { fullName: "Mary Key", id: 3 }
  14. ]
  15. */
  16. alert( usersMapped[0].id ); // 1
  17. alert( usersMapped[0].fullName ); // John Smith

请注意,在箭头函数中,我们需要使用额外的括号。

我们不能这样写:

  1. let usersMapped = users.map(user => {
  2. fullName: `${user.name} ${user.surname}`,
  3. id: user.id
  4. });

我们记得,有两种箭头函数的写法:直接返回值 value => expr 和带主体的 value => {...}

JavaScript 在这里会把 { 视为函数体的开始,而不是对象的开始。解决方法是将它们包装在普通括号 () 中:

  1. let usersMapped = users.map(user => ({
  2. fullName: `${user.name} ${user.surname}`,
  3. id: user.id
  4. }));

这样就可以了。

浏览器的交互

我们看几个与用户交互的函数:alertpromptconfirm

alert

这个我们前面已经看到过了。它会显示一条信息,并等待用户按下 “OK”。

  1. alert("Hello");

JavaScript基础 - 图6

弹出的这个带有信息的小窗口被称为 模态窗。“modal” 意味着用户不能与页面的其他部分(例如点击其他按钮等)进行交互,直到他们处理完窗口。在上面示例这种情况下 —— 直到用户点击“确定”按钮。

prompt

prompt 函数接收两个参数:

  1. result = prompt(title, [default]);
  1. result = prompt("test", "hello");

JavaScript基础 - 图7

浏览器会显示一个带有文本消息的模态窗口,还有 input 框和确定/取消按钮。

title: 显示给用户的文本

default : 可选的第二个参数,指定 input 框的初始值。

访问者可以在提示输入栏中输入一些内容,然后按“确定”键。然后我们在 result 中获取该文本。或者他们可以按取消键或按 Esc 键取消输入,然后我们得到 null 作为 result

prompt 将返回用户在 input 框内输入的文本,如果用户取消了输入,则返回 null

  1. let age = prompt('How old are you ?',100);
  2. alert(`You are ${age} years old !`);

IE 浏览器会提供默认值

第二个参数是可选的。但是如果我们不提供的话,Internet Explorer 会把 "undefined" 插入到 prompt。

我们可以在 Internet Explorer 中运行下面这行代码来看看效果:

  1. let test = prompt("Test");

所以,为了 prompt 在 IE 中有好的效果,我们建议始终提供第二个参数:

  1. let test = prompt("Test", ''); // <-- 用于 IE 浏览器

confirm

语法:

  1. result = confirm(question);

confirm 函数显示一个带有 question 以及确定和取消两个按钮的模态窗口。

点击确定返回 true,点击取消返回 false

  1. let isBoss = confirm("Are you the Boss ?");
  2. alert(isBoss);

JavaScript基础 - 图8

JavaScript 类型转换

大多数情况下,运算符和函数会自动将赋予它们的值转换为正确的类型。

比如,alert 会自动将任何值都转换为字符串以进行显示。算术运算符会将值转换为数字。

在某些情况下,我们需要将值显式地转换为我们期望的类型。

字符串转换

String()

  1. let value = true;
  2. alert(typeof value); // boolean
  3. // 类型转换
  4. value = String(value); // boolean --> str
  5. console.log(typeof value);

数字型转换

在算术函数和表达式中,会自动进行 number 类型转换。

比如,当把除法 / 用于非 number 类型:

  1. alert( "6" / "2" ); // 3, string 类型的值被自动转换成 number 类型后进行计算

我们也可以使用 Number(value) 显式地将这个 value 转换为 number 类型。

  1. let str = "123";
  2. console.log(typeof str);
  3. let num = Number(str); //str --> number
  4. console.log(num);

当我们从 string 类型源(如文本表单)中读取一个值,但期望输入一个数字时,通常需要进行显式转换。

如果该字符串不是一个有效的数字,转换的结果会是 NaN。例如:

  1. let age = Number("an arbitrary string instead of a number");
  2. alert(age); // NaN,转换失败

number 类型转换规则:

变成……
undefined NaN
null 0
true 和 false 1
and 0
string 去掉首尾空格后的纯数字字符串中含有的数字。如果剩余字符串为空,则转换结果为 0
。否则,将会从剩余字符串中“读取”数字。当类型转换出现 error 时返回 NaN

布尔型转换

布尔(boolean)类型转换是最简单的一个。

它发生在逻辑运算中(稍后我们将进行条件判断和其他类似的东西),但是也可以通过调用 Boolean(value) 显式地进行转换。

转换规则如下:

  • 直观上为“空”的值(如 0、空字符串、nullundefinedNaN)将变为 false
  • 其他值变成 true

比如:

  1. alert( Boolean(1) ); // true
  2. alert( Boolean(0) ); // false
  3. alert( Boolean("hello") ); // true
  4. alert( Boolean("") ); // false

请注意:包含 0 的字符串 **"0"****true**

一些编程语言(比如 PHP)视 "0"false。但在 JavaScript 中,非空的字符串总是 true

  1. alert( Boolean("0") ); // true
  2. alert( Boolean(" ") ); // 空白,也是 true(任何非空字符串都是 true)

上述的大多数规则都容易理解和记忆。人们通常会犯错误的值得注意的例子有以下几个:

  • undefined 进行数字型转换时,输出结果为 NaN,而非 0
  • "0" 和只有空格的字符串(比如:" ")进行布尔型转换时,输出结果为 true

JavaScript 运算符

支持以下数学运算:

  • 加法 +,
  • 减法 -,
  • 乘法 *,
  • 除法 /,
  • 取余 %,
  • 求幂 **

取余

取余运算符是 %,尽管它看起来很像百分数,但实际并无关联。

a % b 的结果是 a 整除 b余数)。

例如:

  1. alert( 5 % 2 ); // 1,5 除以 2 的余数
  2. alert( 8 % 3 ); // 2,8 除以 3 的余数

求幂

求幂运算 a ** ba 乘以自身 b 次。

例如:

  1. alert( 2 ** 2 ); // 4 (2 * 2,自乘 2 次)
  2. alert( 2 ** 3 ); // 8 (2 * 2 * 2,自乘 3 次)
  3. alert( 2 ** 4 ); // 16 (2 * 2 * 2 * 2,自乘 4 次)

在数学上,求幂的定义也适用于非整数。例如,平方根是以 1/2 为单位的求幂:

  1. alert( 4 ** (1/2) ); // 2(1/2 次方与平方根相同)
  2. alert( 8 ** (1/3) ); // 2(1/3 次方与立方根相同)

用二元运算符 + 连接字符串

我们来看一些学校算术未涉及的 JavaScript 运算符的特性。

通常,加号 + 用于求和。

但是如果加号 + 被应用于字符串,它将合并(连接)各个字符串:

  1. let s = "my" + "string";
  2. alert(s); // mystring
  3. alert(typeof s); // str

注意:只要任意一个运算元是字符串,那么另一个运算元也将被转化为字符串。

举个例子:

  1. alert( '1' + 2 ); // "12"
  2. alert( 2 + '1' ); // "21"

二元 + 是唯一一个以这种方式支持字符串的运算符。其他算术运算符只对数字起作用,并且总是将其运算元转换为数字。

下面是减法和除法运算的示例:

  1. alert( 6 - '2' ); // 4, 将 '2' 转换为数字
  2. alert( '6' / '2' ); // 3,将两个运算元都转换为数字

数字转换, 一元运算符 +

加号 + 有两种形式。一种是上面我们刚刚讨论的二元运算符,还有一种是一元运算符。

一元运算符加号,或者说,加号 + 应用于单个值,对数字没有任何作用。但是如果运算元不是数字,加号 + 则会将其转化为数字。

  1. // + 对数字无效
  2. let x = 1;
  3. alert( +x ); // 1
  4. let y = -2;
  5. alert( +y ); // -2
  6. // 转化非数字
  7. alert( +true ); // 1
  8. alert( +"" ); // 0
  9. let apples = "2";
  10. let oranges = "3";
  11. // 在二元运算符加号起作用之前,所有的值都被转化为了数字
  12. alert( +apples + +oranges ); // 5
  13. // 更长的写法
  14. // alert( Number(apples) + Number(oranges) ); // 5

它的效果和 Number(...) 相同,但是更加简短。

  1. // NaN
  2. console.log("4px" - 2); // NaN

运算符优先级

如果一个表达式拥有超过一个运算符,执行的顺序则由 优先级 决定。换句话说,所有的运算符中都隐含着优先级顺序。

从小学开始,我们就知道在表达式 1 + 2 * 2 中,乘法先于加法计算。这就是一个优先级问题。乘法比加法拥有 更高的优先级

圆括号拥有最高优先级,所以如果我们对现有的运算顺序不满意,我们可以使用圆括号来修改运算顺序,就像这样:(1 + 2) * 2

在 JavaScript 中有众多运算符。每个运算符都有对应的优先级数字。数字越大,越先执行。如果优先级相同,则按照由左至右的顺序执行。

原地修改

  1. let n = 2;
  2. n *= 3 + 5;
  3. alert( n ); // 16 (右边部分先被计算,等同于 n *= 8)

自增自减

自增/自减只能应用于变量。试一下,将其应用于数值(比如 5++)则会报错。

运算符 ++-- 可以置于变量前,也可以置于变量后。

  • 当运算符置于变量后,被称为“后置形式”:counter++
  • 当运算符置于变量前,被称为“前置形式”:++counter

如果我们想要对变量进行自增操作,并且 需要立刻使用自增后的值,那么我们需要使用前置形式:

  1. let counter = 0;
  2. alert( ++counter ); // 1

如果我们想要将一个数加一,但是我们想使用其自增之前的值,那么我们需要使用后置形式:

  1. let counter = 0;
  2. alert( counter++ ); // 0

自增/自减和其它运算符的对比

++/-- 运算符同样可以在表达式内部使用。它们的优先级比绝大部分的算数运算符要高。

举个例子:

  1. let counter = 1;
  2. alert( 2 * ++counter ); // 4

与下方例子对比:

  1. let counter = 1;
  2. alert( 2 * counter++ ); // 2,因为 counter++ 返回的是“旧值”

值的比较

Boolean 类型

  1. alert( 2 > 1 ); // true(正确)
  2. alert( 2 == 1 ); // false(错误)
  3. alert( 2 != 1 ); // true(正确)

字符串的比较

  1. alert( 'Z' > 'A' ); // true
  2. alert( 'Glow' > 'Glee' ); // true
  3. alert( 'Bee' > 'Be' ); // true

不同类型间的比较

当对不同类型的值进行比较时,JavaScript 会首先将其转化为数字(number)再判定大小。

  1. alert( '2' > 1 ); // true,字符串 '2' 会被转化为数字 2
  2. alert( '01' == 1 ); // true,字符串 '01' 会被转化为数字 1

相等运算符

普通的相等性检查 == 存在一个问题,它不能区分出 0false

  1. alert( 0 == false ); // true

也同样无法区分空字符串和 false

  1. alert( '' == false ); // true

这是因为在比较不同类型的值时,处于相等判断符号 == 两侧的值会先被转化为数字。空字符串和 false 也是如此,转化后它们都为数字 0。

如果我们需要区分 0false,该怎么办?

严格相等运算符 **===** 在进行比较时不会做任何的类型转换。

换句话说,如果 ab 属于不同的数据类型,那么 a === b 不会做任何的类型转换而立刻返回 false

让我们试试:

  1. alert( 0 === false ); // false,因为被比较值的数据类型不同

同样的,与“不相等”符号 != 类似,“严格不相等”表示为 !==

严格相等的运算符虽然写起来稍微长一些,但是它能够很清楚地显示代码意图,降低你犯错的可能性。

null 和 undefined 的比较

当使用 nullundefined 与其他值进行比较时,其返回结果常常出乎你的意料。

  • 当使用严格相等 === 比较二者时
    它们不相等,因为它们属于不同的类型。alert( null === undefined ); // false
  • 当使用非严格相等 == 比较二者时
    JavaScript 存在一个特殊的规则,会判定它们相等。它们俩就像“一对恋人”,仅仅等于对方而不等于其他任何的值(只在非严格相等下成立)。alert( null == undefined ); // true

null VS 0

通过比较 null 和 0 可得:

  1. alert( null > 0 ); // (1) false , 这是因为相等性检查 `==` 和普通比较符 `> < >= <=` 的代码逻辑是相互独立的。进行值的比较时,`null` 会被转化为数字,因此它被转化为了 `0`。
  2. alert( null == 0 ); // (2) false , `undefined` 和 `null` 在相等性检查 `==` 中不会进行任何的类型转换,它们有自己独立的比较规则,所以除了它们之间互等外,不会等于任何其他的值。
  3. alert( null >= 0 ); // (3) true ,使用 >= 等逻辑运算符时, null 会转变为0 ,

是的,上面的结果完全打破了你对数学的认识。在最后一行代码显示“null 大于等于 0”的情况下,前两行代码中一定会有一个是正确的,然而事实表明它们的结果都是 false。

为什么会出现这种反常结果,这是因为相等性检查 == 和普通比较符 > < >= <= 的代码逻辑是相互独立的。进行值的比较时,null 会被转化为数字,因此它被转化为了 0。这就是为什么(3)中 null >= 0 返回值是 true,(1)中 null > 0 返回值是 false。

另一方面,undefinednull 在相等性检查 == 中不会进行任何的类型转换,它们有自己独立的比较规则,所以除了它们之间互等外,不会等于任何其他的值。这就解释了为什么(2)中 null == 0 会返回 false。

特立的undefined

undefined 不应该被与其他值进行比较:

  1. alert( undefined > 0 ); // false (1) , undefined 转为 NaN
  2. alert( undefined < 0 ); // false (2)
  3. alert( undefined == 0 ); // false (3)

为何它看起来如此厌恶 0?返回值都是 false!

原因如下:

  • (1)(2) 都返回 false 是因为 undefined 在比较中被转换为了 NaN,而 NaN 是一个特殊的数值型值,它与任何值进行比较都会返回 false
  • (3) 返回 false 是因为这是一个相等性检查,而 undefined 只与 null 相等,不会与其他值相等。

值的比较 总结

  1. 5 > 4 true
  2. "apple" > "pineapple" false
  3. "2" > "12" true // 转为数字,然后 先比较 首字母
  4. undefined == null true
  5. undefined === null false
  6. null == "\n0\n" false
  7. null === +"\n0\n" false

逻辑运算符

逻辑运算符有:!、&&和||三种。返回的值一般是逻辑值true或false,也可能返回其它值。

!:逻辑非(取反)(单目/一元运算)
!true -> false !false -> true

  1. alert( !true ); // false
  2. alert( !0 ); // true

&&:逻辑与(双目/二元运算)
只要有一个操作数为false,结果为false。
注意:
如果两个中任意一个操作数非逻辑值,第一个操作数的结果为true时,返回第二个操作数的值;
第一个操作数的结果为false时,返回第一个操作数的值。

  1. alert( true && true ); // true
  2. alert( false && true ); // false
  3. alert( true && false ); // false
  4. alert( false && false ); // false

||:逻辑或(双目/二元运算)

  1. 只要有一个操作数为true,结果为true。<br /> 注意:<br /> 如果两个中任意一个操作数非逻辑值,第一个操作数的结果为true时,返回第一个操作数的值;<br /> 第一个操作数的结果为false时,返回第二个操作数的值。
  1. alert( true || true ); // true
  2. alert( false || true ); // true
  3. alert( true || false ); // true
  4. alert( false || false ); // false

短路运算:
&&运算时,如果第一个操作数为false,不需要计算第二个操作数,结果返回false。
||运算时,如果第一个操作数为true,不需要计算第二个操作数,结果返回true。

JavaScript基础 - 图9

  1. let userName = prompt("Who's there ?", '');
  2. if (userName == 'Admin') {
  3. let password = prompt("PassWord :", '');
  4. if (password === 'TheMaster') {
  5. alert("Welcome!");
  6. } else if (password === '' || password === null) {
  7. alert("Canceled");
  8. } else {
  9. alert("Wrong password");
  10. }
  11. } else if (userName === '' || userName === null) {
  12. alert("Canceled");
  13. } else {
  14. alert("I don't know you");
  15. }

空值合并运算符 ‘??’

空值合并运算符(nullish coalescing operator)的写法为两个问号 ??

a ?? b 的结果是:

  • 如果 a 是已定义的,则结果为 a
  • 如果 a 不是已定义的,则结果为 b

换句话说,如果第一个参数不是 null/undefined,则 ?? 返回第一个参数。否则,返回第二个参数。

空值合并运算符并不是什么全新的东西。它只是一种获得两者中的第一个“已定义的”值的不错的语法。

我们可以使用我们已知的运算符重写 result = a ?? b,像这样:

  1. result = (a !== null && a !== undefined ) ? a : b

通常 ?? 的使用场景是,为可能是未定义的变量提供一个默认值。

例如,在这里,如果 user 是未定义的,我们则显示 Anonymous

  1. let user;
  2. alert(user ?? "Anonymous"); // Anonymous

当然,如果 user 的值为除 null/undefined 外的任意值,那么我们看到的将是它:

  1. let user = "John";
  2. alert(user ?? "Anonymous"); // John

我们还可以使用 ?? 序列从一系列的值中选择出第一个非 null/undefined 的值。

位运算符

位运算符 描述
&
|
~
^ 异或
<< 左移
>> 右移
  1. console.log('5 & 1:', (5 & 1));
  2. console.log('5 | 1:', (5 | 1));
  3. console.log('~ 5:', (~5));
  4. console.log('5 ^ 1:', (5 ^ 1));
  5. console.log('5 << 1:', (5 << 1));
  6. console.log('5 >> 1:', (5 >> 1));

三元运算符(条件运算符)

语法:
表达式1 ? 表达式2 :表达式3
如果表达式1成立,返回表达式2的结果;如果不成立,返回表达式3的结果。

Tips:
三目运算相当于if语句中的双分支结构。
如果表达式2或表达式3较为复杂,建议用if语句或switch语句实现。

  1. var b = 5 > 4 ? '对': '错';
  2. console.log(b);
  1. let age = prompt('age?',18);
  2. let massage = (age < 3) ? 'Hi, baby! ' :
  3. (age < 18 ) ? 'Hello !' :
  4. (age < 100) ? 'Greentings !' :
  5. 'What an unusual age !' ;
  6. alert(massage);

JavaScript 流程控制

JS是一门既面向过程,也是面向对象的解释型语言。
面向过程:按照代码书写的顺序依次执行(OOP)。

JS也是一门结构性语言。
JS的结构分为顺序结构、分支(条件/选择)结构和循环结构三种。
顺序结构:按照代码的书写顺序依次执行,一般包含初始化、赋值、输入/输出等语句。
条件结构:用if或switch语句实现,其中的代码是有条件选择执行的。
循环结构:某部分代码在指定的条件范围内反复执行,用for/for…in/forEach/while/do…while语句实现。

if … else

语法

  1. 1)条件结构
  2. a.单分支
  3. 语法:
  4. if(条件)语句;
  5. 或:
  6. if(条件){
  7. 语句组;
  8. }
  9. 如果条件成立,将执行语句或语句组;条件不成立,执行if的下一条语句。
  10. b.双分支
  11. 语法:
  12. if(条件)语句1;else 语句2;
  13. 或:
  14. if(条件){
  15. 语句组1;
  16. }else{
  17. 语句组2;
  18. }
  19. 如果条件成立,将执行语句1或语句组1;条件不成立,将执行语句2或语句组2
  20. 注意:else表示“否则”的意思,其后不能写条件。
  21. c.多分支(三分支及以上的)
  22. 多分支实际上是单分支和双分支的嵌套。
  23. 语法:
  24. if(条件1){
  25. if(条件2){
  26. if(条件3){
  27. 语句或语句组;
  28. }
  29. }
  30. }
  31. 或:
  32. if(条件1){
  33. 语句1或语句组1;
  34. }else{
  35. if(条件2){
  36. 语句2或语句组2;
  37. }else{
  38. 语句3或语句组3;
  39. }
  40. }
  41. 或:
  42. if(条件1){
  43. if(条件2){
  44. 语句1或语句组1;
  45. }
  46. }else{
  47. if(条件3){
  48. 语句1或语句组2;
  49. }else{
  50. 语句1或语句组3;
  51. }
  52. }
  53. 或(简洁写法,推荐):
  54. if(条件1){
  55. 语句1或语句组1;
  56. }else if(条件2){
  57. 语句2或语句组2;
  58. }else if(条件3){
  59. 语句3或语句组3;
  60. }
  61. ....
  62. else{
  63. 语句n或语句组n;
  64. }
  65. 如果条件1成立,将执行语句1或语句组1,后面的代码将不会被执行;
  66. 如果条件1不成立,将判断条件2,条件2成立,执行语句2或语句组2……
  67. 如果前面的条件都不满足时,将执行else后面的代码。

switch

  1. 语法:
  2. switch(表达式){
  3. case 表达式1: 语句1或语句组1;[break;]
  4. case 表达式2: 语句2或语句组2;[break;]
  5. case 表达式3: 语句3或语句组3;[break;]
  6. ...
  7. case 表达式n: 语句n或语句组n;[break;]
  8. default:语句n+1或语句组n+1; // 都不满足,将自动执行default后的语句。
  9. }

说明:执行表达式,如果表达式的结果为case后面的某个对应的值,将执行后面所对应的语句或语句组,如果语句后有break,将终止该情况语句,如果没有break,将不再判断条件,继续执行后面的语句,直到遇到break为止;如果条件都不满足,将自动执行default后的语句。

switch与if的区别:
switch一般用于能获取结果的简单条件的判断,而if一般用于较为复杂的条件判断;
if能实现的条件判断,switch不一定能实现,switch能实现的条件判断,if也一定能;
如果switch和if都能用的情况下,switch一般较简洁些

  1. // 将 switch 改为 if...else
  2. switch (browser) {
  3. case 'Edge':
  4. alert( "You've got the Edge!" );
  5. break;
  6. case 'Chrome':
  7. case 'Firefox':
  8. case 'Safari':
  9. case 'Opera':
  10. alert( 'Okay we support these browsers too' );
  11. break;
  12. default:
  13. alert( 'We hope that this page looks ok!' );
  14. }
  15. // if...else
  16. let browser = prompt("Please Enter browser : ",'');
  17. if (browser == 'Edge') {
  18. alert( "You've got the Edge!" );
  19. }else if (browser == 'Chrome' || browser == 'Firefox' || browser == 'Safari' || browser == 'Opera' ) {
  20. alert( 'Okay we support these browsers too' );
  21. }else {
  22. alert( 'We hope that this page looks ok!' );
  23. }

While 循环

语法:

  1. while (condition) { // 当 condition 为真时,执行循环体的 code。
  2. // 代码
  3. // 所谓的“循环体”
  4. }
  1. while ( let i = 0 < 3) {
  2. console.log(i);
  3. i++;
  4. }

如果上述示例中没有 i++,那么循环(理论上)会永远重复执行下去。实际上,浏览器提供了阻止这种循环的方法,我们可以通过终止进程,来停掉服务器端的 JavaScript。

do…while 循环

语法 :

  1. do {
  2. // 循环体
  3. }while (condition);
  4. // 循环首先执行循环体,然后检查条件,当条件为真时,重复执行循环体。
  1. jjklet i = 0 ;
  2. do {
  3. console.log(i);
  4. i++;
  5. }while(i > 3);
  6. //这种形式的语法很少使用,除非你希望不管条件是否为真,循环体 至少执行一次。通常我们更倾向于使用另一个形式:while(…) {…}。

for 循环

for 循环更加复杂,但它是最常使用的循环形式。

语法 :

  1. for (begin; condition; step) { // beging: 开始; condition:判断表达式; step:步长
  2. // ……循环体……
  3. }
  1. for ( let i = 0 ; i < 3 ; i++) {
  2. console.log(i);
  3. }

省略语句段

for 循环的任何语句段都可以被省略。

例如,如果我们在循环开始时不需要做任何事,我们就可以省略 begin 语句段

  1. let i = 0;
  2. for (; i<3; i++) { // 不再需要 `begin` 语句段
  3. console.log(i);
  4. }
  5. // 我们也可以移除 step 语句段:
  6. for (; i < 3;) {
  7. console.log(i++);
  8. }
  9. //该循环与 while (i < 3) 等价。
  10. // 无限循环
  11. for (;;) { // 请注意 for 的两个 ; 必须存在,否则会出现语法错误。
  12. // 无限循环
  13. }

跳出循环

但我们随时都可以使用 break 指令强制退出。

  1. let sum = 0;
  2. while (true) {
  3. let value = +prompt("Enter a number",'');
  4. if (!value) break; // 如果 输入的是 null || undefined 则break;
  5. sum += value;
  6. }
  7. console.log(sum);

如果用户输入空行或取消输入,在 (*) 行的 break 指令会被激活。它立刻终止循环,将控制权传递给循环后的第一行,即,alert

根据需要,”无限循环 + break“ 的组合非常适用于不必在循环开始/结束时检查条件,但需要在中间甚至是主体的多个位置进行条件检查的情况。

继续下一次迭代

continue 指令是 break 的“轻量版”。它不会停掉整个循环。而是停止当前这一次迭代,并强制启动新一轮循环(如果条件允许的话)。

如果我们完成了当前的迭代,并且希望继续执行下一次迭代,我们就可以使用它。

  1. for ( let i = 0; i < 10 ; i++) {
  2. // 如果为真,跳出循环体的剩余部分
  3. if ( i % 2 == 0) continue;
  4. console.log(i);
  5. }

对于偶数的 i 值,continue 指令会停止本次循环的继续执行,将控制权传递给下一次 for 循环的迭代(使用下一个数字)。因此 alert 仅被奇数值调用。

**continue** 指令利于减少嵌套

显示奇数的循环可以像下面这样:

  1. for (let i = 0; i < 10; i++) {
  2. if (i % 2) {
  3. alert( i );
  4. }
  5. }

从技术角度看,它与上一个示例完全相同。当然,我们可以将代码包装在 if 块而不使用 continue

但在副作用方面,它多创建了一层嵌套(大括号内的 alert 调用)。如果 if 中代码有多行,则可能会降低代码整体的可读性。

禁止 **break/continue** 在 ‘?’ 的右边

请注意非表达式的语法结构不能与三元运算符 ? 一起使用。特别是 break/continue 这样的指令是不允许这样使用的。

例如,我们使用如下代码:

  1. if (i > 5) {
  2. alert(i);
  3. } else {
  4. continue;
  5. }

……用问号重写:

  1. (i > 5) ? alert(i) : continue; // continue 不允许在这个位置

……代码会停止运行,并显示有语法错误。

这是不(建议)使用问号 ? 运算符替代 if 语句的另一个原因。

break / continue 标签

有时候我们需要从一次从多层嵌套的循环中跳出来。

例如,下述代码中我们的循环使用了 ij,从 (0,0)(3,3) 提示坐标 (i, j)

  1. for (let i = 0; i < 3; i++) {
  2. for (let j = 0; j < 3; j++) {
  3. let input = prompt(`Value at coords (${i},${j})`, '');
  4. // 如果我想从这里退出并直接执行 alert('Done!')
  5. }
  6. }
  7. alert('Done!');

我们需要提供一种方法,以在用户取消输入时来停止这个过程。

input 之后的普通 break 只会打破内部循环。这还不够 —— 标签可以实现这一功能!

标签 是在循环之前带有冒号的标识符:

  1. labelName: for (...) {
  2. ...
  3. }

break 语句跳出循环至标签处:

  1. outer: for (let i = 0; i < 3; i++) {
  2. for (let j = 0; j < 3; j++) {
  3. let input = prompt(`Value at coords (${i},${j})`, '');
  4. // 如果是空字符串或被取消,则中断并跳出这两个循环。
  5. if (!input) break outer; // (*)
  6. // 用得到的值做些事……
  7. }
  8. }
  9. alert('Done!');

上述代码中,break outer 向上寻找名为 outer 的标签并跳出当前循环。

因此,控制权直接从 (*) 转至 alert('Done!')

我们还可以将标签移至单独一行:

  1. outer:
  2. for (let i = 0; i < 3; i++) { ... }

continue 指令也可以与标签一起使用。在这种情况下,执行跳转到标记循环的下一次迭代。

标签并不允许“跳到”所有位置

标签不允许我们跳到代码的任意位置。

例如,这样做是不可能的:

  1. break label; // 无法跳转到这个标签
  2. label: for (...)

只有在循环内部才能调用 break/continue,并且标签必须位于指令上方的某个位置。

JS函数

我们经常需要在脚本的许多地方执行很相似的操作。

例如,当访问者登录、注销或者在其他地方时,我们需要显示一条好看的信息。

函数是程序的主要“构建模块”。函数使该段代码可以被调用很多次,而不需要写重复的代码。

我们已经看到了内置函数的示例,如 alert(message)prompt(message, default)confirm(question)。但我们也可以创建自己的函数。定义函数

函数声明

使用 函数声明 创建函数

  1. function showMessage() {
  2. alert('Hello everyone !');
  3. }

function 关键字首先出现,然后是 函数名,然后是括号之间的 参数 列表(用逗号分隔,在上述示例中为空),最后是花括号之间的代码(即“函数体”)。

  1. function name(parameters) {
  2. ...body...
  3. }

我们的新函数可以通过名称调用:showMessage()

  1. function showMessage() {
  2. alert( 'Hello everyone!' );
  3. }
  4. showMessage();
  5. showMessage();

局部变量

在函数中声明的变量只在该函数内部可见。

  1. function showMessage() {
  2. let message = "Hello, I'm JavaScript!"; // 局部变量
  3. alert( message );
  4. }
  5. showMessage(); // Hello, I'm JavaScript!
  6. alert( message ); // <-- 错误!变量是函数的局部变量

外部变量

函数也可以访问外部变量

  1. let userName = 'John';
  2. function showMessage () {
  3. let message = 'Hello' + userName ;
  4. console.log(message);
  5. }
  6. showMessage();

函数对外部变量拥有全部的访问权限。函数也可以修改外部变量。

  1. let userName = 'John';
  2. function showMessage() {
  3. userName = "Bob"; // (1) 改变外部变量
  4. let message = 'Hello, ' + userName;
  5. alert(message);
  6. }
  7. alert( userName ); // John 在函数调用之前
  8. showMessage();
  9. alert( userName ); // Bob,值被函数修改了

只有在没有局部变量的情况下才会使用外部变量。

如果在函数内部声明了同名变量,那么函数会 遮蔽 外部变量。例如,在下面的代码中,函数使用局部的 userName,而外部变量被忽略:

全局变量

任何函数之外声明的变量,例如上述代码中的外部变量 userName,都被称为 全局 变量。

全局变量在任意函数中都是可见的(除非被局部变量遮蔽)。

减少全局变量的使用是一种很好的做法。现代的代码有很少甚至没有全局变量。大多数变量存在于它们的函数中。但是有时候,全局变量能够用于存储项目级别的数据。

参数

我们可以使用参数(也称“函数参数”)来将任意数据传递给函数。

  1. function showMessage(from, text) { // 参数:from 和 text
  2. alert(from + ': ' + text);
  3. }
  4. showMessage('Ann', 'Hello!'); // Ann: Hello! (*)
  5. showMessage('Ann', "What's up?"); // Ann: What's up? (**)

参数默认值

如果未提供参数,那么其默认值则是 undefined

后备的参数

有些时候,将参数默认值的设置放在函数执行(相较更后期)而不是函数声明的时候,也能行得通。

为了判断参数是否被省略掉,我们可以拿它跟 undefined 做比较:

  1. function showMessage(text) {
  2. if ( text == undefined) {
  3. // 后备参数
  4. text = 'test';
  5. }
  6. console.log(text);
  7. }
  8. showMessage();
  9. // 也可以使用 ||
  10. // 如果 "text" 参数被省略或者被传入空字符串,则赋值为 'empty'
  11. function showMessage(text) {
  12. text = text || 'empty'; // 如果text==null或者 text == undefined ; 则 text == 'empty'
  13. }
  14. // 使用 ?? 空值合并运算 ;
  15. // 如果没有传入 "count" 参数,则显示 "unknown"
  16. function showCount(count) {
  17. alert(count ?? "unknown");
  18. }

返回值

函数可以将一个值返回到调用代码中作为结果。

  1. function sum (a,b) {
  2. return a + b ;
  3. }
  4. console.log(sum(2,3));

指令 return 可以在函数的任意位置。当执行到达时,函数停止,并将值返回给调用代码(分配给上述代码中的 result)。

在一个函数中可能会出现很多次 return。例如:

  1. function checkAge(age) {
  2. if (age >= 18) {
  3. return true;
  4. } else {
  5. return confirm('Got a permission from the parents?');
  6. }
  7. }
  8. let age = prompt('How old are you?', 18);
  9. if ( checkAge(age) ) {
  10. alert( 'Access granted' );
  11. } else {
  12. alert( 'Access denied' );
  13. }

只使用 return 但没有返回值也是可行的。但这会导致函数立即退出。

  1. function showMovie(age) {
  2. if (!checkAge(age)) {
  3. return;
  4. }
  5. console.log("Showing you the movie ");
  6. }
  7. // 在上述代码中,如果 checkAge(age) 返回 false,那么 showMovie 将不会运行到 alert。

空值的 **return** 或没有 **return** 的函数返回值为 **undefined**

如果函数无返回值,它就会像返回 undefined 一样:

  1. function doNothing() { /* 没有代码 */ }
  2. alert( doNothing() === undefined ); // true

空值的 returnreturn undefined 等效:

  1. function doNothing() {
  2. return;
  3. }
  4. alert( doNothing() === undefined ); // true

不要在 **return** 与返回值之间添加新行

对于 return 的长表达式,可能你会很想将其放在单独一行,如下所示:

  1. return
  2. (some + long + expression + or + whatever * f(a) + f(b))

但这不行,因为 JavaScript 默认会在 return 之后加上分号。上面这段代码和下面这段代码运行流程相同:

  1. return;
  2. (some + long + expression + or + whatever * f(a) + f(b))

因此,实际上它的返回值变成了空值。

如果我们想要将返回的表达式写成跨多行的形式,那么应该在 return 的同一行开始写此表达式。或者至少按照如下的方式放上左括号:

  1. return (
  2. some + long + expression
  3. + or +
  4. whatever * f(a) + f(b)
  5. )

然后它就能像我们预想的那样正常运行了。

函数的命名

函数就是行为(action)。所以它们的名字通常是动词。它应该简短且尽可能准确地描述函数的作用。这样读代码的人就能清楚地知道这个函数的功能。

一种普遍的做法是用动词前缀来开始一个函数,这个前缀模糊地描述了这个行为。团队内部必须就前缀的含义达成一致。

例如,以 "show" 开头的函数通常会显示某些内容。

函数以 XX 开始……

  • "get…" —— 返回一个值,
  • "calc…" —— 计算某些内容,
  • "create…" —— 创建某些内容,
  • "check…" —— 检查某些内容并返回 boolean 值,等。

这类名字的示例:

  1. showMessage(..) // 显示信息
  2. getAge(..) // 返回 age(gets it somehow)
  3. calcSum(..) // 计算求和并返回结果
  4. createForm(..) // 创建表格(通常会返回它)
  5. checkPermission(..) // 检查权限并返回 true/false

有了前缀,只需瞥一眼函数名,就可以了解它的功能是什么,返回什么样的值。

一个函数 —— 一个行为

一个函数应该只包含函数名所指定的功能,而不是做更多与函数名无关的功能。

两个独立的行为通常需要两个函数,即使它们通常被一起调用(在这种情况下,我们可以创建第三个函数来调用这两个函数)。

有几个违反这一规则的例子:

  • getAge —— 如果它通过 alert 将 age 显示出来,那就有问题了(只应该是获取)。
  • createForm —— 如果它包含修改文档的操作,例如向文档添加一个表单,那就有问题了(只应该创建表单并返回)。
  • checkPermission —— 如果它显示 access granted/denied 消息,那就有问题了(只应执行检查并返回结果)。

这些例子假设函数名前缀具有通用的含义。你和你的团队可以自定义这些函数名前缀的含义,但是通常都没有太大的不同。无论怎样,你都应该对函数名前缀的含义、带特定前缀的函数可以做什么以及不可以做什么有深刻的了解。所有相同前缀的函数都应该遵守相同的规则。并且,团队成员应该形成共识。

函数 == 注释

函数应该简短且只有一个功能。如果这个函数功能复杂,那么把该函数拆分成几个小的函数是值得的。有时候遵循这个规则并不是那么容易,但这绝对是件好事。

一个单独的函数不仅更容易测试和调试 —— 它的存在本身就是一个很好的注释!

例如,比较如下两个函数 showPrimes(n)。它们的功能都是输出到 n 的 素数。

  1. function showPrimes(n) {
  2. nextPrime: for ( let i = 2 ; i < n ; i++) {
  3. for ( let j = 2; j < i ; j++) {
  4. if ( i % j == 0) continue nextPrime; // 如果 i % j == 0 ; 则continue终止循环, 然后执行 nextPrime ;
  5. }
  6. console.log(i);
  7. }
  8. }
  1. // 第二个变体使用附加函数 isPrime(n) 来检验素数:
  2. function showPrimes(n) {
  3. for ( let i = 2 ; i < n ; i++) {
  4. if (!isPrime(i)) continue; //
  5. console.log(i + '是一个素数');
  6. }
  7. }
  8. function isPrime(n) {
  9. for ( let i < 2; i < n; i++) {
  10. if (n%i==0) return false;
  11. }
  12. return true;
  13. }

我们通过函数名(isPrime)就可以看出函数的行为,而不需要通过代码。人们通常把这样的代码称为 自描述

因此,即使我们不打算重用它们,也可以创建函数。函数可以让代码结构更清晰,可读性更强。

总结

函数声明方式如下所示:

  1. function name(parameters, delimited, by, comma) {
  2. /* code */
  3. }
  • 作为参数传递给函数的值,会被复制到函数的局部变量。
  • 函数可以访问外部变量。但它只能从内到外起作用。函数外部的代码看不到函数内的局部变量。
  • 函数可以返回值。如果没有返回值,则其返回的结果是 undefined

为了使代码简洁易懂,建议在函数中主要使用局部变量和参数,而不是外部变量。

与不获取参数但将修改外部变量作为副作用的函数相比,获取参数、使用参数并返回结果的函数更容易理解。

函数命名:

  • 函数名应该清楚地描述函数的功能。当我们在代码中看到一个函数调用时,一个好的函数名能够让我们马上知道这个函数的功能是什么,会返回什么。
  • 一个函数是一个行为,所以函数名通常是动词。
  • 目前有许多优秀的函数名前缀,如 create…show…get…check… 等等。使用它们来提示函数的作用吧。

箭头函数

创建函数还有另外一种非常简单的语法,并且这种方法通常比函数表达式更好。

它被称为“箭头函数”,因为它看起来像这样:

  1. let func = (arg1,arg2,... argN) => expression

……这里创建了一个函数 func,它接受参数 arg1..argN,然后使用参数对右侧的 expression 求值并返回其结果。

换句话说,它是下面这段代码的更短的版本:

  1. let func = function (arg1,arg2,...argN) {
  2. return expression;
  3. }

例子

  1. let sum = (a, b) => a + b;
  2. /* 这个箭头函数是下面这个函数的更短的版本:
  3. let sum = function(a, b) {
  4. return a + b;
  5. };
  6. */
  7. alert( sum(1, 2) ); // 3

可以看到 (a, b) => a + b 表示一个函数接受两个名为 ab 的参数。在执行时,它将对表达式 a + b 求值,并返回计算结果。

  • 如果我们只有一个参数,还可以省略掉参数外的圆括号,使代码更短。
    例如:
    1. let double = n => n * 2 ;
    2. // 差不多等同于:let double = function(n) { return n * 2 }
    3. console.log(double(2));
  • 如果没有参数,括号将是空的(但括号应该保留):
    1. let sayHi = () => alert("Hello!");
    2. sayHi();

箭头函数可以像函数表达式一样使用。

例如,动态创建一个函数:

  1. let age = prompt("What is your age ?",18);
  2. let welcome = (age < 18 ) ?
  3. () => console.log('Hello'):
  4. () => console.log('Creetings !');
  5. welcome();

一开始,箭头函数可能看起来并不熟悉,也不容易读懂,但一旦我们看习惯了之后,这种情况很快就会改变。

箭头函数对于简单的单行行为(action)来说非常方便,尤其是当我们懒得打太多字的时候。

多行的箭头函数

上面的例子从 => 的左侧获取参数,然后使用参数计算右侧表达式的值。

但有时我们需要更复杂一点的东西,比如多行的表达式或语句。这也是可以做到的,但是我们应该用花括号括起来。然后使用一个普通的 return 将需要返回的值进行返回。

  1. let sum = (a,b) => { // 花括号表示开始一个多行函数
  2. let result = a + b ;
  3. return result; // 如果我们使用了花括号,那么我们需要一个显式的 “return”
  4. }
  5. console.log(sum(1,2));

在这里,我们赞扬了箭头函数的简洁性。但还不止这些!

箭头函数还有其他有趣的特性。

为了更深入地学习它们,我们首先需要了解一些 JavaScript 其他方面的知识,因此我们将在后面的 深入理解箭头函数 一章中再继续研究箭头函数。

现在,我们已经可以用箭头函数进行单行行为和回调了。

箭头函数的总结

对于一行代码的函数来说,箭头函数是相当方便的。它具体有两种:

  1. 不带花括号:(...args) => expression — 右侧是一个表达式:函数计算表达式并返回其结果。
  2. 带花括号:(...args) => { body }; — 花括号允许我们在函数中编写多个语句,但是我们需要显式地 return 来返回一些内容。
  1. // 1
  2. let ask = (question, yes, no) => {
  3. if (confirm(question)) yes()
  4. else no();
  5. };
  6. ask(
  7. "Do you agree?",
  8. () => console.log('You agreed');
  9. () => console.log('You canceled the execution ');
  10. );
  11. // 2
  12. let ask = (question, yes, no) => confirm(question) ? yes() : no();
  13. ask(
  14. "Do you agree?",
  15. () => console.log('You agreed');
  16. () => console.log('You canceled the execution ');
  17. );

JavaScript 特性

代码结构

语句用分号分隔:

  1. alert('Hello'); alert('World');

通常,换行符也被视为分隔符,因此下面的例子也能正常运行:

  1. alert('Hello')
  2. alert('World')

这就是所谓的「自动分号插入」。但有时它不起作用,例如:

  1. alert("There will be an error after this message")
  2. [1, 2].forEach(alert)

大多数代码风格指南都认为我们应该在每个语句后面都加上分号。

在代码块 {...} 后以及有代码块的语法结构(例如循环)后不需要加分号:

  1. function f() {
  2. // 函数声明后不需要加分号
  3. }
  4. for(;;) {
  5. // 循环语句后不需要加分号
  6. }

……但即使我们在某处添加了「额外的」分号,这也不是错误。分号会被忽略的。

更多内容:代码结构

严格模式

为了完全启用现代 JavaScript 的所有特性,我们应该在脚本顶部写上 "use strict" 指令。

  1. 'use strict';
  2. ...

该指令必须位于 JavaScript 脚本的顶部或函数体的开头。

如果没有 "use strict",所有东西仍可以正常工作,但是某些特性的表现方式与旧式「兼容」方式相同。我们通常更喜欢现代的方式。

语言的一些现代特征(比如我们将来要学习的类)会隐式地启用严格模式。

更多内容:现代模式,”use strict”

变量

可以使用以下方式声明变量:

  • let
  • const(不变的,不能被改变)
  • var(旧式的,稍后会看到)

一个变量名可以由以下组成:

  • 字母和数字,但是第一个字符不能是数字。
  • 字符 $_ 是允许的,用法同字母。
  • 非拉丁字母和象形文字也是允许的,但通常不会使用。

变量是动态类型的,它们可以存储任何值:

  1. let x = 5;
  2. x = "John";

有 8 种数据类型:

  • number — 可以是浮点数,也可以是整数,
  • bigint — 用于任意长度的整数,
  • string — 字符串类型,
  • boolean — 逻辑值:true/false
  • null — 具有单个值 null 的类型,表示“空”或“不存在”,
  • undefined — 具有单个值 undefined 的类型,表示“未分配(未定义)”,
  • objectsymbol — 对于复杂的数据结构和唯一标识符,我们目前还没学习这个类型。

typeof 运算符返回值的类型,但有两个例外:

  1. typeof null == "object" // JavaScript 编程语言的设计错误
  2. typeof function(){} == "function" // 函数被特殊对待

更多内容:变量数据类型

交互

我们使用浏览器作为工作环境,所以基本的 UI 功能将是:

  • [prompt(question[, default\])](https://developer.mozilla.org/zh/docs/Web/API/Window/prompt)
    提出一个问题,并返回访问者输入的内容,如果他按下「取消」则返回 null
  • [confirm(question)](https://developer.mozilla.org/zh/docs/Web/API/Window/confirm)
    提出一个问题,并建议用户在“确定”和“取消”之间进行选择。选择结果以 true/false 形式返回。
  • [alert(message)](https://developer.mozilla.org/zh/docs/Web/API/Window/alert)
    输出一个 消息

这些函数都会产生 模态框,它们会暂停代码执行并阻止访问者与页面的其他部分进行交互,直到用户做出回答为止。

举个例子:

  1. let userName = prompt("Your name?", "Alice");
  2. let isTeaWanted = confirm("Do you want some tea?");
  3. alert( "Visitor: " + userName ); // Alice
  4. alert( "Tea wanted: " + isTeaWanted ); // true

更多内容:交互:alert、prompt 和 confirm

运算符

JavaScript 支持以下运算符:

  • 算数运算符
    常规的:+ - * /(加减乘除),取余运算符 % 和幂运算符 **。二进制加号 + 可以连接字符串。如果任何一个操作数是一个字符串,那么另一个操作数也将被转换为字符串:alert( '1' + 2 ); // '12',字符串 alert( 1 + '2' ); // '12',字符串
  • 赋值
    简单的赋值:a = b 和合并了其他操作的赋值:a * = 2
  • 按位运算符
    按位运算符在最低位级上操作 32 位的整数:详见 文档
  • 三元运算符
    唯一具有三个参数的操作:cond ? resultA : resultB。如果 cond 为真,则返回 resultA,否则返回 resultB
  • 逻辑运算符
    逻辑与 && 和或 || 执行短路运算,然后返回运算停止处的值(true/false 不是必须的)。逻辑非 ! 将操作数转换为布尔值并返回其相反的值。
  • 空值合并运算符
    ?? 运算符从一列变量中,选取值为已定义的值(defined value)的变量。a ?? b 的结果是 a,除非 anull/undefined,这时结果是 b
  • 比较运算符
    对不同类型的值进行相等检查时,运算符 == 会将不同类型的值转换为数字(除了 nullundefined,它们彼此相等而没有其他情况),所以下面的例子是相等的:alert( 0 == false ); // true alert( 0 == '' ); // true其他比较也将转换为数字。严格相等运算符 === 不会进行转换:不同的类型总是指不同的值。值 nullundefined 是特殊的:它们只在 == 下相等,且不相等于其他任何值。大于/小于比较,在比较字符串时,会按照字符顺序逐个字符地进行比较。其他类型则被转换为数字。
  • 其他运算符
    还有很少一部分其他运算符,如逗号运算符。

更多内容:基础运算符,数学值的比较逻辑运算符空值合并运算符 ‘??’

循环

  • 我们涵盖了 3 种类型的循环: ```javascript // 1 while (condition) { … }

// 2 do { … } while (condition);

// 3 for(let i = 0; i < 10; i++) { … }

  1. - `for(let...)` 循环内部声明的变量,只在该循环内可见。但我们也可以省略 `let` 并重用已有的变量。
  2. - 指令 `break/continue` 允许退出整个循环/当前迭代。使用标签来打破嵌套循环。
  3. 更多内容:[循环:while for](https://zh.javascript.info/while-for)。
  4. 稍后我们将学习更多类型的循环来处理对象。
  5. <a name="20b56f8d"></a>
  6. ## [“switch” 结构](https://zh.javascript.info/javascript-specials#switch-jie-gou)
  7. switch 结构可以替代多个 `if` 检查。它内部使用 `===`(严格相等)进行比较。
  8. 例如:
  9. ```javascript
  10. let age = prompt('Your age?', 18);
  11. switch (age) {
  12. case 18:
  13. alert("Won't work"); // prompt 的结果是一个字符串,而不是数字
  14. break;
  15. case "18":
  16. alert("This works!");
  17. break;
  18. default:
  19. alert("Any value not equal to one above");
  20. }

详情请见:“switch” 语句

函数

我们介绍了三种在 JavaScript 中创建函数的方式:

  1. 函数声明:主代码流中的函数

    1. function sum(a, b) {
    2. let result = a + b;
    3. return result;
    4. }
  1. 函数表达式:表达式上下文中的函数

    1. let sum = function(a, b) {
    2. let result = a + b;
    3. return result;
    4. }
  1. 箭头函数: ```javascript // 表达式在右侧 let sum = (a, b) => a + b;

// 或带 {…} 的多行语法,此处需要 return: let sum = (a, b) => { // … return a + b; }

// 没有参数 let sayHi = () => alert(“Hello”);

// 有一个参数 let double = n => n * 2;

  1. - 函数可能具有局部变量:在函数内部声明的变量。这类变量只在函数内部可见。
  2. - 参数可以有默认值:`function sum(a = 1, b = 2) {...}`
  3. - 函数总是返回一些东西。如果没有 `return` 语句,那么返回的结果是 `undefined`
  4. <a name="3444f10d"></a>
  5. # JavaScript 代码质量
  6. <a name="d230d14e"></a>
  7. ## 代码风格
  8. 我们的代码必须尽可能的清晰和易读。
  9. 这实际上是一种编程艺术 —— 以一种正确并且人们易读的方式编码来完成一个复杂的任务。一个良好的代码风格大大有助于实现这一点。
  10. <a name="f2b0b493"></a>
  11. ## 语法
  12. ![](https://gitee.com/yunhai0644/imghub/raw/master/20210815163043.png#crop=0&crop=0&crop=1&crop=1&id=Dvtug&originHeight=728&originWidth=1095&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
  13. 例子
  14. 没有人喜欢读一长串代码,最好将代码分割一下。
  15. ```javascript
  16. // 回勾引号 ` 允许将字符串拆分为多行
  17. let str = `
  18. ECMA International's TC39 is a group of JavaScript developers,
  19. implementers, academics, and more, collaborating with the community
  20. to maintain and evolve the definition of JavaScript.
  21. `;

对于 if 语句:

  1. // 不合理写法 ; 一行的代码太长;
  2. if ( id === 123 && moonPhase === 'Waning Gibbous' && zodiacSign === 'Libra') {
  3. letTheSorceryBegin();
  4. }
  5. // 合理写法
  6. if (
  7. id === 123 &&
  8. moonPhase === 'Waning Gibbous' &&
  9. zodiacSign === 'Libra'
  10. ) {
  11. letTheSorceryBegin();
  12. }

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

  1. // 不好的 js代码 风格
  2. function pow(x,n)
  3. {
  4. let result=1;
  5. for(let i=0;i<n;i++) {result*=x;}
  6. return result;
  7. }
  8. let x=prompt("x?",''), n=prompt("n?",'')
  9. if (n<=0)
  10. {
  11. alert(`Power ${n} is not supported, please enter an integer number greater than zero`);
  12. }
  13. else
  14. {
  15. alert(pow(x,n))
  16. }
  1. // pow()
  2. function pow(x, n) {
  3. let result = 1;
  4. for ( let i = 0; i < n; i++) result *= x;
  5. return result;
  6. }
  7. // input
  8. let x = prompt("x?", '');
  9. let n = prompt("n?", '');
  10. if ( n <= 0) {
  11. alert(`Power ${n} is not supported,
  12. please enter an integer number greater than zero`);
  13. }else {
  14. alert( pow(x, n) );
  15. }

JavaScript 面向对象编程

对象

正如我们在 数据类型 一章学到的,JavaScript 中有八种数据类型。有七种原始类型,因为它们的值只包含一种东西(字符串,数字或者其他)。

相反,对象则用来存储键值对和更复杂的实体。在 JavaScript 中,对象几乎渗透到了这门编程语言的方方面面。所以,在我们深入理解这门语言之前,必须先理解对象。

我们可以通过使用带有可选 属性列表 的花括号 {…} 来创建对象。一个属性就是一个键值对(“key: value”),其中键(key)是一个字符串(也叫做属性名),值(value)可以是任何值。

我们可以把对象想象成一个带有签名文件的文件柜。每一条数据都基于键(key)存储在文件中。这样我们就可以很容易根据文件名(也就是“键”)查找文件或添加/删除文件了。

创建对象

  1. let user = new Object(); // “构造函数” 的语法
  2. let user = {}; // 字面量 的语法

文本和属性

  1. let user = { // 一个对象
  2. name: 'John', // 键 name , 值 John
  3. age: 30 // 键 "age" , 值 30
  4. };

属性有键(或者也可以叫做“名字”或“标识符”),位于冒号 ":" 的前面,值在冒号的右边。

user 对象中,有两个属性:

  1. 第一个的键是 "name",值是 "John"
  2. 第二个的键是 "age",值是 30

生成的 user 对象可以被想象为一个放置着两个标记有 “name” 和 “age” 的文件的柜子。

JavaScript基础 - 图10

我们可以随时添加、删除和读取文件。

  1. // 读取文件的属性
  2. console.log(user.name);
  3. console.log(user.age);
  4. // 属性的值可以是任意类型,让我们加个布尔类型:
  5. user.isAdmin = true;
  6. // 删除属性
  7. delete user.age;

我们也可以用多字词语来作为属性名,但必须给它们加上引号:

  1. let user {
  2. name: "John",
  3. age: 30,
  4. "likes birds": true // 多词属性名必须加上引号
  5. };

列表中的最后一个属性应以逗号结尾:

  1. let user = {
  2. name: "John",
  3. age: 30,
  4. }

这叫做尾随(trailing)或悬挂(hanging)逗号。这样便于我们添加、删除和移动属性,因为所有的行都是相似的。

使用 const 声明的对象是可以被修改的

请注意:用 const 声明的对象 被修改。

例如:

(*) 行似乎会触发一个错误,但实际并没有。const 声明仅固定了 user 的值,而不是值(该对象)里面的内容。

仅当我们尝试将 user=... 作为一个整体进行赋值时,const 会抛出错误。

有另一种将对象属性变为常量的方式,我们将在后面的 属性标志和属性描述符 一章中学习它。

  1. const user = {
  2. name: "John"
  3. };
  4. user.name = "Pete"; // (*)
  5. alert(user.name); // Pete

方括号

对于多词属性,点操作就不能用了:

  1. // 这将提示有语法错误
  2. user.likes birds = true

JavaScript 理解不了。它认为我们在处理 user.likes,然后在遇到意外的 birds 时给出了语法错误。

点符号要求 key 是有效的变量标识符。这意味着:不包含空格,不以数字开头,也不包含特殊字符(允许使用 **$****_**)。

有另一种方法,就是使用方括号,可用于任何字符串:

  1. let user = {};
  2. // 设置
  3. user["likes birds"] = true;
  4. // 读取
  5. alert(user["likes birds"]); // true
  6. // 删除
  7. delete user["likes birds"];

现在一切都可行了。请注意方括号中的字符串要放在引号中,单引号或双引号都可以。

方括号同样提供了一种可以通过任意表达式来获取属性名的方法 —— 跟语义上的字符串不同 —— 比如像类似于下面的变量:

  1. let key = "likes birds";
  2. // 跟 user["likes birds"] = true; 一样
  3. user[key] = true;

在这里,变量 key 可以是程序运行时计算得到的,也可以是根据用户的输入得到的。然后我们可以用它来访问属性。这给了我们很大的灵活性。

  1. let user = {
  2. name: "John",
  3. age: 30
  4. };
  5. let key = prompt("What do you want to know about the user?", "name");
  6. // 访问变量
  7. alert( user[key] ); // John(如果输入 "name")

计算属性

当创建一个对象时,我们可以在对象字面量中使用方括号。这叫做 计算属性

例如:

  1. let fruit = prompt("Which fruit to buy?", "apple");
  2. let bag = {
  3. [fruit]: 5, // 属性名是从 fruit 变量中得到的
  4. };
  5. alert( bag.apple ); // 5 如果 fruit="apple"

计算属性的含义很简单:[fruit] 含义是属性名应该从 fruit 变量中获取。

所以,如果一个用户输入 "apple"bag 将变为 {apple: 5}

本质上,这跟下面的语法效果相同:

  1. let fruit = prompt("Which fruit to buy?", "apple");
  2. let bag = {};
  3. // 从 fruit 变量中获取值
  4. bag[fruit] = 5;

……但是看起来更好。

我们可以在方括号中使用更复杂的表达式:

  1. let fruit = 'apple';
  2. let bag = {
  3. [fruit + 'Computers']: 5 // bag.appleComputers = 5
  4. };

方括号比点符号更强大。它允许任何属性名和变量,但写起来也更加麻烦。

所以,大部分时间里,当属性名是已知且简单的时候,就使用点符号。如果我们需要一些更复杂的内容,那么就用方括号。

属性值简写

在实际开发中,我们通常用已存在的变量当做属性名。

  1. function makeUser (name, age) {
  2. return {
  3. name: name,
  4. age: age
  5. // 其他属性
  6. };
  7. }
  8. let user = makeUser("John", 30);
  9. console.log(user);

在上面的例子中,属性名跟变量名一样。这种通过变量生成属性的应用场景很常见,在这有一种特殊的 属性值缩写 方法,使属性名变得更短。

可以用 name 来代替 name:name 像下面那样:

  1. function makeUser(name, age) {
  2. return {
  3. name, // 与 name: name 相同
  4. age, // 与 age: age 相同
  5. // ...
  6. };
  7. }

我们可以把属性名简写方式和正常方式混用:

  1. let user = {
  2. name, // 与 name:name 相同
  3. age: 30
  4. };

属性名限制

我们已经知道,变量名不能是编程语言的某个保留字,如 “for”、“let”、“return” 等……

但对象的属性名并不受此限制:

  1. // 这些属性都没问题
  2. let obj = {
  3. for: 1,
  4. let: 2,
  5. return: 3
  6. };
  7. alert( obj.for + obj.let + obj.return ); // 6

简而言之,属性命名没有限制。属性名可以是任何字符串或者 symbol(一种特殊的标志符类型,将在后面介绍)。

其他类型会被自动地转换为字符串。

例如,当数字 0 被用作对象的属性的键时,会被转换为字符串 "0"

  1. let obj = {
  2. 0: "test" // 等同于 "0": "test"
  3. };
  4. // 都会输出相同的属性(数字 0 被转为字符串 "0")
  5. alert( obj["0"] ); // test
  6. alert( obj[0] ); // test (相同的属性)

这里有个小陷阱:一个名为 __proto__ 的属性。我们不能将它设置为一个非对象的值:

  1. let obj = {};
  2. obj.__proto__ = 5; // 分配一个数字
  3. alert(obj.__proto__); // [object Object] — 值为对象,与预期结果不同

我们从代码中可以看出来,把它赋值为 5 的操作被忽略了。

我们将在 后续章节 中学习 __proto__ 的特殊性质,并给出了 解决此问题的方法

属性存在性测试 in 操作

相比于其他语言,JavaScript 的对象有一个需要注意的特性:能够被访问任何属性。即使属性不存在也不会报错!

读取不存在的属性只会得到 undefined。所以我们可以很容易地判断一个属性是否存在:

  1. let user = {};
  2. console.log(user.noSuchProperty === undefined); // true 意思是没有这个属性

这里还有一个特别的,检查属性是否存在的操作符 "in"

  1. let user = {
  2. name: "yellowsea",
  3. age: 20
  4. };
  5. console.log("age" in user); // true
  6. console.log("blabla" in user); // false
  7. // 请注意,in 的左边必须是 属性名。通常是一个带引号的字符串。

请注意,in 的左边必须是 属性名。通常是一个带引号的字符串。

如果我们省略引号,就意味着左边是一个变量,它应该包含要判断的实际属性名。例如:

  1. let user = { age: 30 };
  2. let key = "age";
  3. alert( key in user ); // true,属性 "age" 存在

为何会有 in 运算符呢?与 undefined 进行比较来判断还不够吗?

确实,大部分情况下与 undefined 进行比较来判断就可以了。但有一个例外情况,这种比对方式会有问题,但 in 运算符的判断结果仍是对的。

那就是属性存在,但存储的值是 undefined 的时候:

  1. let obj = {
  2. test: undefined
  3. };
  4. // 区别
  5. alert( obj.test ); // 显示 undefined,所以属性不存在?
  6. alert( "test" in obj ); // true,属性存在!

在上面的代码中,属性 obj.test 事实上是存在的,所以 in 操作符检查通过。

这种情况很少发生,因为通常情况下不应该给对象赋值 undefined。我们通常会用 null 来表示未知的或者空的值。因此,in 运算符是代码中的特殊来宾。

for… in 循环

为了遍历一个对象的所有键(key),可以使用一个特殊形式的循环:for..in。这跟我们在前面学到的 for(;;) 循环是完全不一样的东西。

  1. for (key in object) {
  2. // 对此对象属性中的每个键执行的代码
  3. }
  1. // for... in
  2. let user = {
  3. name: 'John',
  4. age: 20,
  5. isAdmin: false
  6. };
  7. for (let key in user) {
  8. console.log(key); // key 属性名
  9. console.log(user[key]); //属性值
  10. }

注意,所有的 “for” 结构体都允许我们在循环中定义变量,像这里的 let key

同样,我们可以用其他属性名来替代 key。例如 "for(let prop in obj)" 也很常用

属性值排列

对象有顺序吗?换句话说,如果我们遍历一个对象,我们获取属性的顺序是和属性添加时的顺序相同吗?这靠谱吗?

简短的回答是:“有特别的顺序”:整数属性会被进行排序,其他属性则按照创建的顺序显示。详情如下:

例如,让我们考虑一个带有电话号码的对象:

  1. // 属性值排列
  2. let codes = {
  3. '49': "A",
  4. "42": "B",
  5. "44": "C",
  6. "1": "D"
  7. };
  8. for (let code in codes) {
  9. console.log(code); // 会按照顺序排列 。 1 ....
  10. //因为这些电话号码是整数,所以它们以升序排列。所以我们看到的是 1, 41, 44, 49。
  11. }

这里的“整数属性”指的是一个可以在不做任何更改的情况下与一个整数进行相互转换的字符串。

所以,“49” 是一个整数属性名,因为我们把它转换成整数,再转换回来,它还是一样的。但是 “+49” 和 “1.2” 就不行了:

  1. // Math.trunc 是内置的去除小数部分的方法。
  2. alert( String(Math.trunc(Number("49"))) ); // "49",相同,整数属性
  3. alert( String(Math.trunc(Number("+49"))) ); // "49",不同于 "+49" ⇒ 不是整数属性
  4. alert( String(Math.trunc(Number("1.2"))) ); // "1",不同于 "1.2" ⇒ 不是整数属性

所以,为了解决电话号码的问题,我们可以使用非整数属性名来 欺骗 程序。只需要给每个键名加一个加号 "+" 前缀就行了。

像这样:

  1. let codes = {
  2. "+49": "Germany",
  3. "+41": "Switzerland",
  4. "+44": "Great Britain",
  5. // ..,
  6. "+1": "USA"
  7. };
  8. for (let code in codes) {
  9. alert( +code ); // 49, 41, 44, 1
  10. }

现在跟预想的一样了。

对象初识总结

对象是具有一些特殊特性的关联数组。

它们存储属性(键值对),其中:

  • 属性的键必须是字符串或者 symbol(通常是字符串)。
  • 值可以是任何类型。

我们可以用下面的方法访问属性:

  • 点符号: obj.property
  • 方括号 obj["property"],方括号允许从变量中获取键,例如 obj[varWithKey]

其他操作:

  • 删除属性:delete obj.prop
  • 检查是否存在给定键的属性:"key" in obj
  • 遍历对象:for(let key in obj) 循环。

我们在这一章学习的叫做“普通对象(plain object)”,或者就叫对象。

JavaScript 中还有很多其他类型的对象:

  • Array 用于存储有序数据集合,
  • Date 用于存储时间日期,
  • Error 用于存储错误信息。
  • ……等等。

它们有着各自特别的特性,我们将在后面学习到。有时候大家会说“Array 类型”或“Date 类型”,但其实它们并不是自身所属的类型,而是属于一个对象类型即 “object”。它们以不同的方式对 “object” 做了一些扩展。

JavaScript 中的对象非常强大。这里我们只接触了其冰山一角。在后面的章节中,我们将频繁使用对象进行编程,并学习更多关于对象的知识。

  1. // 写一个 isEmpty(obj) 函数,当对象没有属性的时候返回 true,否则返回 false。
  2. let isEmpty = (schedule) => {
  3. for (let key in schedule) {
  4. return false;
  5. }
  6. return true;
  7. };
  8. let schedule = {};
  9. console.log(isEmpty(schedule));
  10. schedule["8:30"] = "get up";
  11. console.log(isEmpty(schedule)); // false
  1. // 对象属性求和
  2. // 我们有一个保存着团队成员工资的对象
  3. let salaries = {
  4. John: 100,
  5. Ann: 160,
  6. Pete: 130
  7. };
  8. // 写一段代码求出我们的工资总和,将计算结果保存到变量 sum。从所给的信息来看,结果应该是 390。
  9. // let sum = salaries.John + salaries.Ann + salaries.Pete;
  10. // console.log(sum);
  11. let sum = 0;
  12. for (let key in salaries) {
  13. sum += salaries[key];
  14. }
  15. console.log(sum);

JavaScript对象引用和复制

与原始类型相比,对象的根本区别之一是对象是“通过引用”被存储和复制的,与原始类型值相反:字符串,数字,布尔值等 —— 始终是以“整体值”的形式被复制的。

赋值了对象的变量存储的不是对象本身,而是该对象“在内存中的地址”,换句话说就是对该对象的“引用”。

让我们看一个这样的变量的例子:

  1. let user = {
  2. name: "John"
  3. };

这是它实际存储在内存中的方式:

JavaScript基础 - 图11

该对象被存储在内存中的某个位置(在图片的右侧),而变量 user(在左侧)保存的是对其的“引用”。

我们可以将对象变量(例如 user)想象成一张带有地址的纸。

当我们对对象执行操作时,例如获取一个属性 user.name,JavaScript 引擎将对该地址进行搜索,并在实际对象上执行操作。

现在,这就是为什么它很重要。

当一个对象变量被复制 —— 引用则被复制,而该对象并没有被复制。

  1. let user = {name: 'yellowsea'};
  2. let admin = user;

现在我们有了两个变量,它们保存的都是对同一个对象的引用:

JavaScript基础 - 图12

正如你所看到的,这里仍然只有一个对象,现在有两个引用它的变量。

通过引用过来的比较

仅当两个对象为同一对象时,两者才相等。

例如,这里 ab 两个变量都引用同一个对象,所以它们相等:

  1. let a = {};
  2. let b = a; // 复制引用
  3. alert( a == b ); // true,都引用同一对象
  4. alert( a === b ); // true

而这里两个独立的对象则并不相等,即使它们看起来很像(都为空):

  1. let a = {};
  2. let b = {}; // 两个独立的对象
  3. alert( a == b ); // false

克隆与合并 Object.assign 方法

那么,拷贝一个对象变量会又创建一个对相同对象的引用。

但是,如果我们想要复制一个对象,那该怎么做呢?创建一个独立的拷贝,克隆?

这也是可行的,但稍微有点困难,因为 JavaScript 没有提供对此操作的内建的方法。实际上,也很少需要这样做。通过引用进行拷贝在大多数情况下已经很好了。

但是,如果我们真的想要这样做,那么就需要创建一个新对象,并通过遍历现有属性的结构,在原始类型值的层面,将其复制到新对象,以复制已有对象的结构。

  1. let user = {
  2. name: "John",
  3. age: 30
  4. };
  5. let clone = {};
  6. // 将 user 中所有的属性拷贝到其中
  7. for (let key in user) {
  8. clone[key] = user[key];
  9. }
  10. // 现在 clone 是带有相同内容的完全独立的对象
  11. console.log(clone.name);

Object.assign 方法 【合并对象】

语法是:

  1. Object.assign(dest, [src1, src2, src3...])
  • 第一个参数 dest 是指目标对象。
  • 更后面的参数 src1, ..., srcN(可按需传递多个参数)是源对象
  • 该方法将所有源对象的属性拷贝到目标对象 dest 中。换句话说,从第二个开始的所有参数的属性都被拷贝到第一个参数的对象中。
  • 调用结果返回 dest

合并

  1. // 使用 Object.assign 合并对象
  2. let user1 = { name: "John", age: 30 };
  3. let permissions1 = { canView: true };
  4. let permissions2 = { canEdit: true };
  5. // 将 permissions1 和 permissions2 中的所有属性都拷贝到 user 中
  6. Object.assign(user1, permissions1, permissions2);
  7. // 现在 user = { name: "John", canView: true, canEdit: true }
  8. console.log(user1);

如果被拷贝的属性的属性名已经存在,那么它会被覆盖

我们也可以用 Object.assign 代替 for..in 循环来进行简单克隆:

  1. let user = {
  2. name: "John",
  3. age: 30
  4. };
  5. let clone = Object.assign({}, user);

它将 user 中的所有属性拷贝到了一个空对象中,并返回这个新的对象。

深层克隆

到现在为止,我们都假设 user 的所有属性均为原始类型。但属性可以是对其他对象的引用。那应该怎样处理它们呢?

例如:

  1. let user = {
  2. name: "John",
  3. sizes: {
  4. height: 182,
  5. width: 50
  6. }
  7. };
  8. alert( user.sizes.height ); // 182

现在这样拷贝 clone.sizes = user.sizes 已经不足够了,因为 user.sizes 是个对象,它会以引用形式被拷贝。因此 cloneuser 会共用一个 sizes:

就像这样:

  1. let user = {
  2. name: "John",
  3. sizes: {
  4. height: 182,
  5. width: 50
  6. }
  7. };
  8. let clone = Object.assign({}, user);
  9. alert( user.sizes === clone.sizes ); // true,同一个对象
  10. // user 和 clone 分享同一个 sizes
  11. user.sizes.width++; // 通过其中一个改变属性值
  12. alert(clone.sizes.width); // 51,能从另外一个看到变更的结果

为了解决此问题,我们应该使用会检查每个 user[key] 的值的克隆循环,如果值是一个对象,那么也要复制它的结构。这就叫“深拷贝”。

我们可以用递归来实现。或者不自己造轮子,使用现成的实现,例如 JavaScript 库 lodash 中的 _.cloneDeep(obj)

小结

对象通过引用被赋值和拷贝。换句话说,一个变量存储的不是“对象的值”,而是一个对值的“引用”(内存地址)。因此,拷贝此类变量或将其作为函数参数传递时,所拷贝的是引用,而不是对象本身。

所有通过被拷贝的引用的操作(如添加、删除属性)都作用在同一个对象上。

为了创建“真正的拷贝”(一个克隆),我们可以使用 Object.assign 来做所谓的“浅拷贝”(嵌套对象被通过引用进行拷贝)或者使用“深拷贝”函数,例如 _.cloneDeep(obj)

对象方法 “this”

方法简写

  1. // 方法示例
  2. let user = {
  3. name: "John",
  4. age: 28
  5. };
  6. // sayHello
  7. user.sayHello = () => {
  8. console.log("Hello");
  9. }
  10. user.sayHello();

面向对象编程

当我们在代码中用对象表示实体时,就是所谓的 面向对象编程,简称为 “OOP”。

OOP 是一门大学问,本身就是一门有趣的科学。怎样选择合适的实体?如何组织它们之间的交互?这就是架构,有很多关于这方面的书,例如 E. Gamma、R. Helm、R. Johnson 和 J. Vissides 所著的《设计模式:可复用面向对象软件的基础》,G. Booch 所著的《面向对象分析与设计》等

方法中的 “this”

通常,对象方法需要访问对象中存储的信息才能完成其工作。

例如,user.sayHi() 中的代码可能需要用到 user 的 name 属性。

为了访问该对象,方法中可以使用 **this** 关键字。

this 的值就是在点之前的这个对象,即调用该方法的对象。

  1. _// "this" 指的是“当前的对象”_
  1. // this 方法中的this
  2. let user1 = {
  3. name: 'John',
  4. age: 20,
  5. // sayHi = () => { // 这个创建函数的方法不行
  6. // // this 指的是 “当前的对象 ”
  7. // console.log(this.name);
  8. // }
  9. sayHi() {
  10. // this 指的是 “当前的对象 ”
  11. console.log(this.name);
  12. }
  13. };
  14. user1.sayHi();

……但这样的代码是不可靠的。如果我们决定将 user 复制给另一个变量,例如 admin = user,并赋另外的值给 user,那么它将访问到错误的对象

  1. let user = {
  2. name: "John",
  3. age: 30,
  4. sayHi() {
  5. alert( user.name ); // 导致错误
  6. }
  7. };
  8. let admin = user;
  9. user = null; // 重写让其更明显
  10. // 报错
  11. admin.sayHi(); // TypeError: Cannot read property 'name' of null

this 不受限制

在 JavaScript 中,this 关键字与其他大多数编程语言中的不同。JavaScript 中的 this 可以用于任何函数,即使它不是对象的方法。

  1. // 这样写没有错误
  2. let sayHi = () => {
  3. console.log(this.name);
  4. }

this 的值是在代码运行时计算出来的,它取决于代码上下文。

  1. let user = { name: "John" };
  2. let admin = { name: "Admin" };
  3. function sayHi() {
  4. alert( this.name );
  5. }
  6. // 在两个对象中使用相同的函数
  7. user.f = sayHi;
  8. admin.f = sayHi;
  9. // 这两个调用有不同的 this 值
  10. // 函数内部的 "this" 是“点符号前面”的那个对象
  11. user.f(); // John(this == user)
  12. admin.f(); // Admin(this == admin)
  13. admin['f'](); // Admin(使用点符号或方括号语法来访问这个方法,都没有关系。)

这个规则很简单:如果 obj.f() 被调用了,则 thisf 函数调用期间是 obj。所以在上面的例子中 this 先是 user,之后是 admin

在没有对象的情况下调用:**this == undefined**

我们甚至可以在没有对象的情况下调用函数:

  1. function sayHi() {
  2. alert(this);
  3. }
  4. sayHi(); // undefined

在这种情况下,严格模式下的 this 值为 undefined。如果我们尝试访问 this.name,将会报错。

在非严格模式的情况下,this 将会是 全局对象(浏览器中的 window,我们稍后会在 全局对象 一章中学习它)。这是一个历史行为,"use strict" 已经将其修复了。

通常这种调用是程序出错了。如果在一个函数内部有 this,那么通常意味着它是在对象上下文环境中被调用的。

箭头函数没有自己的 this

箭头函数有些特别:它们没有自己的 this。如果我们在这样的函数中引用 thisthis 值取决于外部“正常的”函数。

  1. // 举个例子,这里的 arrow() 使用的 this 来自于外部的 user.sayHi() 方法:
  2. let user = {
  3. firstName = "Ilya",
  4. sayHi() {
  5. let arrow = () => console.log(this.firstName);
  6. arrow();
  7. }
  8. };
  9. user.sayHi(); //Ilya

这是箭头函数的一个特性,当我们并不想要一个独立的 this,反而想从外部上下文中获取时,它很有用。在后面的 深入理解箭头函数 一章中,我们将深入介绍箭头函数。

小结

  • 存储在对象属性中的函数被称为“方法”。
  • 方法允许对象进行像 object.doSomething() 这样的“操作”。
  • 方法可以将对象引用为 this

this 的值是在程序运行时得到的。

  • 一个函数在声明时,可能就使用了 this,但是这个 this 只有在函数被调用时才会有值。
  • 可以在对象之间复制函数。
  • 以“方法”的语法调用函数时:object.method(),调用过程中的 this 值是 object

请注意箭头函数有些特别:它们没有 this。在箭头函数内部访问到的 this 都是从外部获取的。

构造器和操作符 new

常规的 {...} 语法允许创建一个对象。但是我们经常需要创建许多类似的对象,例如多个用户或菜单项等。

也可以使用构造函数和 new 操作符来实现 。

构造函数

构造函数在技术上是常规函数。不过有两个约定:

  1. 它们的命名以大写字母开头。
  2. 它们只能由 "new" 操作符来执行。
  1. function User(name) {
  2. this.name = name;
  3. this.isAdmin = false;
  4. }
  5. let user = User('Yellowsea');
  6. console.log(user.name);
  7. console.log(user.isAdmin);

当一个函数被使用 new 操作符执行时,它按照以下步骤:

  1. 一个新的空对象被创建并分配给 this
  2. 函数体执行。通常它会修改 this,为其添加新的属性。
  3. 返回 this 的值。

换句话说,new User(...) 做的就是类似的事情:

  1. function User(name) {
  2. // this = {}; (隐式创建)
  3. // 添加属性到 this
  4. this.name = name;
  5. this.isAdmin = false;
  6. // return this; (返回隐式)
  7. }

所以 new User("Jack") 的结果是相同的对象:

  1. let user = {
  2. name: "Jack",
  3. isAdmin: false
  4. };

现在,如果我们想创建其他用户,我们可以调用 new User("Ann")new User("Alice") 等。比每次都使用字面量创建要短得多,而且更易于阅读。

这是构造器的主要目的 —— 实现可重用的对象创建代码。

new function() { … }

如果我们有许多行用于创建单个复杂对象的代码,我们可以将它们封装在一个立即调用的构造函数中,像这样:

  1. let user = new function() {
  2. this.name = "John";
  3. this.isAdmin = false;
  4. // ……用于用户创建的其他代码
  5. // 也许是复杂的逻辑和语句
  6. // 局部变量等
  7. };

这个构造函数不能被再次调用,因为它不保存在任何地方,只是被创建和调用。因此,这个技巧旨在封装构建单个对象的代码,而无需将来重用。

构造器的 return

通常,构造器没有 return 语句。它们的任务是将所有必要的东西写入 this,并自动转换为结果。

但是,如果这有一个 return 语句,那么规则就简单了:

  • 如果 return 返回的是一个对象,则返回这个对象,而不是 this
  • 如果 return 返回的是一个原始类型,则忽略。

换句话说,带有对象的 return 返回该对象,在所有其他情况下返回 this

例如,这里 return 通过返回一个对象覆盖 this

  1. function BigUser() {
  2. this.name = "yellowsea";
  3. return {
  4. name. "John"
  5. }; // 返回这个对象
  6. }
  7. console.log(new BigUser().name); // John 得到的是对象
  1. function BigUser() {
  2. this.name = "yellowsea";
  3. return; // 返回 this
  4. }
  5. console.log(new BigUsr().name); // yellowsea

通常构造器没有 return 语句。这里我们主要为了完整性而提及返回对象的特殊行为。

省略括号

顺便说一下,如果没有参数,我们可以省略 new 后的括号:

  1. let user = new User; // <-- 没有参数
  2. // 等同于
  3. let user = new User();

这里省略括号不被认为是一种“好风格”,但是规范允许使用该语法。

构造器中的方法

使用构造函数来创建对象会带来很大的灵活性。构造函数可能有一些参数,这些参数定义了如何构造对象以及要放入什么。

当然,我们不仅可以将属性添加到 this 中,还可以添加方法。

  1. function User(name) {
  2. this.name = name;
  3. this.sayHi = function() {
  4. console.log("this name is :" + this.name);
  5. };
  6. }
  7. // 使用
  8. let john = new User("John");
  9. john.sayHi();
  10. /*
  11. john = {
  12. name: "John",
  13. sayHi: function() { ... }
  14. }
  15. */
  16. // 类 是用于创建复杂对象的一个更高级的语法, 我们稍后会讲到。

小结

  • 构造函数,或简称构造器,就是常规函数,但大家对于构造器有个共同的约定,就是其命名首字母要大写。
  • 构造函数只能使用 new 来调用。这样的调用意味着在开始时创建了空的 this,并在最后返回填充了值的 this

我们可以使用构造函数来创建多个类似的对象。

JavaScript 为许多内置的对象提供了构造函数:比如日期 Date、集合 Set 以及其他我们计划学习的内容。

  1. let obj = {};
  2. function A() {
  3. this.name = "John";
  4. return obj;
  5. }
  6. function B() {
  7. this.name = "John";
  8. return obj;
  9. }
  10. let a = new A();
  11. let b = new B();
  12. console.log(a == b); // true
  13. // 如果没有 return 返回到obj, a == b // false
  1. function Calculator() {
  2. this.read = function() {
  3. this.x = +prompt("x:", ''); // +prompt : str ==> number
  4. this.y = +prompt("y:", '');
  5. };
  6. this.sum = function() {
  7. return this.x + this.y;
  8. }
  9. this.mul = function() {
  10. return this.x * this.y;
  11. }
  12. }
  13. let calculator = new Calculator();
  14. calculator.read();
  15. alert("Sum=" + calculator.sum());
  16. alert("Mul=" + calculator.mul());

可选链 “?”

https://zh.javascript.info/optional-chaining

“不存在的属性”的问题

如果你才刚开始读此教程并学习 JavaScript,那可能还没接触到这个问题,但它却相当常见。

举个例子,假设我们有很多个 user 对象,其中存储了我们的用户数据。

我们大多数用户的地址都存储在 user.address 中,街道地址存储在 user.address.street 中,但有些用户没有提供这些信息。

在这种情况下,当我们尝试获取 user.address.street,而该用户恰好没提供地址信息,我们则会收到一个错误:

  1. let user = {}; // 一个没有 "address" 属性的 user 对象
  2. alert(user.address.street); // Error!

这是预期的结果。JavaScript 的工作原理就是这样的。因为 user.addressundefined,尝试读取 user.address.street 会失败,并收到一个错误。

但是在很多实际场景中,我们更希望得到的是 undefined(表示没有 street 属性)而不是一个错误。

……还有另一个例子。在 Web 开发中,我们可以使用特殊的方法调用(例如 document.querySelector('.elem'))以对象的形式获取一个网页元素,如果没有这种对象,则返回 null

  1. // 如果 document.querySelector('.elem') 的结果为 null,则这里不存在这个元素
  2. let html = document.querySelector('.elem').innerHTML; // 如果 document.querySelector('.elem') 的结果为 null,则会出现错误

同样,如果该元素不存在,则访问 null.innerHTML 时会出错。在某些情况下,当元素的缺失是没问题的时候,我们希望避免出现这种错误,而是接受 html = null 作为结果。

我们如何实现这一点呢?

可能最先想到的方案是在访问该值的属性之前,使用 if 或条件运算符 ? 对该值进行检查,像这样:

  1. let user = {};
  2. alert(user.address ? user.address.street : undefined);

这样可以,这里就不会出现错误了……但是不够优雅。就像你所看到的,"user.address" 在代码中出现了两次。对于嵌套层次更深的属性就会出现更多次这样的重复,这就是问题了。

例如,让我们尝试获取 user.address.street.name

我们既需要检查 user.address,又需要检查 user.address.street

  1. let user = {}; // user 没有 address 属性
  2. alert(user.address ? user.address.street ? user.address.street.name : null : null);

这样就太扯淡了,并且这可能导致写出来的代码很难让别人理解。

甚至我们可以先忽略这个问题,因为我们有一种更好的实现方式,就是使用 && 运算符:

  1. let user = {}; // user 没有 address 属性
  2. alert( user.address && user.address.street && user.address.street.name ); // undefined(不报错)

依次对整条路径上的属性使用与运算进行判断,以确保所有节点是存在的(如果不存在,则停止计算),但仍然不够优雅。

就像你所看到的,在代码中我们仍然重复写了好几遍对象属性名。例如在上面的代码中,user.address 被重复写了三遍。

这就是为什么可选链 ?. 被加入到了 JavaScript 这门编程语言中。那就是彻底地解决以上所有问题!

可选链

如果可选链 ?. 前面的部分是 undefined 或者 null,它会停止运算并返回该部分。

为了简明起见,在本文接下来的内容中,我们会说如果一个属性既不是 **null** 也不是 **undefined**,那么它就“存在”。

换句话说,例如 value?.prop

  • 如果 value 存在,则结果与 value.prop 相同,
  • 否则(当 valueundefined/null 时)则返回 undefined

下面这是一种使用 ?. 安全地访问 user.address.street 的方式:

  1. let user = {}; // user 没有 address 属性
  2. alert( user?.address?.street ); // undefined(不报错)

代码简洁明了,也不用重复写好几遍属性名。

即使 对象 user 不存在,使用 user?.address 来读取地址也没问题:

  1. let user = null;
  2. alert( user?.address ); // undefined
  3. alert( user?.address.street ); // undefined

请注意:?. 语法使其前面的值成为可选值,但不会对其后面的起作用。

例如,在 user?.address.street.name 中,?. 允许 usernull/undefined,但仅此而已。更深层次的属性是通过常规方式访问的。如果我们希望它们中的一些也是可选的,那么我们需要使用更多的 ?. 来替换 .

不要过度使用可选链

我们应该只将 ?. 使用在一些东西可以不存在的地方。

例如,如果根据我们的代码逻辑,user 对象必须存在,但 address 是可选的,那么我们应该这样写 user.address?.street,而不是这样 user?.address?.street

所以,如果 user 恰巧因为失误变为 undefined,我们会看到一个编程错误并修复它。否则,代码中的错误在不恰当的地方被消除了,这会导致调试更加困难。

**?.** 前的变量必须已声明

如果未声明变量 user,那么 user?.anything 会触发一个错误:

  1. // ReferenceError: user is not defined
  2. user?.address;

?. 前的变量必须已声明(例如 let/const/var user 或作为一个函数参数)。可选链仅适用于已声明的变量。

短路效应

正如前面所说的,如果 ?. 左边部分不存在,就会立即停止运算(“短路效应”)。

所以,如果后面有任何函数调用或者副作用,它们均不会执行。

例如:

  1. let user = null;
  2. let x = 0;
  3. user?.sayHi(x++); // 没有 "sayHi",因此代码执行没有触达 x++
  4. alert(x); // 0,值没有增加

其它变体:?.(),?.[]

可选链 ?. 不是一个运算符,而是一个特殊的语法结构。它还可以与函数和方括号一起使用。

例如,将 ?.() 用于调用一个可能不存在的函数。

在下面这段代码中,有些用户具有 admin 方法,而有些没有:

  1. let userAdmin = {
  2. admin() {
  3. alert("I am admin");
  4. }
  5. };
  6. let userGuest = {};
  7. userAdmin.admin?.(); // I am admin
  8. userGuest.admin?.(); // 啥都没有(没有这样的方法)

在这两行代码中,我们首先使用点符号(userAdmin.admin)来获取 admin 属性,因为用户对象一定存在,因此可以安全地读取它。

然后 ?.() 会检查它左边的部分:如果 admin 函数存在,那么就调用运行它(对于 userAdmin)。否则(对于 userGuest)运算停止,没有错误。

如果我们想使用方括号 [] 而不是点符号 . 来访问属性,语法 ?.[] 也可以使用。跟前面的例子类似,它允许从一个可能不存在的对象上安全地读取属性。

  1. let user1 = {
  2. firstName: "John"
  3. };
  4. let user2 = null; // 假设,我们不能授权此用户
  5. let key = "firstName";
  6. alert( user1?.[key] ); // John
  7. alert( user2?.[key] ); // undefined
  8. alert( user1?.[key]?.something?.not?.existing); // undefined

此外,我们还可以将 ?.delete 一起使用:

  1. delete user?.name; // 如果 user 存在,则删除 user.name

我们可以使用 **?.** 来安全地读取或删除,但不能写入

可选链 ?. 不能用在赋值语句的左侧。

例如:

  1. let user = null;
  2. user?.name = "John"; // Error,不起作用
  3. // 因为它在计算的是 undefined = "John"

这还不是那么智能。

总结

可选链 ?. 语法有三种形式:

  1. obj?.prop —— 如果 obj 存在则返回 obj.prop,否则返回 undefined
  2. obj?.[prop] —— 如果 obj 存在则返回 obj[prop],否则返回 undefined
  3. obj.method?.() —— 如果 obj.method 存在则调用 obj.method(),否则返回 undefined

正如我们所看到的,这些语法形式用起来都很简单直接。?. 检查左边部分是否为 null/undefined,如果不是则继续运算。

?. 链使我们能够安全地访问嵌套属性。

但是,我们应该谨慎地使用 ?.,仅在当左边部分不存在也没问题的情况下使用为宜。以保证在代码中有编程上的错误出现时,也不会对我们隐藏。

Symbol 类型

根据规范,对象的属性键只能是字符串类型或者 Symbol 类型。不是 Number,也不是 Boolean,只有字符串或 Symbol 这两种类型。

https://zh.javascript.info/symbol

Symblo

“Symbol” 值表示唯一的标识符。

可以使用 Symbol() 来创建这种类型的值:

  1. // id 是 symbol 的一个实例化对象
  2. let id = Symbol();

创建时,我们可以给 Symbol 一个描述(也称为 Symbol 名),这在代码调试时非常有用:

  1. // id 是描述为 "id" 的 Symbol
  2. let id = Symbol("id");

Symbol 保证是唯一的。即使我们创建了许多具有相同描述的 Symbol,它们的值也是不同。描述只是一个标签,不影响任何东西。

例如,这里有两个描述相同的 Symbol —— 它们不相等:

  1. let id1 = Symbol("id");
  2. let id2 = Symbol("id");
  3. alert(id1 == id2); // false

如果你熟悉 Ruby 或者其他有 “Symbol” 的语言 —— 别被误导。JavaScript 的 Symbol 是不同的。

Symbol 不会被自动转换为字符串

JavaScript 中的大多数值都支持字符串的隐式转换。例如,我们可以 alert 任何值,都可以生效。Symbol 比较特殊,它不会被自动转换。

例如,这个 alert 将会提示出错:

  1. let id = Symbol("id");
  2. alert(id); // 类型错误:无法将 Symbol 值转换为字符串。

这是一种防止混乱的“语言保护”,因为字符串和 Symbol 有本质上的不同,不应该意外地将它们转换成另一个。

如果我们真的想显示一个 Symbol,我们需要在它上面调用 .toString(),如下所示:

  1. let id = Symbol("id");
  2. alert(id.toString()); // Symbol(id),现在它有效了

或者获取 symbol.description 属性,只显示描述(description):

  1. let id = Symbol("id");
  2. alert(id.description); // id

隐藏属性

Symbol 允许我们创建对象的“隐藏”属性,代码的任何其他部分都不能意外访问或重写这些属性。

例如,如果我们使用的是属于第三方代码的 user 对象,我们想要给它们添加一些标识符。

我们可以给它们使用 Symbol 键:

  1. let user = { // 属于另一个代码
  2. name: "John"
  3. };
  4. let id = Symbol("id");
  5. user[id] = 1;
  6. alert( user[id] ); // 我们可以使用 Symbol 作为键来访问数据

因为 user 对象属于其他的代码,那些代码也会使用这个对象,所以我们不应该在它上面直接添加任何字段,这样很不安全。但是你添加的 Symbol 属性不会被意外访问到,第三方代码根本不会看到它,所以使用 Symbol 基本上不会有问题。

另外,假设另一个脚本希望在 user 中有自己的标识符,以实现自己的目的。这可能是另一个 JavaScript 库,因此脚本之间完全不了解彼此。

Symbol 在 for … in 中会被跳过

Symbol 属性不参与 for..in 循环。

  1. let id = Symbol("id");
  2. let user = {
  3. name: "John",
  4. age: 30,
  5. [id]: 123
  6. };
  7. for (let key in user) alert(key); // name, age (no symbols)
  8. // 使用 Symbol 任务直接访问
  9. alert( "Direct: " + user[id] );

小结

Symbol 是唯一标识符的基本类型

Symbol 是使用带有可选描述(name)的 Symbol() 调用创建的。

Symbol 总是不同的值,即使它们有相同的名字。如果我们希望同名的 Symbol 相等,那么我们应该使用全局注册表:Symbol.for(key) 返回(如果需要的话则创建)一个以 key 作为名字的全局 Symbol。使用 Symbol.for 多次调用 key 相同的 Symbol 时,返回的就是同一个 Symbol。

Symbol 有两个主要的使用场景:

  1. “隐藏” 对象属性。 如果我们想要向“属于”另一个脚本或者库的对象添加一个属性,我们可以创建一个 Symbol 并使用它作为属性的键。Symbol 属性不会出现在 for..in 中,因此它不会意外地被与其他属性一起处理。并且,它不会被直接访问,因为另一个脚本没有我们的 symbol。因此,该属性将受到保护,防止被意外使用或重写。
    因此我们可以使用 Symbol 属性“秘密地”将一些东西隐藏到我们需要的对象中,但其他地方看不到它。
  2. JavaScript 使用了许多系统 Symbol,这些 Symbol 可以作为 Symbol.* 访问。我们可以使用它们来改变一些内置行为。例如,在本教程的后面部分,我们将使用 Symbol.iterator 来进行 迭代 操作,使用 Symbol.toPrimitive 来设置 对象原始值的转换 等等。

从技术上说,Symbol 不是 100% 隐藏的。有一个内置方法 Object.getOwnPropertySymbols(obj) 允许我们获取所有的 Symbol。还有一个名为 Reflect.ownKeys(obj) 的方法可以返回一个对象的 所有 键,包括 Symbol。所以它们并不是真正的隐藏。但是大多数库、内置方法和语法结构都没有使用这些方法。

原始类型的方法

JavaScript 允许我们像使用对象一样使用原始类型(字符串,数字等)。JavaScript 还提供了这样的调用方法。我们很快就会学习它们,但是首先我们将了解它的工作原理,毕竟原始类型不是对象(在这里我们会分析地更加清楚)。

我们来看看原始类型和对象之间的关键区别。

一个原始值:

  • 是原始类型中的一种值。
  • 在 JavaScript 中有 7 种原始类型:stringnumberbigintbooleansymbolnullundefined

一个对象:

  • 能够存储多个值作为属性。
  • 可以使用大括号 {} 创建对象,例如:{name: "John", age: 30}。JavaScript 中还有其他种类的对象,例如函数就是对象。

关于对象的最好的事儿之一是,我们可以把一个函数作为对象的属性存储到对象中。

  1. let john = {
  2. name: "john",
  3. sayHi: function() console.log("Hi buddy");
  4. };
  5. john.sayHi();

当作对象的原始类型

而解决方案看起来多少有点尴尬,如下:

  1. 原始类型仍然是原始的。与预期相同,提供单个值
  2. JavaScript 允许访问字符串,数字,布尔值和 symbol 的方法和属性。
  3. 为了使它们起作用,创建了提供额外功能的特殊“对象包装器”,使用后即被销毁。

“对象包装器”对于每种原始类型都是不同的,它们被称为 StringNumberBooleanSymbol。因此,它们提供了不同的方法。

  1. // 例如 字符串方法 str.toUpperCase() 返回一个大写化处理的字符串
  2. let str = "Hello";
  3. console.log(str.toUpperCase());

很简单,对吧?以下是 str.toUpperCase() 中实际发生的情况:

  1. 字符串 str 是一个原始值。因此,在访问其属性时,会创建一个包含字符串字面值的特殊对象,并且具有有用的方法,例如 toUpperCase()
  2. 该方法运行并返回一个新的字符串(由 alert 显示)。
  3. 特殊对象被销毁,只留下原始值 str

所以原始类型可以提供方法,但它们依然是轻量级的。

JavaScript 引擎高度优化了这个过程。它甚至可能跳过创建额外的对象。但是它仍然必须遵守规范,并且表现得好像它创建了一样。

  1. // 2. toFixed(n) 将数字舍入到给定的精度
  2. let n = 1.234123;
  3. console.log(n.toFixed(2)); // 保留几位小数

构造器 **String/Number/Boolean** 仅供内部使用

像 Java 这样的一些语言允许我们使用 new Number(1)new Boolean(false) 等语法,明确地为原始类型创建“对象包装器”。

在 JavaScript 中,由于历史原因,这也是可以的,但极其 不推荐。因为这样会出问题。

例如:

  1. alert( typeof 0 ); // "number"
  2. alert( typeof new Number(0) ); // "object"!

对象在 if 中始终为真,因此此处的 alert 将显示:

  1. let zero = new Number(0);
  2. if (zero) { // zero 为 true,因为它是一个对象
  3. alert( "zero is truthy?!?" );
  4. }

另一方面,调用不带 new(关键字)的 String/Number/Boolean 函数是完全理智和有用的。它们将一个值转换为相应的类型:转成字符串、数字或布尔值(原始类型)。

例如,下面完全是有效的:

  1. let num = Number("123"); // 将字符串转成数字

null/undefined 没有任何方法

特殊的原始类型 nullundefined 是例外。它们没有对应的“对象包装器”,也没有提供任何方法。从某种意义上说,它们是“最原始的”。

尝试访问这种值的属性会导致错误:

  1. alert(null.test); // error
  • nullundefined 以外的原始类型都提供了许多有用的方法。我们后面的章节中学习这些内容。
  • 从形式上讲,这些方法通过临时对象工作,但 JavaScript 引擎可以很好地调整,以在内部对其进行优化,因此调用它们并不需要太高的成本。

试试运行一下:

  1. let str = "Hello";
  2. str.test = 5; // (*)
  3. alert(str.test);

根据你是否开启了严格模式 use strict,会得到如下结果:

  1. undefined(非严格模式)
  2. 报错(严格模式)。

为什么?让我们看看在 (*) 那一行到底发生了什么:

  1. 当访问 str 的属性时,一个“对象包装器”被创建了。
  2. 在严格模式下,向其写入内容会报错。
  3. 否则,将继续执行带有属性的操作,该对象将获得 test 属性,但是此后,“对象包装器”将消失,因此在最后一行,str 并没有该属性的踪迹。