前言
最近在学习小程序的开发,看到教程里面的写的 JavaScript 有箭头函数,还有 Promise、async/await 这些内容完全是陌生的,因此搜索了一下,发现了现代 JavaScript 教程,网址:https://zh.javascript.info/,看了一下简介:
现代 JavaScript 教程(The Modern JavaScript Tutorial),以最新的 ECMAScript 规范为基准,通过简单但足够详细的内容,为你讲解从基础到高阶的 JavaScript 相关知识。
这个简介看起来很符合我的学习需求,看了几篇文章觉得确实不错,因此决定记个笔记,有所输出,也不枉我在紧急的小程序开发学习中抽出时间单独学习 JS。
对于 JavaScript 的学习我也就是大学时看过几本书,日常网页开发也会用到,但会极力避免使用,因为不熟,都是东拼西凑、复制粘贴修改的水平,时隔多年,再一次静下心来学习现代 JavaScript。
这篇文章仅记录我个人的笔记,每个人情况不一样,如果你是要系统的学习,请直接看原始教程,这篇笔记是该教程第一部分:JavaScript 编程语言 的学习笔记。
简介
为什么叫 JavaScript?
JavaScript 在刚诞生的时候,它的名字叫 “LiveScript”。但是因为当时 Java 很流行,所以决定将一种新语言定位为 Java 的“弟弟”会有助于它的流行。
浏览器中的 JavaScript 不能做什么?
- 文件操作限制: JavaScript 不能直接读写用户硬盘上的文件。 只有在用户授权的情况下(如拖放文件到浏览器),JavaScript 才能进行有限的文件操作。
- 设备交互限制: 与设备(如相机和麦克风)的交互需要用户明确许可。
- 窗口间通信限制: 不同标签页或窗口间的 JavaScript 通常不能相互通信,除非它们是同源的。 同源策略要求两个标签页都包含特定的 JavaScript 代码来允许数据交换。
- 网络通信限制: JavaScript 可以与同源服务器通信,但从其他源接收数据需要远程服务器的明确协议。
如果在浏览器环境外(例如在服务器上)使用 JavaScript,则不存在此类限制。现代浏览器还允许安装可能会要求扩展权限的插件/扩展。
JavaScript “上层”语言
- CoffeeScript 是 JavaScript 的一种语法糖。它引入了更加简短的语法,使我们可以编写更清晰简洁的代码。通常,Ruby 开发者喜欢它。
- TypeScript 专注于添加“严格的数据类型”以简化开发,以更好地支持复杂系统的开发。由微软开发。
- Flow 也添加了数据类型,但是以一种不同的方式。由 Facebook 开发。
- Dart 是一门独立的语言。它拥有自己的引擎,该引擎可以在非浏览器环境中运行(例如手机应用),它也可以被编译成 JavaScript。由 Google 开发。
- Brython 是一个 Python 到 JavaScript 的转译器,让我们可以在不使用 JavaScript 的情况下,以纯 Python 编写应用程序。
- Kotlin 是一个现代、简洁且安全的编程语言,编写出的应用程序可以在浏览器和 Node 环境中运行。
JavaScript 基础知识
现代模式,“use strict”
“use strict” 是一个特殊指令,用于激活 ECMAScript 5 (ES5) 中的新特性和修改。它必须放在脚本或函数体的最顶部。
在脚本顶部添加 “use strict”; 以启用严格模式。也可以在函数体的开头添加,仅在该函数中启用严格模式。
“use strict” 不能被取消,一旦启用就无法返回默认模式。只有注释可以出现在 “use strict” 的上面。
严格模式修正了 JavaScript 语言中的一些错误和不完善的决定。它提供了更好的错误检查,防止了一些潜在的错误。
默认情况下,浏览器控制台不启用严格模式。可以通过多行输入或将代码放在一个立即执行的函数表达式中来启用。
现代 JavaScript 的 “class” 和 “module” 结构自动启用严格模式。使用这些结构时,无需手动添加 “use strict” 指令。
let
和 var
的区别
在现代 JavaScript 中使用 let
关键字创建变量,var
关键字是老旧的写法。
用 var
声明的变量没有块级作用域,它们仅在当前函数内可见,或者全局可见(如果变量是在函数外声明的),let
声明的变量作用域是代码块级别的。
使用 var
,我们可以重复声明一个变量,不管多少次都行;用 let
在同一作用域下将同一个变量声明两次,则会出现错误。
var
声明的变量,可以在其声明语句前被使用,let
声明的变量必须先声明再使用。
数据类型
JavaScript 中有八种基本的数据类型:
- 七种原始数据类型:
Number
用于任何类型的数字:整数或浮点数,在±(2^53-1)
范围内的整数。BigInt
用于任意长度的整数,通过将 n 附加到整数字段的末尾来创建 BigInt 值:const bigInt = 12345n
。String
用于字符串,单引号和双引号相同,反引号中可以通过${变量名}
将变量值嵌入到字符串中。Boolean
用于 true 和 false。null
用于未知的值 —— 只有一个null
值的独立类型。undefined
用于未定义的值 —— 只有一个undefined
值的独立类型。Symbol
用于唯一的标识符。
- 以及一种非原始数据类型:
Object
用于更复杂的数据结构。
我们可以通过 typeof
运算符查看存储在变量中的数据类型。
- 通常用作
typeof x
,但typeof(x)
也可行。 - 以字符串的形式返回类型名称,例如 “string”。
typeof null
会返回"object"
—— 这是 JavaScript 编程语言的一个错误,实际上它并不是一个object
。
JavaScript 中的二元运算符 +
字符串连接:
- 加号
+
通常用于数字的相加。 - 当 加号
+
应用于字符串时,它会将字符串连接起来。 - 例如:
let s = "my" + "string";
会得到"mystring"
。
字符串与数字的连接:
- 如果运算元中有一个是字符串,那么另一个运算元也会被转换为字符串。
- 例如:
alert( '1' + 2 );
输出"12"
。alert( 2 + '1' );
输出"21"
。
运算顺序:
- 运算符按顺序执行。
- 例如:
alert(2 + 2 + '1' );
输出"41"
,而不是"221"
。alert('1' + 2 + 2);
输出"122"
,而不是"14"
。
其他算术运算符:
- 除了二元
+
,其他算术运算符(如-
、/
)只对数字有效,并且会将其运算元转换为数字。 - 例如:
alert( 6 - '2' );
输出4
。alert( '6' / '2' );
输出3
。
数字转化,一元运算符 +
- 一元运算符
+
对数字没有影响。 - 如果运算元不是数字,一元运算符
+
会将其转化为数字。 - 例如:
alert( +true );
输出1
。alert( +"" );
输出0
。
字符串转化为数字
- 通常,从 HTML 表单获取的值是字符串,如果需要求和,必须先转换为数字。
- 二元
+
会将字符串合并,而不是求和。 - 通过在每个字符串前加上一元
+
,可以先将字符串转换为数字,然后再求和。 - 例如:
let apples = "2";
let oranges = "3";
alert( +apples + +oranges );
输出5
。
JavaScript 中的相等性检查
非严格相等 (==
)
- 非严格相等 (
==
) 会在比较前将值转换为相同类型。 0 == false
和'' == false
都返回true
,因为它们都被转换为数字0
。不同类型的值在比较时会先转换为数字(除了严格相等检查)。
严格相等 (===
)
- 严格相等 (
===
) 不会进行类型转换,如果两个值的类型不同,则直接返回false
。 0 === false
返回false
,因为它们类型不同。
对 null
和 undefined
的比较
- 使用 严格相等 (
===
) 比较时,null
和undefined
不相等。 - 使用 非严格相等 (
==
) 比较时,null
和undefined
被视为相等。 null
和undefined
在数学比较 (<
,>
,<=
,>=
) 中分别被转换为0
和NaN
。
特殊情况:null
vs 0
alert(null > 0); // (1) false
alert(null == 0); // (2) false
alert(null >= 0); // (3) true
相等性检查 ==
和普通比较符 >
<
>=
<=
的代码逻辑是相互独立的。
进行值的比较时,null 会被转化为数字,因此它被转化为了 0。这就是为什么(3)中 null >= 0 返回值是 true,(1)中 null > 0 返回值是 false。
另一方面,undefined 和 null 在相等性检查 ==
中不会进行任何的类型转换,它们有自己独立的比较规则,所以除了它们之间互等外,不会等于任何其他的值。
特立独行的 undefined
undefined
不应与其他值进行比较,因为它在数学比较中被转换为NaN
,并且在非严格相等 (==
) 下只与null
相等。
避免问题的建议
- 除了使用 严格相等 (
===
) 外,避免使用undefined
或null
进行比较。 - 不要使用
>=
,>
,<
,<=
去比较可能为null
或undefined
的变量,而是单独检查它们的值。
JS 的逻辑运算符
JavaScript 中有四个逻辑运算符:||
(或),&&
(与),!
(非),??
(空值合并运算符)。
使用或运算符 ||
寻找第一个真值:
alert("" || null || 123 || "abc"); // 弹出 123(因为 123 是第一个真值)
||
会从左到右依次评估每个操作数,每个操作数都会被转换为布尔值,如果转换结果是 true
,则停止计算并返回该操作数的原始值。如果所有操作数都被评估过(即都为 false),则返回最后一个操作数。
||
运算符返回链中的第一个真值,或者如果没有真值,则返回最后一个值。
使用与运算符 &&
寻找第一个假值:
alert(1 && 2 && null && 3); // 弹出 null(因为 null 是第一个假值)
&&
从左到右依次评估每个操作数。每个操作数都会被转换为布尔值。如果转换结果是 false,则停止计算并返回该操作数的原始值。如果所有操作数都被评估过(即都为真值),则返回最后一个操作数。
&&
运算符返回链中的第一个假值,或者如果没有假值,则返回最后一个值。
JS 的空值合并运算符(??)
空值合并运算符 ?? 用于选择两个值中的第一个“已定义的”值。其语法为 a ?? b
。它提供了一种简洁的方式来处理可能为 null
或 undefined
的值。
如果 a
是已定义的(即不是 null
或 undefined
),则结果为 a
;否则,结果为 b
。
运算符 ?? 的常见用途是为变量提供默认值。例如,user ?? "匿名"
会在 user
未定义时显示“匿名”。
?? 可以用来从一系列值中选择第一个非 null/undefined
的值,如 firstName ?? lastName ?? nickName ?? "匿名"
。
与 || 的比较:||
返回第一个真值,而 ??
返回第一个已定义的值。这意味着 ||
无法区分 false
、0
、空字符串 ""
和 null/undefined
,而 ??
只在值为 null/undefined
时考虑第二个参数。
例如,height || 100
会在 height
为 0
时返回 100
,而 height ?? 100
会返回 0
,因为 0
是一个有效值。
JavaScript switch 语句用法
switch
语句会评估表达式并将其与每个 case
后的值进行严格比较(使用 ===
)。
如果找到匹配的 case
,则执行该 case
下的代码,直到遇到 break
语句或 switch
语句的末尾。
如果没有匹配的 case
,则执行 default
代码块(如果有的话)。
无 break
的情况:如果 case
代码块中没有 break
语句,控制流将继续执行下一个 case
代码块,这可能会导致多个 case
代码块被执行。
任意表达式:switch
和 case
都可以使用任意表达式。
case
分组:可以将多个 case
分支组合在一起,以共享相同的代码块。
类型匹配:switch
语句中的比较是严格的,这意味着值和类型都必须匹配。
以下是一个 switch
语句的示例:
let arg = prompt("Enter a value?");
switch (arg) {
case "0": // 这两个 case 被分在一组
case "1":
alert("One or zero");
break;
case "2":
alert("Two");
break;
case 3:
alert("Never executes!"); // 因为 '3'(字符串)不等于 3(数字)
break;
default:
alert("An unknown value");
}
JavaScript 箭头函数用法
箭头函数是 JavaScript 中的一个功能,它提供了一种更简洁的方式来编写函数表达式。这种函数主要有两种形式:
- 不带花括号的箭头函数:
(...args) => expression
,这种形式适用于单个表达式,自动返回该表达式的结果。例如,n => n*2
是一个将输入值加倍的箭头函数。 - 带花括号的箭头函数:
(...args) => { body }
,这种形式允许包含多条语句,但需要使用return
语句来返回结果。
JS 代码质量
尽量避免代码嵌套层级过深: 为了提高代码的可读性和简洁性,应当尽量减少嵌套层级,例如通过使用 continue
指令或在条件检查后立即返回,从而避免不必要的嵌套。
函数位置: 在编程中,通常推荐先写出主要逻辑代码,然后再声明辅助函数,以便首先展示程序的核心功能,从而提高代码的可读性。
一些受欢迎的代码风格指南:
一些最出名的代码检查工具:
如何写注释
糟糕的注释:
- 初学者常常错误地使用注释来解释代码的每个动作,这通常是不必要的。
- 代码应该足够清晰,不需要过多的解释性注释。如果代码需要注释才能理解,可能需要重写。
好的编程实践:
- 分解函数:将复杂的代码片段替换为函数,使代码自描述,提高可读性。
- 创建函数:将长代码块重构为函数,使代码结构更清晰,减少对注释的需求。
好的注释:
- 描述代码的高层架构和组件如何相互作用。
- 使用JSDoc等工具记录函数的参数和用法,帮助理解函数的目的和正确使用方法。
- 解释为什么选择了特定的解决方案,尤其是当它不是最明显的选择时。
注释的正确使用:
- 注释应该提供整体架构的高层次视图。
- 注释函数的用法,特别是参数和返回值。
- 注释重要的解决方案,特别是当它们不是显而易见的时候。
避免的注释:
- 避免注释描述代码的操作细节,如果代码已经足够清晰。
- 避免在代码已经自描述的情况下添加不必要的注释。
JavaScript 自动化测试库
- Mocha —— ☕️ simple, flexible, fun javascript test framework for node.js & the browser
- Chai —— BDD / TDD assertion framework for node.js and the browser that can be paired with any testing framework.
- Sinon —— Test spies, stubs and mocks for JavaScript.
这些库都既适用于浏览器端,也适用于服务器端。
JavaScript 的转译器和垫片用法
使转译器和垫片的作用可以使得开发者能够使用最新的 JavaScript 特性,同时保证代码在旧版浏览器中也能正常运行。
转译器(Transpilers) 是一种工具,它能够将使用最新语法编写的代码转换成旧版 JavaScript 引擎能够理解的代码。例如,Babel 就是一个流行的转译器,它可以将 ES2020 中的新特性转换成旧版浏览器能够执行的代码。
垫片(Polyfills) 是用来实现旧版浏览器中不存在的新的内建函数和方法的脚本。例如,如果旧浏览器不支持 Math.trunc
函数,我们可以通过 polyfill 来添加这个函数的实现,从而使得旧浏览器也能使用这个新特性。
if (!Math.trunc) {
// 如果没有这个函数
// 实现它
Math.trunc = function (number) {
// Math.ceil 和 Math.floor 甚至存在于上古年代的 JavaScript 引擎中
// 在本教程的后续章节中会讲到它们
return number < 0 ? Math.ceil(number) : Math.floor(number);
};
}
两个有趣的 polyfill 库:
- core js 支持了很多特性,允许只包含需要的特性。core-js 的作者是一位彪悍的俄罗斯程序员,名字叫丹尼斯·普什卡列夫(Denis Pushkarev),平时爱好就是飙摩托车。并在一次事故中,他以 60 km/h 的速度驾驶,结果撞了两个行人,一人现场死亡。根据俄罗斯联邦法律,他被判处有期徒刑 18 个月,剥夺 2 年驾驶权利,另处以罚金 138 万卢布。
- polyfill.io Polyfill.io 通过分析请求头信息中的 UserAgent 实现自动加载浏览器所需的 polyfill。
JavaScript Object(对象):基础知识
可以使用 new Object()
或 {}
(字面量语法)来创建一个新对象。
对象由属性组成,每个属性都有一个键(通常是字符串,属性键名会自动转换为字符串类型,且命名没有限制可以和保留的关键字相同)和一个值(可以是任何数据类型)。方法是存储为对象属性的函数。
可以使用点符号(.
)或方括号([]
)来访问对象的属性。如果属性名包含特殊字符或空格,或者是动态确定的,必须使用方括号。
let user = {};
// 设置
user["likes birds"] = true;
// 读取
alert(user["likes birds"]); // true
// 删除
delete user["likes birds"];
检查属性是否存在的操作符 “in”:
let user = {
name: "John",
age: 30,
// 方法简写:与 "sayHi: function(){...}" 一样
sayHi() {
alert("Hi");
},
};
alert("age" in user); // true,user.age 存在
alert("blabla" in user); // false,user.blabla 不存在。
对象中属性键名跟属性值的变量名相同时,可以缩写:
let user = {
name, // 与 name:name 相同
age: userAge,
};
计算属性:
当创建一个对象时,在对象字面量中使用方括号可以动态从方括号中的变量取值作为键名。
let fruit = prompt("Which fruit to buy?", "apple");
let bag = {
[fruit]: 5, // 属性名是从 fruit 变量中得到的
};
alert(bag.apple); // 5 如果 fruit="apple"
可以在方括号中使用更复杂的表达式:
let fruit = "apple";
let bag = {
[fruit + "Computers"]: 5, // bag.appleComputers = 5
};
遍历对象的属性:
可以使用 for..in
循环遍历访问对象中的所有键(key)和它们对应的值(value):
let codes = {
49: "Germany",
41: "Switzerland",
44: "Great Britain",
// ..,
1: "USA",
};
for (let code in codes) {
alert(code); // 1, 41, 44, 49
alert(codes[code]); // USA, Switzerland, Great Britain, Germany
}
属性顺序:对象属性的遍历顺序有特定的规则。整数键会自动按照升序排序,而其他键则按照它们被添加到对象中的顺序遍历。
整数属性:如果一个属性名可以无更改地转换为一个整数,那么它就被认为是一个整数属性。例如,“49” 是一个整数属性,但 “1.2” 和 “+49” 不是。
非整数属性的顺序:非整数属性会按照它们在对象中创建的顺序进行排序。
解决排序问题:如果我们希望属性按照特定的顺序遍历,可以通过将属性名转换为非整数属性来实现。例如,给电话号码添加 “+” 前缀,使其成为非整数属性,从而保持了添加到对象中的顺序。
JS 对象引用和复制
赋值了对象的变量存储的不是对象本身,而是该对象“在内存中的地址” —— 换句话说就是对该对象的“引用”。当一个对象变量被复制 —— 引用被复制,而该对象自身并没有被复制。
let user = { name: "John" };
let admin = user; // 复制引用
admin.name = "Pete"; // 通过 "admin" 引用来修改
alert(user.name); // 'Pete',修改能通过 "user" 引用看到
这里仍然只有一个对象,但现在有两个引用它的变量:user 和 admin,可以通过其中任意一个变量来访问该对象并修改它的内容。
可以使用 Object.assign(dest, [src1, src2, src3...])
方法来克隆和合并对象:
let user = {
name: "John",
age: 30,
};
let clone = Object.assign({}, user); // 将 user 中的所有属性拷贝到了一个空对象中,并返回这个新的对象
let permissions1 = { canView: true };
let permissions2 = { canEdit: true };
// 将 permissions1 和 permissions2 中的所有属性都拷贝到 user 中
Object.assign(clone, permissions1, permissions2);
// 现在 clone = { name: "John", age: 30, canView: true, canEdit: true }
// user还是原来的user = {name: 'John', age: 30}
深层克隆:
如果对象属性不是原始数据类型,而是对其他对象的引用,则需要进行深层克隆,否则它会以引用形式被拷贝,导致克隆的对象并不是完全真正独立的两个对象。
let user = {
name: "John",
sizes: {
height: 182,
width: 50,
},
};
let clone = Object.assign({}, user);
alert(user.sizes === clone.sizes); // true,同一个对象
// user 和 clone 分享同一个 sizes
user.sizes.width++; // 通过其中一个改变属性值
alert(clone.sizes.width); // 51,能从另外一个获取到变更后的结果
深层克隆实现:
function deepClone(obj) {
if (typeof obj !== "object" || obj === null) {
// 如果不是对象或者是 null,直接返回该值
return obj;
}
// 创建一个数组或对象来保持复制的值
let clone;
if (Array.isArray(obj)) {
clone = [];
} else {
clone = {};
}
for (let key in obj) {
// 保证 key 不是原型的属性
if (obj.hasOwnProperty(key)) {
// 递归复制每个属性
clone[key] = deepClone(obj[key]);
}
}
return clone;
}
// 使用示例
let user = {
name: "John",
sizes: {
height: 182,
width: 50,
},
};
let clone = deepClone(user);
console.log(clone.sizes); // 原始clone.sizes = {height: 182, width: 50}
console.log(user.sizes); // 原始user.sizes = {height: 182, width: 50}
user.sizes.width++; // 改变user.sizes属性值
console.log(clone.sizes); // clone.sizes未改变 {height: 182, width: 50}
console.log(user.sizes); // 只有user.sizes被修改 {height: 182, width: 51}
JavaScript 的垃圾回收机制
垃圾回收的两种主要方法是:
- 标记清除(Mark-and-Sweep):这是现代 JavaScript 引擎中最常用的垃圾回收策略。垃圾回收器会定期从根对象(如全局对象)开始,标记所有从根开始可达的对象。不可达的对象(即不再被引用的对象)被认为是垃圾,并将被清除。
- 引用计数:这是一种较早的垃圾回收策略,它追踪每个对象的引用数量。当一个对象的引用数量变为零时,意味着没有任何其他对象引用它,这个对象就可以被回收。然而,引用计数方法无法处理循环引用的情况。
现代 JavaScript 引擎,如 V8,使用了更复杂的垃圾回收策略,包括增量标记、延迟清除和分代回收,以优化性能并减少对程序执行的影响。
JS this
关键字用法
在 JavaScript 中,this
关键字的行为与其他编程语言中的不同。this
的值由调用时的上下文决定,而非定义时的位置,它不固定于任何特定的对象。
函数上下文中的 this
:
- 当函数作为对象的方法被调用时,
this
指向那个对象。 - 如果函数独立调用(不作为任何对象的方法),
this
的值取决于是否在严格模式下运行:- 在严格模式下,
this
是undefined
。 - 在非严格模式下,
this
指向全局对象(浏览器中是window
)。
- 在严格模式下,
this
的灵活性: this
的动态性允许函数在不同对象间共享,增加了代码的复用性。但这也意味着需要更小心地管理 this
,以避免错误和不期望的行为。
let user = { name: "John" };
let admin = { name: "Admin" };
function sayHi() {
alert(this.name);
}
// 在两个对象中使用相同的函数
user.f = sayHi;
admin.f = sayHi;
// 这两个调用有不同的 this 值
// 函数内部的 "this" 是“点符号前面”的那个对象
user.f(); // John(this == user)
admin.f(); // Admin(this == admin)
admin["f"](); // Admin(使用点符号或方括号语法来访问这个方法,都没有关系。)
箭头函数没有自己的 “this”,如果我们在箭头函数中引用 this,this 值取决于外部“正常的”函数,在箭头函数内部访问到的 this 都是从外部获取的。
JavaScript 构造器和操作符 “new"用法
构造函数在技术上是常规函数。不过有两个约定:
- 它们的命名以大写字母开头。
- 它们只能由 “new” 操作符来执行。
从技术上讲,任何函数(除了箭头函数,它没有自己的 this)都可以用作构造器。即可以通过 new 来运行,它会执行上面的算法。“首字母大写”是一个共同的约定,以明确表示一个函数将被使用 new 来运行。
当一个函数被使用 new 操作符执行时,它按照以下步骤:
function User(name) {
// this = {};(隐式创建),一个新的空对象被创建并分配给 this。
// 函数体执行。通常它会修改 this,为其添加新的属性。
this.name = name;
this.isAdmin = false;
// return this;(隐式返回),返回 this 的值。
}
如果构造器有明确的 return 语句返回一个对象,则构造器返回的结果是那个对象。 如果 return 返回原始类型或没有 return,则结果是 this。
构造器如果没有参数,使用时,我们可以省略 new 后的括号:
let user = new User(); // <-- 没有参数
// 等同于
let user = new User();
所以 new User("Jack")
的结果与下面 user 对象相同:
let user = {
name: "Jack",
isAdmin: false
};
在一个函数内部,我们可以使用 new.target
属性来检查它是否被使用 new 进行调用了。
function User() {
alert(new.target);
}
// 不带 "new":
User(); // undefined
// 带 "new":
new User(); // function User { ... }
JavaScrript 可选链 ?.
用法
可选链 ?.
是 JavaScript 的一个现代特性,它允许开发者安全地从可能不存在的嵌套对象中读取属性。如果链中的某个部分是 null
或 undefined
,表达式的求值会停止并返回 undefined
,而不是抛出错误。
基本用法:
user?.address?.street
:如果user
或user.address
为null/undefined
,则不会尝试读取street
,表达式返回undefined
。document.querySelector('.elem')?.innerHTML
:如果.elem
元素不存在,返回undefined
而不是抛出错误。
注意事项:
- 只在对象的某些属性可能不存在时使用
?.
,不要滥用它。 ?.
应用于链的当前位置,不影响后续属性的访问方式。
其他变体:
?.()
:用于调用可能不存在的方法。?.[]
:用于安全地访问可能不存在的属性。delete user?.name
:如果user
存在,则安全地删除user.name
。
限制:?.
不能用于赋值语句的左侧,因为它不能确定是否应该创建属性路径。
let user = null;
user?.name = "John"; // Error,不起作用
// 因为它在计算的是:undefined = "John"
JS 中的 symbol 类型
唯一性:每个 Symbol
都是唯一的,即使它们有相同的描述也不会相等。
let id1 = Symbol("id");
let id2 = Symbol("id");
alert(id1 == id2); // false
不可转换为字符串:Symbol
类型不能被自动转换为字符串,这可以防止意外的类型转换。
let id = Symbol("id");
alert(id); // TypeError: Cannot convert a Symbol value to a string
显示 Symbol:要显示一个 Symbol
,可以调用 .toString()
方法或访问 .description
属性。
let id = Symbol("id");
alert(id.toString()); // Symbol(id)
alert(id.description); // id
对象的隐藏属性:Symbol
可以用作对象属性的键,这些属性不会出现在常规的遍历中,如 for..in
循环或 Object.keys()
方法遍历到。
let id = Symbol("id");
let user = { name: "John", [id]: 123 };
for (let key in user) alert(key); // name
alert("Direct: " + user[id]); // Direct: 123
Object.assign:Object.assign()
方法会复制包含 Symbol
的属性。
let id = Symbol("id");
let user = { [id]: 123 };
let clone = Object.assign({}, user);
alert(clone[id]); // 123
全局 Symbol 注册表:Symbol.for(key)
方法可以从全局注册表中读取(或创建)Symbol
,确保相同名称的 Symbol
是相同的实例。
let id = Symbol.for("id");
let idAgain = Symbol.for("id");
alert(id === idAgain); // true
Symbol.keyFor:Symbol.keyFor(sym)
方法可以通过全局 Symbol
获取其名称,但它不适用于非全局 Symbol
。
let sym = Symbol.for("name");
alert(Symbol.keyFor(sym)); // name
JS 对象到原始值的转换
在 JavaScript 中,对象到原始值的转换可以通过三个不同的方法来控制:
Symbol.toPrimitive 方法:这是一个内建的 symbol 方法,用于对象到原始值的转换。如果一个对象有这个方法,它会被用于所有转换类型(“string”、“number” 或 “default”)。
let user = {
name: "John",
money: 1000,
[Symbol.toPrimitive](hint) {
alert(`hint: ${hint}`);
return hint == "string" ? `{name: "${this.name}"}` : this.money;
},
};
// 转换演示:
alert(user); // hint: string -> {name: "John"}
alert(+user); // hint: number -> 1000
alert(user + 500); // hint: default -> 1500
toString 和 valueOf 方法:如果没有 Symbol.toPrimitive 方法,JavaScript 会尝试调用 toString 和 valueOf 方法。对于 “string” 类型的 hint,会优先调用 toString 方法;对于 “number” 或 “default” 类型的 hint,会优先调用 valueOf 方法。
let user = {
name: "John",
money: 1000,
// 对于 hint="string"
toString() {
return `{name: "${this.name}"}`;
},
// 对于 hint="number" 或 "default"
valueOf() {
return this.money;
},
};
alert(user); // toString -> {name: "John"}
alert(+user); // valueOf -> 1000
alert(user + 500); // valueOf -> 1500
默认方法:如果一个对象没有 Symbol.toPrimitive、toString 或 valueOf 方法,它会默认使用 Object 的 toString 方法,返回 “[object Object]” 字符串。
JavaScript 的数据类型
number
一些不常见的用法:
// 这里的下划线 _ 扮演了“语法糖”的角色,使得数字具有更强的可读性。
// JavaScript 引擎会直接忽略数字之间的 _
let billion = 1_000_000_000;
// 方法 num.toString(base) 返回在给定 base 进制数字系统中 num 的字符串表示形式。
let num = 255;
alert(num.toString(16)); // ff
alert(num.toString(2)); // 11111111
// 如果我们想直接在一个数字上调用一个方法,
// 比如上面例子中的 toString,那么我们需要在它后面放置两个点 ..
// 如果我们放置一个点:123456.toString(36),那么就会出现一个 error,
// 因为 JavaScript 语法隐含了第一个点之后的部分为小数部分。
// 如果我们再放一个点,那么 JavaScript 就知道小数部分为空,现在使用该方法。
alert((123456).toString(36)); // 2n9c
// 也可以写成 (123456).toString(36)。
alert((123456).toString(36)); // 2n9c
isNaN、isFinite
// isNaN(value) 将其参数转换为数字,然后测试它是否为 NaN,NaN 代表一个 error:
alert(isNaN(NaN)); // true
alert(isNaN("str")); // true
// isFinite(value) 将其参数转换为数字,如果是常规数字而不是 NaN/Infinity/-Infinity,则返回 true:
alert(isFinite("15")); // true
alert(isFinite("str")); // false,因为是一个特殊的值:NaN
alert(isFinite(Infinity)); // false,因为是一个特殊的值:Infinity
Object.is
有一个特殊的内建方法 Object.is,它类似于 ===
一样对值进行比较,但它对于两种边缘情况更可靠:
- 它适用于 NaN:
Object.is(NaN, NaN) === true
,这是件好事。 - 值 0 和 -0 是不同的:
Object.is(0, -0) === false
,从技术上讲这是对的,因为在内部,数字的符号位可能会不同,即使其他所有位均为零。
parseInt
和 parseFloat
在 JavaScript 中,parseInt
和 parseFloat
函数用于从字符串中提取数字,直到遇到无法识别为数字的字符。
console.log(parseInt("100px")); // 输出: 100
console.log(parseFloat("12.5em")); // 输出: 12.5
console.log(parseInt("12.3")); // 输出: 12
console.log(parseFloat("12.3.4")); // 输出: 12.3
console.log(parseInt("a123")); // 输出: NaN
console.log(parseInt("0xff", 16)); // 输出: 255
console.log(parseInt("ff", 16)); // 输出: 255
console.log(parseInt("2n9c", 36)); // 输出: 123456
这些函数在处理带有单位的数值字符串(如 CSS 中的 “100px”)或货币符号(如 “19€")时特别有用。它们允许我们从这些字符串中提取出数字部分,以便在计算和其他操作中使用。在使用 parseInt
时,如果需要解析特定进制的数字,不要忘记提供 radix
参数。这样可以确保正确解析字符串并获得预期的数字值。
使用加号 +
或 Number()
的数字转换是严格的。如果一个值不完全是一个数字,就会失败:
alert(+"100px"); // NaN
JS 数组
使用 “at” 获取最后一个数组元素:
let fruits = ["Apple", "Orange", "Plum"];
// 与 fruits[fruits.length-1] 相同
alert(fruits.at(-1)); // Plum
JavaScript 的数组可以看作一个双端队列,同时作为栈和队列使用,数组的 pop
/push
和 shift
/unshift
方法允许我们以不同的方式操作数组元素
let fruits = ["Apple", "Orange", "Pear"];
// 栈操作
fruits.push("Banana"); // 添加到末端
console.log(fruits.pop()); // 从末端移除
// 队列操作
fruits.shift(); // 从首端移除
fruits.unshift("Strawberry"); // 添加到首端
// 结果
console.log(fruits); // 输出当前数组的元素
遍历数组的方法主要有 for
循环和 for..of
循环。
for
循环:
let arr = ["Apple", "Orange", "Pear"];
for (let i = 0; i < arr.length; i++) {
alert(arr[i]);
}
for..of
循环:
let fruits = ["Apple", "Orange", "Plum"];
for (let fruit of fruits) {
alert(fruit);
}
for..in
循环通常不推荐用于数组,因为它会遍历所有属性,包括非数字属性。更适合用于普通对象的属性遍历。对于数组,for..in
循环的执行速度比 for
和 for..of
慢很多。
修改数组的时候,length 属性会自动更新。准确来说,它实际上不是数组里元素的个数,而是最大的数字索引值加一:
let fruits = [];
fruits[123] = "Apple";
alert(fruits.length); // 124
length 属性是可写的。我们减少它,数组就会被截断:
let arr = [1, 2, 3, 4, 5];
arr.length = 2; // 截断到只剩 2 个元素
alert(arr); // [1, 2]
arr.length = 5; // 又把 length 加回来
alert(arr[3]); // undefined:被截断的那些数值并没有回来
清空数组最简单的方法就是:arr.length = 0;
数组有自己的 toString 方法的实现,会返回以逗号隔开的元素字符串:
let arr = [1, 2, 3];
alert(arr); // 1,2,3
alert(String(arr) === "1,2,3"); // true
此外:
alert([] + 1); // "1"
alert([1] + 1); // "11"
alert([1, 2] + 1); // "1,21"
数组没有 Symbol.toPrimitive
,也没有 valueOf
,它们只能执行 toString
进行转换,所以这里 []
就变成了一个空字符串,[1]
变成了 "1"
,[1,2]
变成了 "1,2"
。
不要使用 ==
比较数组,可以在循环中或者使用迭代方法逐项地比较它们。
JS 中的可迭代对象和类数组对象
可迭代对象是实现了 Symbol.iterator
方法的对象。通过 for..of
循环可以遍历可迭代对象的元素。例如,字符串是可迭代的,我们可以使用 for..of
遍历字符串中的字符。
迭代器是一个有 next
方法的对象,next
方法在 for..of
循环的每一轮迭代中被调用,返回的结果格式必须是 {done: Boolean, value: any}
。当 done=true 时,表示循环结束,否则 value 是下一个值。
使用 Symbol.iterator 方法自定义迭代器:
let range = {
from: 1,
to: 5,
};
// 1. for..of 调用首先会调用这个:
range[Symbol.iterator] = function () {
// ……它返回迭代器对象(iterator object):
// 2. 接下来,for..of 仅与下面的迭代器对象一起工作,要求它提供下一个值
return {
current: this.from,
last: this.to,
// 3. next() 在 for..of 的每一轮循环迭代中被调用
next() {
// 4. 它将会返回 {done:.., value :...} 格式的对象
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
},
};
};
// 现在它可以运行了!
for (let num of range) {
alert(num); // 1, 然后是 2, 3, 4, 5
}
显式调用迭代器:
let str = "Hello";
// 和 for..of 做相同的事
// for (let char of str) alert(char);
let iterator = str[Symbol.iterator]();
while (true) {
let result = iterator.next();
if (result.done) break;
alert(result.value); // 一个接一个地输出字符
}
类数组对象具有索引和 length 属性,因此它们看起来很像数组。但是,它们不具备数组的方法(如 push 和 pop)。例如,一个普通的对象 { 0: "Hello", 1: "World", length: 2 }
就是类数组对象。
Array.from 是一个全局方法,用于将可迭代对象或类数组对象转换为真正的数组。
它接受一个对象作为参数,检查该对象是否是可迭代对象或类数组对象,然后创建一个新数组并复制对象的所有元素到新数组中。可以使用这个新数组调用数组方法。
let arrayLike = {
0: "Hello",
1: "World",
length: 2,
};
let arr = Array.from(arrayLike); // (*)
alert(arr.pop()); // World(pop 方法有效)
完整语法:
Array.from(obj[, mapFn, thisArg])
可选的第二个参数 mapFn 可以是一个函数,该函数会在对象中的元素被添加到数组前,被应用于每个元素,此外 thisArg 允许我们为该函数设置 this。
// 假设 range 来自上文例子中
// 求每个数的平方
let arr = Array.from(range, (num) => num * num);
alert(arr); // 1,4,9,16,25
JavaScript 的 Map 和 Set 用法
map 方法和属性如下:
new Map()
—— 创建 map。map.set(key, value)
—— 根据键存储值。map.get(key)
—— 根据键来返回值,如果 map 中不存在对应的 key,则返回 undefined。map[key]
不是使用 Map 的正确方式map.has(key)
—— 如果 key 存在则返回 true,否则返回 false。map.delete(key)
—— 删除指定键的值。map.clear()
—— 清空 map。map.size
—— 返回当前元素个数。map.keys()
—— 遍历并返回一个包含所有键的可迭代对象,map.values()
—— 遍历并返回一个包含所有值的可迭代对象,map.entries()
—— 遍历并返回一个包含所有实体[key, value]
的可迭代对象,for..of
在默认情况下使用的就是这个。
与对象不同,map 的键不会被转换成字符串。键可以是任何类型,Map 还可以使用对象作为键。。
Object.entries:从对象创建 Map:
let obj = {
name: "John",
age: 30,
};
let map = new Map(Object.entries(obj));
alert(map.get("name")); // John
Object.fromEntries:从 Map 创建对象:
let map = new Map();
map.set("banana", 1);
map.set("orange", 2);
map.set("meat", 4);
let obj = Object.fromEntries(map.entries()); // 创建一个普通对象(plain object)(*)
// 完成了!
// obj = { banana: 1, orange: 2, meat: 4 }
alert(obj.orange); // 2
set 的主要方法如下:
new Set(iterable)
—— 创建一个 set,如果提供了一个 iterable 对象(通常是数组),将会从数组里面复制值到 set 中。set.add(value)
—— 添加一个值,返回 set 本身set.delete(value)
—— 删除值,如果 value 在这个方法调用的时候存在则返回 true ,否则返回 false。set.has(value)
—— 如果 value 在 set 中,返回 true,否则返回 false。set.clear()
—— 清空 set。set.size
—— 返回元素个数。set.keys()
—— 遍历并返回一个包含所有值的可迭代对象,set.values()
—— 与set.keys()
作用相同,这是为了兼容 Map,set.entries()
—— 遍历并返回一个包含所有的实体[value, value]
的可迭代对象,它的存在也是为了兼容 Map。
遍历 set:
let set = new Set(["oranges", "apples", "bananas"]);
for (let value of set) alert(value);
// 与 forEach 相同:
set.forEach((value, valueAgain, set) => {
alert(value);
});
JavaScript 中 WeakMap 和 WeakSet 用法
把一个对象放入到数组中,那么只要这个数组存在,那么这个对象也就存在,即使没有其他对该对象的引用。类似的,如果我们使用对象作为常规 Map 的键,那么当 Map 存在时,该对象也将存在。它会占用内存,并且不会被(垃圾回收机制)回收。
WeakMap 和 WeakSet 最明显的局限性就是不能迭代,并且无法获取所有当前内容。
WeakMap 和 Map 的第一个不同点就是,WeakMap 的键必须是对象,不能是原始值:
let weakMap = new WeakMap();
let obj = {};
weakMap.set(obj, "ok"); // 正常工作(以对象作为键)
// 不能使用字符串作为键
weakMap.set("test", "Whoops"); // Error,因为 "test" 不是一个对象
// 如果我们在 weakMap 中使用一个对象作为键,并且没有其他对这个对象的引用 —— 该对象将会被从内存(和map)中自动清除。
obj = null; // 覆盖引用
// obj 被从内存中删除了!
WeakMap 不支持迭代以及 keys()
,values()
和 entries()
方法。
WeakMap 的主要应用场景是 额外数据的存储。例如,我们有用于处理用户访问计数的代码。收集到的信息被存储在 map 中:一个用户对象作为键,其访问次数为值。当一个用户离开时(该用户对象将被垃圾回收机制回收),这时我们就不再需要他的访问次数了。
我们将这些数据放到 WeakMap 中,并使用该对象作为这些数据的键,那么当该对象被垃圾回收机制回收后,这些数据也会被自动清除。
// 📁 visitsCount.js
let visitsCountMap = new Map(); // map: user => visits count
// 递增用户来访次数
function countUser(user) {
let count = visitsCountMap.get(user) || 0;
visitsCountMap.set(user, count + 1);
}
WeakSet 的表现类似,WeakSet 支持 add,has 和 delete 方法。
let visitedSet = new WeakSet();
let john = { name: "John" };
let pete = { name: "Pete" };
let mary = { name: "Mary" };
visitedSet.add(john); // John 访问了我们
visitedSet.add(pete); // 然后是 Pete
visitedSet.add(john); // John 再次访问
// visitedSet 现在有两个用户了
// 检查 John 是否来访过?
alert(visitedSet.has(john)); // true
// 检查 Mary 是否来访过?
alert(visitedSet.has(mary)); // false
john = null;
// visitedSet 将被自动清理(即自动清除其中已失效的值 john)
js 数组和对象的解构赋值
数组解构赋值
使用方括号 []
来声明解构赋值,可以将数组中的元素解构到对应的变量中。
let arr = ["John", "Smith"];
let [firstName, surname] = arr;
alert(firstName); // John
alert(surname); // Smith
// 数组解构不仅限于数组,还可以用于任何可迭代对象(如字符串、Set 等)。
let [a, b, c] = "abc"; // a="a", b="b", c="c"
// 使用逗号 , 来跳过数组中不需要的元素。
let [firstName, , title] = [
"Julius",
"Caesar",
"Consul",
"of the Roman Republic",
];
alert(title); // Consul
// 使用解构赋值来交换两个变量的值。
let guest = "Jane";
let admin = "Pete";
[guest, admin] = [admin, guest];
alert(`${guest} ${admin}`); // Pete Jane
// 使用 ... 来获取剩余的数组项。
let [name1, name2, ...rest] = [
"Julius",
"Caesar",
"Consul",
"of the Roman Republic",
];
alert(rest[0]); // Consul
alert(rest[1]); // of the Roman Republic
// 使用 Object.entries(obj) 方法遍历对象的键值对,然后进行解构赋值。
let user = { name: "John", age: 30 };
for (let [key, value] of Object.entries(user)) {
alert(`${key}:${value}`); // name:John, then age:30
}
对象解构赋值
当我们需要从一个对象中提取属性并将其赋值给变量时,对象解构是一种非常有用的技术。
基本语法:let {var1, var2} = {var1:…, var2:…};
let options = {
title: "Menu",
width: 100,
height: 200,
};
// 变量的顺序并不重要
let { title, width, height } = options;
alert(title); // Menu
alert(width); // 100
alert(height); // 200
// ---------
// 指定变量名
let { width: w, height: h, title } = options;
// width -> w
// height -> h
// title -> title
alert(title); // Menu
alert(w); // 100
alert(h); // 200
// -----
// 默认值
let options = {
title: "Menu",
};
let { width = 100, height = 200, title } = options;
alert(title); // Menu
alert(width); // 100
alert(height); // 200
嵌套解构:
let options = {
size: {
width: 100,
height: 200,
},
items: ["Cake", "Donut"],
extra: true,
};
let {
size: { width, height },
items: [item1, item2],
title = "Menu",
} = options;
alert(title); // Menu
alert(width); // 100
alert(height); // 200
alert(item1); // Cake
alert(item2); // Donut
我们可以将一个对象传递给函数,然后在函数内部解构出各个参数:
let options = {
title: "My menu",
items: ["Item1", "Item2"],
};
function showMenu({
title = "Untitled",
width: w = 100,
height: h = 200,
items: [item1, item2],
}) {
alert(`${title} ${w} ${h}`); // My Menu 100 200
alert(item1); // Item1
alert(item2); // Item2
}
showMenu(options);
showMenu({}); // 所有值都取默认值
JS 日期(Date)用法
调用 new Date()
来创建一个新的 Date 对象。
// 不带参数 —— 创建一个表示当前日期和时间的 Date 对象:
let now = new Date();
alert(now); // 显示当前的日期/时间
// 传入时间戳参数
let Jan01_1970 = new Date(0);
alert(Jan01_1970);
// 传入字符串参数
let date = new Date("2017-01-26");
alert(date);
// 传入数字参数:new Date(year, month, date, hours, minutes, seconds, ms)
new Date(2011, 0, 1, 0, 0, 0, 0); // 1 Jan 2011, 00:00:00
new Date(2011, 0, 1); // 同样,时分秒等均为默认值 0
Date 对象的 get 方法:
getFullYear()
获取年份(4 位数)getMonth()
获取月份,从 0 到 11。getDate()
获取当月的具体日期,从 1 到 31,这个方法名称可能看起来有些令人疑惑。getHours()
,getMinutes()
,getSeconds()
,getMilliseconds()
获取相应的时间组件。getDay()
获取一周中的第几天,从 0(星期日)到 6(星期六)。第一天始终是星期日,在某些国家可能不是这样的习惯,但是这不能被改变。getUTCFullYear()
,getUTCMonth()
,getUTCDay()
返回基于 UTC+0 时区的日、月、年等。getTime()
返回日期的时间戳 —— 从 1970-1-1 00:00:00 UTC+0 开始到现在所经过的毫秒数。getTimezoneOffset()
返回 UTC 与本地时区之间的时差,以分钟为单位
Date 对象的 get 方法:
setFullYear(year, [month], [date])
setMonth(month, [date])
setDate(date)
setHours(hour, [min], [sec], [ms])
setMinutes(min, [sec], [ms])
setSeconds(sec, [ms])
setMilliseconds(ms)
setTime(milliseconds)
(使用自 1970-01-01 00:00:00 UTC+0 以来的毫秒数来设置整个日期)
以上方法除了 setTime()
都有 UTC 变体,例如:setUTCHours()
。
自动校准 是 Date 对象的一个非常方便的特性。我们可以设置超范围的数值,它会自动校准。
let date = new Date(2013, 0, 32); // 32 Jan 2013 ?!?
alert(date); // ……是 1st Feb 2013!
当 Date 对象被转化为数字时,得到的是对应的时间戳,与使用 date.getTime()
的结果相同:
let date = new Date();
alert(+date); // 以毫秒为单位的数值,与使用 date.getTime() 的结果相同
日期可以相减,相减的结果是以毫秒为单位时间差。
Date 方法:
Date.now()
,它会返回当前的时间戳,let start = Date.now(); // 从 1 Jan 1970 至今的时间戳
Date.parse(str)
方法可以从一个字符串中读取日期,let ms = Date.parse('2012-01-26T13:51:50.417-07:00');
JS JSON 方法
JSON.stringify
JSON.stringify(value[, replacer, space])
将对象转换为 JSON。
- value: 要编码的值。
- replacer: 要编码的属性数组或映射函数 function(key, value)。
- space: 用于格式化的空格数量。
对象的函数属性(方法)、Symbol 类型的键和值、存储 undefined 的属性将被忽略。
使用属性数组进行替换:
let room = {
number: 23,
};
let meetup = {
title: "Conference",
participants: [{ name: "John" }, { name: "Alice" }],
place: room, // meetup 引用了 room
};
room.occupiedBy = meetup; // room 引用了 meetup
alert(
JSON.stringify(meetup, ["title", "participants", "place", "name", "number"])
);
/*
{
"title":"Conference",
"participants":[{"name":"John"},{"name":"Alice"}],
"place":{"number":23}
}
*/
使用函数作为 replacer:
let room = {
number: 23,
};
let meetup = {
title: "Conference",
participants: [{ name: "John" }, { name: "Alice" }],
place: room, // meetup 引用了 room
};
room.occupiedBy = meetup; // room 引用了 meetup
// replacer 函数会获取每个键/值对,包括嵌套对象和数组项。它被递归地应用。replacer 中的 this 的值是包含当前属性的对象。
JSON.stringify(meetup, function replacer(key, value) {
alert(`${key}: ${value}`);
return key == "occupiedBy" ? undefined : value;
});
toJSON
对象可以提供 toJSON 方法来进行 JSON 转换。如果可用,JSON.stringify 会自动调用它。
let room = {
number: 23,
toJSON() {
return this.number;
},
};
let meetup = {
title: "Conference",
room,
};
alert(JSON.stringify(room)); // 23
alert(JSON.stringify(meetup));
/*
{
"title":"Conference",
"room": 23
}
*/
JSON.parse
JSON.parse(str, [reviver])
将 JSON 转换回对象。
- str 要解析的 JSON 字符串。
- reviver 可选的函数
function(key,value)
,该函数将为每个(key, value)
对调用,并可以对值进行转换,这也适用于嵌套对象。
let schedule = `{
"meetups": [
{"title":"Conference","date":"2017-11-30T12:00:00.000Z"},
{"title":"Birthday","date":"2017-04-18T12:00:00.000Z"}
]
}`;
schedule = JSON.parse(schedule, function (key, value) {
if (key == "date") return new Date(value);
return value;
});
alert(schedule.meetups[1].date.getDate());