html / CSS

主要介绍 html/css 入门知识,用来做 typescript 的前置知识。配套课程文件

html

HTML(Hypertext Markup Language):The language your web browser uses to describe the content and structure of web pages

如果整个网页当作人体,那么 html 就是骨架,决定每一资源放置的位置

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html>
<head>
<title>Title!</title>
</head>
<body>
<h1>Heading!</h1>
<p>Paragraph!</p>
</body>
</html>

alt text

文档对象模型(DOM)通过将文档的结构(例如表示网页的 HTML)以对象的形式存储在内存中,将网页与脚本或编程语言连接起来。尽管将 HTML、SVG 或 XML 文档建模为对象并不是 JavaScript 核心语言的一部分,但它通常与 JavaScript 相关。

DOM 使用逻辑树的形式来表示文档。树的每个分支末端都是一个节点,每个节点都包含对象。DOM 方法允许以编程方式来访问树。借助这些方法,你可以改变文档的结构、样式或内容。

节点还可以附加事件处理器。一旦事件被触发,相应的事件处理器就会被执行。

html 是按照 “DOM” (Document Object Model) 的结构进行表达的。因此,上述代码中包含一个 <head> 和 一个 <body>。

其中 <head> 部分是不可见的(除了 <title> 之外),主要用于包含网页的元数据(如字符编码、标题、样式表链接等);<body> 部分则包含网页中可见的实际内容。

tag

html 中的 tag 是由两部分组成的,分别叫做 Opening Tag 和 Closing Tag

1
<tagName> content </tagName>

除此之外,tag 中还可以加入属性(attribute)

HTML Attribute(HTML 属性) 是写在开始标签内部的额外信息,用于配置或修饰该元素的行为、样式或内容。

1
<tagName ABC = "XYZ"> content </tagName>

此外,还有一类特殊的标签——它们没有内容,因此可以省略闭合标签 ,这类标签称为 Void Elements(空元素 / 自闭合标签)。例如:

标签 作用
<img> 嵌入图片
<br> 换行
<hr> 水平分隔线
<input> 输入框
<meta> 元信息
<link> 引入外部资源(如 CSS)
<area> 图像映射区域
<base> 基准 URL
<col> 表格列属性
<embed> 嵌入外部内容
<param> 对象参数
<source> 媒体资源
<track> 媒体字幕
<wbr> 可选换行点

使用方法如下:

1
2
3
4
5
6
7
8
9
<!-- 简洁写法(推荐) -->
<img src="photo.jpg" alt="描述">
<br>
<hr>

<!-- 自闭合写法(XHTML 风格,也合法) -->
<img src="photo.jpg" alt="描述" />
<br />
<hr />

Common tag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- 插入链接 -->
<a href="http://weblab.mit.edu">Link to web.lab!</a>

<!-- 插入图片 -->
<img src="pusheen.gif" >
<img src="pusheen.gif" alt="pic of pusheen"/>

<!-- 序列 -->
<!-- <ol>: ordered list -->
<!-- <ul>: unordered list -->
<!-- <li>: list item -->

<ul>
<li>item 1</li>
<li>item 2</li>
</ul>

其中比较特殊的是 <div> 和 <span>:<div> 本质上是创建了一个 box(块级容器),用于将所有内容封装到一个独立的盒子中;<span> 本质上则是一个 行内标记,用于在文本流中包裹小段内容而不打断布局。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>

<header></header>

<body>
<!-- div:块级,独占一行,常用于布局 -->
<div style="background: yellow;">
这是一个 div,它会独占一整行
</div>
<div style="background: lightblue;">
这是另一个 div,自动换行
</div>

<!-- span:行内,不独占行,常用于文本修饰 -->
<p>
这是一段文字,<span style="color: red;">这里标红</span>,
<span style="font-weight: bold;">这里加粗</span>,其余正常。
</p>

</body>

alt text

但是,在后续的 html5 中引入了语义化标签,可以明显添加可读性,然后简化语法,可以减少使用 <div> 的使用。

CSS

CSS(Cascading Style Sheets):The rules that tell your web browser how stuff looks

如果说 html 是整个网页的骨架,那么 css 就是用来美化这些骨架的衣服。可以让我们的网页变的更加的美观。

基础的语法为

1
2
3
4
selector {
property: value;
property: value;
}

一个基础的示例为:

1
2
3
4
5
6
/* 这是注释 */
div {
color: red;
font-family: Arial;
font-size: 24pt;
}

这部分直接使用 <style> 将 css 嵌入到 html 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>示例</title>
<style>
div {
color: red;
font-family: Arial;
font-size: 24pt;
}
</style>
</head>
<body>
<div>这段文字会显示为红色、Arial 字体、24pt 大小。</div>
</body>
</html>

另外我们还可以使用 . 来进行选择指定的 class 属性,比如下面就直接使用匹配 info

1
2
3
4
5
.info {
color: red;
font-family: Arial;
font-size: 24pt;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>示例</title>
<style>
.info {
color: red;
font-family: Arial;
font-size: 24pt;
}
</style>
</head>
<body>
<div class="info">这段文字会显示为红色、Arial 字体、24pt 大小。</div>
</body>
</html>

或者使用 # 直接匹配对应的 id

1
2
3
4
5
#info {
color: red;
font-family: Arial;
font-size: 24pt;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>示例</title>
<style>
#info {
color: red;
font-family: Arial;
font-size: 24pt;
}
</style>
</head>
<body>
<div id="info">这段文字会显示为红色、Arial 字体、24pt 大小。</div>
</body>
</html>

css 生效优先级

由于每个元素只能拥有一个 id,因此 id 通常用于定义唯一的、完整的样式规则。相比之下,一个元素可以拥有多个 class,每个 class 负责不同的样式维度——例如一个控制布局尺寸,另一个控制颜色字体。此外,class 可以被多个元素复用,从而减少重复代码,提高开发效率。

由于可能存在有多种格式,因此 css 具体要相关的层级:

层级 来源 说明
1 !important 最高优先级,慎用
2 行内样式(Inline) <div style="...">
3 ID 选择器 #header
4 类 / 伪类 / 属性 .btn、:hover、[type]
5 元素 / 伪元素 div、::before
6 通用选择器 *
7 继承(Inherited) 父元素传给子元素
8 浏览器默认样式 User Agent Stylesheet

css 变量

CSS 变量 = 用 --name 定义,用 var() 调用,通常放在 :root 里做全局配色/尺寸配置,方便统一管理和维护。

1
2
3
4
5
6
7
8
9
:root {
--primary-color: #3498db;
--font-size: 16px;
}

button {
background-color: var(--primary-color); /* 调用变量 */
font-size: var(--font-size);
}

Box Model

所有HTML元素可以看作盒子,在CSS中,box model这一术语是用来设计和布局时使用。
CSS盒模型本质上是一个盒子,封装周围的HTML元素,它包括:边距,边框,填充,和实际内容。
盒模型允许我们在其它元素和周围元素边框之间的空间放置元素。
下面的图片说明了盒子模型(Box Model):

alt text

不同部分的说明:

  • Margin(外边距) - 清除边框外的区域,外边距是透明的。
  • Border(边框) - 围绕在内边距和内容外的边框。
  • Padding(内边距) - 清除内容周围的区域,内边距是透明的。
  • Content(内容) - 盒子的内容,显示文本和图像。

最终元素的总宽度计算公式是这样的:
总元素的宽度=宽度+左填充+右填充+左边框+右边框+左边距+右边距

元素的总高度最终计算公式是这样的:
总元素的高度=高度+顶部填充+底部填充+上边框+下边框+上边距+下边距

html + css

如何将 html 和 css 文件结合起来,最佳做法是在 <head> 元素中添加一个 <link> 元素,并带上 rel 和 href 属性。

rel 是 relationship(关系)的缩写。
另外注意,这看起来可能和之前超链接页面的示例有点像——但那个用的是 <a> 标签,而这里用的是 <link> 标签。<link> 实际上是专门用于链接外部资源(比如 CSS 文件)的场景。

规范

8pt Grid System

8pt Grid System(8 点网格系统) 是一种 UI 设计规范,要求所有元素的尺寸、间距、边距都使用 8 的倍数。

javaScript

该部分内容建立在有 golang, python 和 C 的基础上进行补充学习。

JavaScript(简称 js) 是一种控制网页的脚本语言,可以完成 html 和 css 之间的互动。

js 是一个大小写敏感,如果一条语句独占一行的话,那么分号是可以省略的。但如果一行中有多条语句,那么这些语句必须用分号进行分隔。

  1. 在一条语句的末尾总是加上分号被认为是最佳实践,即使是在非严格需要的时候。这个习惯可以大大减少代码出问题的可能性。

  2. 你可能也会在一些 JavaScript 文件的开头见到像 #!/usr/bin/env node 这样的第三种注释。它叫做 hashbang 注释语法,是一种用于指定执行脚本的特定 Javascript 引擎路径的特殊注释。

type

js 一共有五种主要的数据类型:

  • Boolean(true,false)
  • Number(0, -12.1, 123.12)
  • String
  • Null
  • undefined
  • BigInt
  • Symbol 其实例是唯一且不可变的数据类型。

operators

常规运算符

符号 名称 示例 结果
+ 加 5 + 3 8
- 减 5 - 3 2
* 乘 5 * 3 15
/ 除 5 / 2 2.5
% 取余 5 % 2 1
** 幂运算 2 ** 3 8
++ 自增 a++ / ++a 先返回再加 / 先加再返回
-- 自减 a-- / --a 先返回再减 / 先减再返回
符号 名称 示例 等价于
= 赋值 a = 5 —
+= 加等 a += 3 a = a + 3
-= 减等 a -= 3 a = a - 3
*= 乘等 a *= 3 a = a * 3
/= 除等 a /= 3 a = a / 3
%= 余等 a %= 3 a = a % 3
**= 幂等 a **= 3 a = a ** 3
符号 名称 示例 说明
== 相等 5 == "5" true(值相等,类型可不同)
=== 严格相等 5 === "5" false(值和类型都相同)
!= 不等 5 != 3 true
!== 严格不等 5 !== "5" true
> 大于 5 > 3 true
< 小于 5 < 3 false
>= 大于等于 5 >= 5 true
<= 小于等于 5 <= 3 false
符号 名称 示例 说明
&& 逻辑与 a && b 两者都为真才为真
|| 逻辑或 a || b 至少一个为真即为真
! 逻辑非 !a 取反
?? 空值合并 a ?? b a 为 null 或 undefined 时返回 b
符号 名称 示例
& 按位与 5 & 3 → 1
| 按位或 5 | 3 → 7
^ 按位异或 5 ^ 3 → 6
~ 按位非 ~5 → -6
<< 左移 5 << 1 → 10
>> 右移 5 >> 1 → 2
符号 名称 示例 说明
? : 三元运算符 a > b ? "大" : "小" 条件判断简写
typeof 类型检测 typeof 123 → "number" 返回数据类型
instanceof 实例检测 arr instanceof Array 判断是否某类的实例
... 展开运算符 [...arr] / fn(...args) 展开数组或对象
?. 可选链 obj?.name 避免访问 null 报错
??= 逻辑空赋值 a ??= b a 为空时赋值为 b
  1. 在使用 + 运算符的表达式中涉及数字和字符串,JavaScript 会把数字转换成字符串。在使用其他运算符时,JavaScript 不会把数字转换成字符串。
  2. 将字符串转换为数字的另一种方法是使用 +(一元加)运算符。
1
2
3
"1.1" + "1.1" = "1.11.1"
(+"1.1") + (+"1.1") = 2.2
// 注意:括号是为了清晰起见而添加的,不是必需的。

一元运算符

delete

delete操作符,删除一个对象的属性或者一个数组中某一个键值。

1
2
3
delete objectName.property;
delete objectName[index];
delete property; // legal only within a with statement
  1. 当前 with 已经被废弃,他会导致作用域混乱,性能下降。

  2. 在严格模式(”use strict”)下,对不可删除的属性使用 delete 会抛出 TypeError,而非静默返回 false。

  1. 如果 delete 操作成功,属性或者元素会变成 undefined。如果 delete可行会返回true,如果不成功返回false。
  2. 删除数组中的元素时,数组的长度是不变的,例如删除 a[3], a[4],a[4] 和 a[3] 仍然存在变成了 undefined。delete 删除数组中的一个元素,这个元素就不在数组中了。例如,trees[3]被删除,trees[3] 仍然可寻址并返回 undefined。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 正常删除
x = 42;
var y = 43;
myobj = new Number();
myobj.h = 4; // create property h
delete x; // returns true (can delete if declared implicitly)
delete y; // returns false (cannot delete if declared with var)
delete Math.PI; // returns false (cannot delete predefined properties)
delete myobj.h; // returns true (can delete user-defined properties)
delete myobj; // returns true (can delete if declared implicitly)

// 删除数组
var trees = new Array("redwood", "bay", "cedar", "oak", "maple");
delete trees[3];
// trees[3] 变成 empty slot
if (3 in trees) {
// 不会被执行
}

var trees = new Array("redwood", "bay", "cedar", "oak", "maple");
trees[3] = undefined;
// 只是值变成 undefined
if (3 in trees) {
// 会被执行
}

typeof

1
2
typeof operand;
typeof (operand);

typeof 操作符返回一个表示 operand 类型的字符串值。operand 可为字符串、变量、关键词或对象,其类型将被返回。operand 两侧的括号为可选。

void

1
2
void expression;
void (expression);

void 运算符,表明一个运算没有返回值。expression 是 javaScript 表达式,括号中的表达式是一个可选项,当然使用该方式是一种好的形式。

你可以使用 void 运算符指明一个超文本链接。该表达式是有效的,但是并不会在当前文档中进行加载。

in

in操作符,如果所指定的属性确实存在于所指定的对象中,则会返回true,语法如下:

1
propNameOrNumber in objectName;

在这里 propNameOrNumber可以是一个代表着属性名的字符串或者是一个代表着数组索引的数值表达式,而objectName则是一个对象名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Arrays
var trees = new Array("redwood", "bay", "cedar", "oak", "maple");
0 in trees; // returns true
3 in trees; // returns true
6 in trees; // returns false
"bay" in trees; // returns false (you must specify the index number,
// not the value at that index)
"length" in trees; // returns true (length is an Array property)

// Predefined objects
"PI" in Math; // returns true
var myString = new String("coral");
"length" in myString; // returns true

// Custom objects
var mycar = { make: "Honda", model: "Accord", year: 1998 };
"make" in mycar; // returns true
"model" in mycar; // returns true

instanceof

如果所判别的对象确实是所指定的类型,则返回true。其语法如下:

1
objectName instanceof objectType;

objectName 是需要做判别的对象的名称,而objectType是假定的对象的类型,例如Date或 Array.

当你需要确认一个对象在运行时的类型时,可使用instanceof. 例如,需要 catch 异常时,你可以针对抛出异常的类型,来做不同的异常处理。

this

this关键字被用于指代当前的对象,通常,this指代的是方法中正在被调用的对象。用法如下:

1
2
this["propertyName"];
this.propertyName;

()

分组操作符()控制了表达式中计算的优先级。举例来说,你可以改变先乘除后加减的顺序,转而先计算加法。

new

你可以使用 new 运算符创建一个自定义类型或者是预置类型的对象实例。用法如下:

1
var objectName = new objectType([param1, param2, ..., paramN]);

super

super 关键字可以用来调用一个对象父类的函数,它在用来调用一个类的父类的构造函数时非常有用,比如:

1
super([arguments]); // calls the parent constructor. super.functionOnParent([arguments]);

运算优先级

优先级 运算符类型 结合性 运算符
19 分组 n/a ( … )
18 成员访问 从左到右 … . …
18 需计算的成员访问 从左到右 … [ … ]
18 new(带参数列表) n/a new … ( … )
18 函数调用 从左到右 … ( … )
18 可选链(Optional chaining) 从左到右 ?.
17 new(无参数列表) 从右到左 new …
16 后置递增 n/a … ++
16 后置递减 n/a … --
15 逻辑非 (!) 从右到左 ! …
15 按位非 (~) 从右到左 ~ …
15 一元加法 (+) 从右到左 + …
15 一元减法 (-) 从右到左 - …
15 前置递增 从右到左 ++ …
15 前置递减 从右到左 -- …
15 typeof 从右到左 typeof …
15 void 从右到左 void …
15 delete 从右到左 delete …
15 await 从右到左 await …
14 幂 (**) 从右到左 … ** …
13 乘法 (*) 从左到右 … * …
13 除法 (/) 从左到右 … / …
13 取余 (%) 从左到右 … % …
12 加法 (+) 从左到右 … + …
12 减法 (-) 从左到右 … - …
11 按位左移 (<<) 从左到右 … << …
11 按位右移 (>>) 从左到右 … >> …
11 无符号右移 (>>>) 从左到右 … >>> …
10 小于 (<) 从左到右 … < …
10 小于等于 (<=) 从左到右 … <= …
10 大于 (>) 从左到右 … > …
10 大于等于 (>=) 从左到右 … >= …
10 in 从左到右 … in …
10 instanceof 从左到右 … instanceof …
9 相等 (==) 从左到右 … == …
9 不相等 (!=) 从左到右 … != …
9 严格相等 (===) 从左到右 … === …
9 严格不相等 (!==) 从左到右 … !== …
8 按位与 (&) 从左到右 … & …
7 按位异或 (^) 从左到右 … ^ …
6 按位或 (|) 从左到右 … | …
5 逻辑与 (&&) 从左到右 … && …
4 逻辑或 (||) 从左到右 … || …
4 空值合并 (??) 从左到右 … ?? …
3 条件(三元)运算符 从右到左 … ? … : …
2 赋值 从右到左 … = …
2 加等赋值 从右到左 … += …
2 减等赋值 从右到左 … -= …
2 幂等赋值 从右到左 … **= …
2 乘等赋值 从右到左 … *= …
2 除等赋值 从右到左 … /= …
2 余等赋值 从右到左 … %= …
2 左移等赋值 从右到左 … <<= …
2 右移等赋值 从右到左 … >>= …
2 无符号右移等赋值 从右到左 … >>>= …
2 按位与等赋值 从右到左 … &= …
2 按位异或等赋值 从右到左 … ^= …
2 按位或等赋值 从右到左 … |= …
2 逻辑与等赋值 从右到左 … &&= …
2 逻辑或等赋值 从右到左 … ||= …
2 空值合并等赋值 从右到左 … ??= …
1 逗号 / 序列 从左到右 … , …

declaration

  • var:声明一个变量,可选择将其初始化为一个值。可以用来声明局部,或者全局变量
  • let:声明一个块级作用域的局部变量,可选择将其初始化为一个值。用来声明块级作用域的局部变量。如果在非严格模式下,重新声明两个相同的变量不会报错。
  • const:声明一个块级作用域的只读命名常量。

另外,js 还有解构语法用于解包值

1
2
3
4
5
6
7
8
9
10
11
12
13
let a, b, rest;
[a, b] = [10, 20];

console.log(a);
// 期望输出:10

console.log(b);
// 期望输出:20

[a, b, ...rest] = [10, 20, 30, 40, 50];

console.log(rest);
// 期望输出:Array [30, 40, 50]

array

  • 数组
  • 布尔值数组
  • 数字数组
  • 对象数组
  • 字符串数组
  • RegExp 数组

数组字面量中的多余逗号

1
const fish = ["Lion", , "Angel"];

打印出数组的形式为:

1
2
console.log(fish);
// [ 'Lion', <1 empty item>, 'Angel' ]

注意,第二项是“empty”,与实际的 undefined 值完全不同。当使用数组遍历方法(例如,Array.prototype.map)时,空槽会被跳过。然而,索引访问 fish[1] 仍会返回 undefined。

如果你在元素列表的尾部添加了一个逗号,它将会被忽略。

在书写代码时,应该显式地将缺失的元素声明为 undefined,或者至少插入一个注释以突出元素缺失。这样做能提高代码的清晰度和维护性。

另外,还有一种形式,数组长度为 4:

1
const myList = ["home", , "school", ,];

这个可以省略最后一个逗号

1
const myList = ["home", , "school", ];

尾后逗号在多行数组中能保持 git diff 整洁,因为向末尾添加元素只需要添加一行,而不需要修改前面的行。

对象字面值

对象字面值是由一对花括号 ({}) 括起来的包含零个或者多个属性名和相关值的列表。如果合法,那么使用 . 来访问,如果名字非法,那么使用 [] 来访问。

1
2
3
4
const car = { manyCars: { a: "Saab", b: "Jeep" }, 7: "Mazda" };

console.log(car.manyCars.b); // Jeep
console.log(car[7]); // Mazda

此外,还有增强型对象字面量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 1. 定义原型对象,其 toString() 返回 42
const theProtoObj = {
toString() {
return "42";
}
};

// 2. handler 变量
const handler = "some handler";

// 3. 创建对象
const obj = {
// 设置原型
__proto__: theProtoObj,

// 属性简写:handler: handler
handler,

// 方法简写
toString() {
// super.toString() 调用原型链上的 toString()
return "d " + super.toString();
},

// 计算属性名:["prop_" + 42] → prop_42
// () => 42 是一个立即执行的箭头函数,返回 42
["prop_" + (() => 42)()]: 42,
};

// 4. 调用
console.log(obj.toString()); // "d 42"
console.log(obj.prop_10); // 42
console.log(obj.handler); // "some handler"
console.log(Object.getPrototypeOf(obj) === theProtoObj); // true
console.log(()=> 42())

RegExp 字面值

一个正则表达式字面量是字符被正斜杠围成的表达式。下面是正则表达式字面量的一个示例。

1
const re = /ab+c/;

关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
// ========== 控制流 ==========

// break: 跳出循环或 switch
for (let i = 0; i < 10; i++) {
if (i === 3) break;
console.log(i); // 输出 0, 1, 2
}

// break label: 跳出指定标记的语句
outerLoop: for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (i === 1 && j === 1) break outerLoop;
console.log(`break label: i=${i}, j=${j}`);
}
}

// case / switch: 多分支选择
const x = 2;
switch (x) {
case 1: console.log("one"); break;
case 2: console.log("two"); break; // 输出 "two"
default: console.log("other");
}

// continue: 跳过当前迭代
for (let i = 0; i < 5; i++) {
if (i === 2) continue;
console.log(i); // 输出 0, 1, 3, 4(跳过 2)
}

// continue label: 跳到指定循环的下一次迭代
let count = 0;
continueLabel: for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (j === 1) continue continueLabel;
count++;
}
}
console.log("continue label count:", count); // 输出 3

// default: switch 默认分支
const day = 7;
switch (day) {
case 1: console.log("Mon"); break;
case 2: console.log("Tue"); break;
default: console.log("Weekend or other"); // 输出
}

// do...while: 先执行后判断
let n = 0;
do {
console.log("do...while:", n); // 至少执行一次
n++;
} while (n < 3);

// else: if 的否定分支
const num = -5;
if (num > 0) {
console.log("positive");
} else {
console.log("non-positive"); // 输出
}

// for: 标准循环
for (let i = 0; i < 3; i++) {
console.log("for loop:", i); // 0, 1, 2
}

// for...in: 遍历对象的可枚举属性(包括继承的)
const person = { name: "Alice", age: 25 };
for (const key in person) {
console.log(`for...in: ${key} = ${person[key]}`);
}

// for...in 在数组上(不推荐,会遍历到非索引属性)
const arr = [3, 5, 7];
arr.foo = "hello";
for (const index in arr) {
console.log(`for...in arr: ${index}`); // "0", "1", "2", "foo"
}

// for...of: 遍历可迭代对象的值
for (const value of arr) {
console.log(`for...of arr: ${value}`); // 3, 5, 7(注意没有 "hello")
}

// if: 条件判断
const score = 85;
if (score >= 90) {
console.log("A");
} else if (score >= 80) {
console.log("B"); // 输出
} else {
console.log("C");
}

// while: 条件循环
let w = 0;
while (w < 3) {
console.log("while:", w);
w++;
}

// label: 为语句提供标识符(配合 break/continue 使用)
let labeledCount = 0;
outPoint: for (let i = 0; i < 10; i++) {
for (let j = 0; j < 10; j++) {
if (i === 5 && j === 5) {
break outPoint; // 跳出两层循环
}
labeledCount++;
}
}
console.log("labeled break count:", labeledCount); // 55

// with: 扩展作用域链(严格模式下禁用,不推荐)
with (Math) {
console.log("with PI:", PI); // 等价于 Math.PI
console.log("with sqrt:", sqrt(16)); // 等价于 Math.sqrt(16)
}

// ========== 异常处理 ==========

// try...catch...finally
function riskyOperation() {
throw new Error("Something went wrong");
}

try {
riskyOperation();
} catch (err) {
console.error("caught:", err.message);
} finally {
console.log("cleanup always runs");
}

// throw: 抛出异常
function divide(a, b) {
if (b === 0) throw new Error("Cannot divide by zero");
return a / b;
}

// ========== 函数与返回 ==========

// function: 声明函数
function greet(name) {
return "Hello " + name;
}
console.log(greet("World")); // "Hello World"

// return: 提前返回
function checkAge(age) {
if (age < 0) return "Invalid";
if (age < 18) return "Minor";
return "Adult";
}

// yield: 生成器函数,暂停并返回值
function* idGenerator() {
let id = 1;
while (true) {
yield id++;
}
}
const gen = idGenerator();
console.log("yield:", gen.next().value); // 1
console.log("yield:", gen.next().value); // 2

// ========== 变量声明 ==========

// var: 函数作用域(存在变量提升)
var oldStyle = 1;
function varTest() {
var x = 1;
if (true) {
var x = 2; // 同一个变量
console.log("var inside:", x); // 2
}
console.log("var outside:", x); // 2
}
varTest();

// let: 块级作用域
let count = 0;
if (true) {
let count = 10; // 不同的变量
console.log("let inside:", count); // 10
}
console.log("let outside:", count); // 0

// const: 常量,不能重新赋值
const PI = 3.14159;
// PI = 3; // TypeError
const user = { name: "Bob" };
user.name = "Alice"; // 可以修改对象属性
console.log("const object:", user.name);

// ========== 面向对象 ==========

// class / constructor / extends / super / new
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return this.name + " makes noise";
}
}

class Dog extends Animal {
constructor(name, breed) {
super(name); // 调用父类构造函数
this.breed = breed;
}
speak() {
return super.speak() + ", and barks"; // 调用父类方法
}
}

const dog = new Dog("Rex", "Husky");
console.log("class:", dog.speak());

// this: 当前执行上下文
const obj = {
value: 42,
getValue() {
return this.value;
}
};
console.log("this:", obj.getValue()); // 42

// instanceof: 检测原型链
console.log("instanceof:", dog instanceof Dog); // true
console.log("instanceof:", dog instanceof Animal); // true
console.log("instanceof:", dog instanceof Object); // true

// in: 检测属性是否存在(包括继承的属性)
console.log("in:", "name" in dog); // true
console.log("in:", "speak" in dog); // true
console.log("in:", "toString" in dog); // true(继承自 Object.prototype)

// ========== 模块(需在模块环境中运行) ==========

// export: 导出
// export const foo = 1;
// export function bar() { return 2; }
// export default class MyClass {}

// import: 导入
// import { foo, bar } from "./module.js";
// import * as utils from "./utils.js";
// import MyClass from "./module.js";

// ========== 运算符相关关键字 ==========

// typeof: 返回类型字符串
console.log("typeof number:", typeof 123); // "number"
console.log("typeof string:", typeof "hello"); // "string"
console.log("typeof boolean:", typeof true); // "boolean"
console.log("typeof undefined:", typeof undefined); // "undefined"
console.log("typeof object:", typeof {}); // "object"
console.log("typeof function:", typeof function(){}); // "function"
console.log("typeof symbol:", typeof Symbol()); // "symbol"
console.log("typeof bigint:", typeof 123n); // "bigint"

// void: 表达式求值后返回 undefined
let voidResult = void (2 + 3); // 5 被计算,但返回 undefined
console.log("void:", voidResult); // undefined

// 常见用法:立即执行函数表达式(避免返回值被利用)
void function() {
console.log("void IIFE runs");
}();

// delete: 删除对象属性
const car = { brand: "Toyota", year: 2020 };
console.log("before delete:", "year" in car); // true
delete car.year;
console.log("after delete:", "year" in car); // false
console.log("car:", car); // { brand: "Toyota" }

// 注意:delete 不能删除变量或内置对象属性
var globalVar = 1;
// delete globalVar; // 严格模式下报错,非严格模式无效

// ========== 调试 ==========

// debugger: 断点(浏览器/Node 调试器会在此暂停)
function debugMe() {
let x = 1;
// debugger; // 取消注释可在调试时暂停
return x;
}

// ========== 字面量(非关键字但保留) ==========

// null: 表示"无值"的对象占位符
const empty = null;
console.log("null:", empty, typeof empty); // null "object"

// true / false: 布尔值
const flag = true;
console.log("boolean:", flag, !flag); // true false

// ========== 严格模式保留字(不能用作标识符) ==========

// 以下在严格模式下使用会报 SyntaxError:
// implements, interface, let, package, private, protected, public, static

// 示例验证:
// "use strict";
// const private = 1; // SyntaxError: Unexpected strict mode reserved word

假值

下面这些值求值为 false(也叫做假值):

  • false
  • undefined
  • null
  • 0
  • NaN
  • 空字符串(””)

对面字面量恒为真

布尔值和 Boolean 对象有一定区别,下面代码中 b 为 Boolean 对象,对象值恒为真

1
2
3
4
5
6
7
const b = new Boolean(false);
if (b) {
// 这个条件求值为 true
}
if (b == true) {
// 这个条件求值为 false
}

for in 和 for of 实现逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
const arr = [3, 5, 7];
arr.foo = "hello";

for (const i in arr) {
console.log(i); // 输出 "0", "1", "2", "foo"
}

for (const i of arr) {
console.log(i); // 输出 "3", "5", "7"
}

console.log(arr)
// 注意 for...of 的输出没有出现 "hello"

为什么他们的输出逻辑不一样,这个是 JavaScript 的底层逻辑决定的:

1
2
3
4
5
6
7
8
9
10
11
12
arr = {
// 数组元素区(被 length 管理),如果进行 arr["4"] 那么空余部分就会直接填充 empty item,访问的时候会直接打印出 undefined
[0]: "a"
[1]: "b"
[2]: "c"

// 普通属性区(length 不管理)
foo: "hello"

// 特殊属性
length: 3
}

for…of 不直接遍历对象的属性,而是调用数组的 [Symbol.iterator] 迭代器。这个迭代器的内部逻辑是:

empty slot 不会被跳过,而是返回 undefined(因为迭代器会访问 arr[3],发现没有值,就返回 undefined)

1
2
3
4
5
6
7
8
9
10
11
12
// 伪代码:数组迭代器的内部实现
function arrayIterator() {
let index = 0;
return {
next() {
if (index < this.length) { // 只看 length,不看其他属性
return { value: this[index++], done: false };
}
return { done: true };
}
};
}

for…in 遍历的是对象所有可枚举(enumerable)的属性名,包括继承自原型链上的可枚举属性。其内部逻辑大致为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 伪代码:for...in 的内部实现
function forIn(obj) {
const keys = [];
// 1. 收集自身可枚举属性
for (const key of Object.getOwnPropertyNames(obj)) {
if (Object.getOwnPropertyDescriptor(obj, key).enumerable) {
keys.push(key);
}
}
// 2. 收集原型链上的可枚举属性(直到 Object.prototype)
let proto = Object.getPrototypeOf(obj);
while (proto) {
for (const key of Object.getOwnPropertyNames(proto)) {
if (Object.getOwnPropertyDescriptor(proto, key).enumerable) {
keys.push(key);
}
}
proto = Object.getPrototypeOf(proto);
}
// 3. 按规范排序输出:数字索引按数值升序,字符串属性按插入顺序
return keys.sort((a, b) => {
const aNum = parseInt(a, 10);
const bNum = parseInt(b, 10);
if (!isNaN(aNum) && !isNaN(bNum)) return aNum - bNum;
return 0; // 字符串属性保持插入顺序
});
}

此外,底层的存储方式是:如果是一个连续的数组,那么就会直接使用一串连续的内存来保存对应的数组值。如果是加入了大量其他的属性、或者数组变得极度稀疏(如 arr[10000] = 1),那么就会退化成 hash 表。但是退化之后,数字索引部分还是会按照数值进行排序存储,字符串属性则按插入顺序排列,以保证 for…in 和 for…of 的输出顺序符合 ECMAScript 规范。

函数

示例

1
2
3
function square(number) {
return number * number;
}

参数本质上是按值传递给函数的——因此,即使函数体的代码为传递给函数的参数赋了新值,这个改变也不会反映到全局或调用该函数的代码中。

但是数组和对象传入到函数中被修改了,因为这部分是引用类型,只有基础类型(string,string,bigint,bigint,undefined,symbol,null)传入才不会反映到全局。

匿名函数

1
2
3
4
5
const square = function (number) {
return number * number;
};

console.log(square(4)); // 16
1
2
3
4
5
const factorial = function fac(n) {
return n < 2 ? 1 : n * fac(n - 1);
};

console.log(factorial(3)); // 6

函数提升

函数定义可以放在任意位置,因为 js 解释器会自动将函数定义放在当前作用域顶部。但是,函数提升仅适用于函数声明,而不适用于函数表达式。以下代码无法运行:

1
2
3
4
console.log(square(5)); // ReferenceError: Cannot access 'square' before initialization
const square = function (n) {
return n * n;
};

嵌套函数 / 闭包

闭包是可以拥有独立变量以及绑定了这些变量的环境(“封闭”了表达式)的表达式(通常是函数)。

闭包形成的三个条件:

  • 存在嵌套函数(函数内部定义了另一个函数)
  • 内部函数引用了外部函数的变量
  • 内部函数被返回或传递到外部作用域执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function outer() {
let count = 0; // 外部函数的局部变量

function inner() { // 内部函数
count++; // 内部函数引用了外部变量
return count;
}

return inner; // 将内部函数返回出去
}

const fn = outer(); // outer 执行完毕,但 count 没有被销毁!

console.log(fn()); // 1
console.log(fn()); // 2
console.log(fn()); // 3

outer() 执行完后,按理说 count 应该被垃圾回收。但因为 inner 函数引用了 count,引擎会保留这个变量,形成闭包。

用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function createCounter() {
let count = 0; // 外部无法直接访问

return {
increment: () => ++count,
decrement: () => --count,
getValue: () => count
};
}

const counter = createCounter();
console.log(counter.getValue()); // 0
counter.increment();
console.log(counter.getValue()); // 1
// count 无法被外部直接修改

arguments 对象

示例

1
2
3
4
5
6
7
8
function myConcat(separator) {
let result = ""; // 初始化列表
// 迭代 arguments
for (let i = 1; i < arguments.length; i++) {
result += arguments[i] + separator;
}
return result;
}

默认参数

1
2
3
4
5
function multiply(a, b = 1) {
return a * b;
}

console.log(multiply(5)); // 5

剩余参数

1
2
3
4
5
6
function multiply(multiplier, ...theArgs) {
return theArgs.map((x) => multiplier * x);
}

const arr = multiply(2, 1, 2, 3);
console.log(arr); // [2, 4, 6]

箭头函数

箭头函数总是匿名函数,没有单独的 this

1
2
3
4
5
6
7
8
9
10
11
const a = ["Hydrogen", "Helium", "Lithium", "Beryllium"];

const a2 = a.map(function (s) {
return s.length;
});

console.log(a2); // [8, 6, 7, 9]

const a3 = a.map((s) => s.length);

console.log(a3); // [8, 6, 7, 9]

对象

JavaScript 是一种基于对象的语言。

对象是由属性(property)和方法(method)组成的数据结构。

例如:

1
2
3
4
5
6
7
8
9
10
const person = {
name: "Alice",
age: 25,

greet() {
console.log(`Hello, ${this.name}`);
}
};

person.greet();

对象包含:

1
2
3
4
5
6
7
person
|
|-- name 属性
|
|-- age 属性
|
|-- greet 方法

创建对象

JavaScript 创建对象主要有以下几种方式:

  1. 对象字面量(Object Literal)
1
2
3
4
const obj = {
property_1: value_1,
property_2: value_2,
};

例如:

1
2
3
4
5
6
7
8
const myHonda = {
color: "red",
wheels: 4,
engine: {
cylinders: 4,
size: 2.2
}
};
  1. 构造函数(Constructor Function)

构造函数是用于创建多个结构相似对象的模板函数;new 关键字负责调用它,并自动完成创建实例、关联原型、绑定 this 等一系列操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Person(name, age, sex) {

this.name = name;
this.age = age;
this.sex = sex;

}


const rand = new Person(
"Rand McKinnon",
33,
"M"
);


const ken = new Person(
"Ken Jones",
39,
"M"
);
  1. Object.create()

Object.create 可以指定对象的原型创建新对象。
例如:

1
2
3
4
5
6
7
8
9
10
const Animal = {
type:"Invertebrates",
displayType(){
console.log(this.type);

}
};

const animal1 = Object.create(Animal);
animal1.displayType();

输出:

1
Invertebrates

关系:

1
2
3
4
5
6
7
8
9
animal1
|
↓

Animal
|
↓

Object.prototype

animal1 自己没有 displayType 方法。

但是可以通过原型链找到:

1
Animal.displayType()

原型

JavaScript 的继承机制底层不是基于类,而是基于原型。大多数 JavaScript 对象都继承自另一个对象(Object.create(null) 除外)。

prototype 是函数(尤其是构造函数)用来存放共享属性和方法的对象。

对象通过原型链查找属性:沿着 obj.__proto__ 向上查询,直到找到该属性,或链条到达 null(此时返回 undefined)。

__proto__ 是对象上指向原型的属性(遗留特性,推荐使用 Object.getPrototypeOf())。

new 工作原理

new 主要完成:

  1. 创建一个新的对象
  2. 绑定 prototype
  3. 执行 constructor
  4. 返回实例对象

Class(类)

class 是构造函数和 prototype 的语法糖。

ES5 中创建对象通常使用构造函数:

1
2
3
4
5
6
7
function Person(name){
this.name = name;
}

Person.prototype.say = function(){
console.log(this.name);
}

等价于:

1
2
3
4
5
6
7
function Person(name, age){
this.name = name;
this.age = age;
}

Person.prototype.say=function(){
}

所以 class 底层依然使用 prototype。

私有字段

私有字段是以 #(井号)开头的标识符。井号是这个字段名的必要部分,这也就意味着私有字段永远不会与公共字段或方法发生命名冲突。为了在类中的任何地方引用一个私有字段,你必须在类体中声明它(你不能在类体外部创建私有元素)。除此之外,私有字段与普通属性几乎是等价的。

在类外访问私有字段会导致语法错误。访问一个不存在的私有字段会抛出错误,而不是像普通属性一样返回 undefined。如果你不知道一个对象上是否存在一个私有字段,且你希望在不使用 try/catch 来处理错误的情况下访问它,你可以使用 in 运算符。

在单个类中,相同的名称不能声明两次,且它们不能被删除。这两种情况都会导致早期语法错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Color {
// 声明:每个 Color 实例都有一个名为 #values 的私有字段。
#values;
constructor(r, g, b) {
this.#values = [r, g, b];
}
getRed() {
return this.#values[0];
}
setRed(value) {
this.#values[0] = value;
}
}

const red = new Color(255, 0, 0);
console.log(red.getRed()); // 255

静态属性

静态属性是一组在类本身上定义的特性,而不是在类的实例上定义的特性。这些特性包括:静态方法、静态字段、静态 getter 与 setter。比如,存放常量或配置、实例计数器等

静态属性有 static 前缀,且不能从实例中访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Circle {
static PI = 3.14159; // 属于类,所有实例共用

constructor(radius) {
this.radius = radius;
}

getArea() {
return Circle.PI * this.radius ** 2; // 通过类名访问
}
}

console.log(Circle.PI); // 3.14159
console.log(new Circle(2).PI); // undefined(实例访问不到)

继承和扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ColorWithAlpha extends Color {
#alpha;
constructor(r, g, b, a) {
// 在构造函数中,我们调用了 super(r, g, b)。在访问 this 之前,必须调用 super(),这是 JavaScript 的要求。super() 调用父类的构造函数来初始化 this——这里大致相当于 this = new Color(r, g, b)。super() 之前也可以有代码,但你不能在 super() 之前访问 this——JavaScript 会阻止你访问未初始化的 this。
super(r, g, b);
this.#alpha = a;
}
get alpha() {
return this.#alpha;
}
set alpha(value) {
if (value < 0 || value > 1) {
throw new RangeError("Alpha 值必须在 0 与 1 之间");
}
this.#alpha = value;
}
}

派生类也可以覆盖父类的方法。例如,所有类都隐式继承自 Object 类,它定义了一些基本方法,例如 toString()。然而,基本的 toString() 方法是出了名的无用方法,因为它在大多数情况下打印 [object Object]:

1
2
3
4
5
6
7
8
9
10
class ColorWithAlpha extends Color {
#alpha;
// …
toString() {
// 调用父类的 toString(),并以此构建新的返回值
return `${super.toString()}, ${this.#alpha}`;
}
}

console.log(new ColorWithAlpha(255, 0, 0, 0.5).toString()); // '255, 0, 0, 0.5'

当你用 extends 时,静态方法也会继承,因此你也可以覆盖或增强它们。

1
2
3
4
5
6
7
8
9
class ColorWithAlpha extends Color {
// ...
static isValid(r, g, b, a) {
// 调用父类的 isValid(),并在此基础上增强返回值
return super.isValid(r, g, b) && a >= 0 && a <= 1;
}
}

console.log(ColorWithAlpha.isValid(255, 0, 0, -1)); // false

一个类只能至多扩展自一个父类。这可以防止多重继承中的问题,例如菱形问题。然而,由于 JavaScript 的动态性,仍然可以通过类组合和混入来实现多重继承的效果。

菱形问题:当两个类 B 和 C 都继承自 A,而类 D 同时继承自 B 和 C 时出现的歧义。如果 A 中有一个方法,B 和 C 都重写了该方法,而 D 没有重写它,那么 D 继承的是该方法的哪个版本:B 的版本还是 C 的版本?

TypeScript

由于 JavaScript 是一门动态类型语言,并且在 ==、算术运算等场景中存在隐式类型转换,因此一些类型错误可能不会在编写代码时立即暴露,而是在程序运行过程中才产生,容易导致一些难以发现的 bug。

TypeScript 在 JavaScript 的基础上增加了静态类型系统,可以在代码运行之前通过类型检查发现一部分由于类型不匹配而产生的潜在问题,从而提高代码的可维护性和可靠性。

另外,TypeScript 是 JavaScript 的超集。也就是说,合法的 JavaScript 代码通常也是合法的 TypeScript 代码。TypeScript 最终需要经过编译转换为 JavaScript,才能在浏览器或 Node.js 等 JavaScript 运行环境中执行。

TypeScript 并没有重新创造一套独立的运行时,而是建立在 JavaScript 现有的运行机制和生态之上。因此,JavaScript 中的对象、函数、Promise 等语言特性以及大量 JavaScript 库,都可以在 TypeScript 中继续使用,并可以逐步将现有的 JavaScript 项目迁移到 TypeScript。

数据类型

基础的数据类型有以下三种:

  1. string
  2. number
  3. boolean
  4. 数组;[number] 是不同的东西;请参阅关于元组类型的部分。
  5. any:如果没有指定类型,编译器会默认为 any,用 any 就禁用所有进一步的类型检查。

noImplicitAny:编译器标志 noImplicitAny 可以将任何隐式的 any 标记为错误。

类型注解

使用 const、var 或 let 声明变量时,你可以选择性地添加类型注解来显式指定变量的类型:

1
2
3
4
let myName: string = "Alice";

// 使用类型推断
let myName = "Alice";

如果是函数也是可以使用类型注解的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 参数类型注解
function greet(name: string) {
console.log("你好," + name.toUpperCase() + "!!");
}

// 返回类型注解
function getFavoriteNumber(): number {
return 26;
}

// 参数的类型注解是对象类型
function printCoord(pt: { x: number; y: number }) {
console.log("坐标的 x 值是 " + pt.x);
console.log("坐标的 y 值是 " + pt.y);
}
printCoord({ x: 3, y: 7 });

此外,ts 中还存在有上下文类型推断。

1
2
3
4
5
6
7
8
// 这里没有类型注解,但 TypeScript 可以发现错误
const names = ["Alice", "Bob", "Eve"];

// 函数的上下文类型推断
names.forEach(function (s) {
// 这里会自动推断为 string 类型
console.log(s.toUppercase());
});

除此之外,ts 还支持可选类型,具体语法是在变量背后加上 ?,如果访问一个不存在的属性,你会得到 undefined 而不是运行时错误。因此,如果你读取的是一个可选属性的话,那么在使用它之前,你需要检查其是否为 undefined。

1
2
3
4
5
6
function printName(obj: { first: string; last?: string }) {
// ...
}
// 都是有效的
printName({ first: "Bob" });
printName({ first: "Alice", last: "Alisson" });

联合类型

联合(Union)类型是组合类型的一种方式。联合类型是由两个或更多其他类型形成的类型,表示值可以是这些类型中的任意一个。我们将每个类型都称为联合的成员。

1
2
3
4
5
6
7
8
9
function printId(id: number | string) {
console.log("你的 ID 是:" + id);
}
// 正常运行
printId(101);
// 正常运行
printId("202");
// 错误
printId({ myID: 22342 });

使用联合类型需要对不同的类型进行不同的处理,比如说:

1
2
3
4
5
6
7
8
9
function printId(id: number | string) {
if (typeof id === "string") {
// 在这个分支中,id 的类型是 'string'
console.log(id.toUpperCase());
} else {
// 在这里,id 的类型是 'number'
console.log(id);
}
}
1
2
3
4
5
6
7
8
9
function welcomePeople(x: string[] | string) {
if (Array.isArray(x)) {
// 在这里:'x' 的类型是 'string[]'
console.log("你好," + x.join(" 和 "));
} else {
// 在这里:'x' 的类型是 'string'
console.log("欢迎,孤独旅行者 " + x);
}
}

如果一个联合类型有共同的特征,那么就可以直接的使用该特征:

1
2
3
4
// 返回类型被推断为 number[] | string
function getFirstThree(x: number[] | string) {
return x.slice(0, 3);
}

类型别名

使用关键字 type,具体示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
type Point = {
x: number;
y: number;
};

// 与前面的示例完全相同
function printCoord(pt: Point) {
console.log("x 的坐标值是 " + pt.x);
console.log("y 的坐标值是 " + pt.y);
}

printCoord({ x: 100, y: 100 });

接口

接口使用的关键字为 interface,具体示例为:

1
2
3
4
5
6
7
8
9
10
11
interface Point {
x: number;
y: number;
}

function printCoord(pt: Point) {
console.log("x 的坐标值是 " + pt.x);
console.log("y 的坐标值是 " + pt.y);
}

printCoord({ x: 100, y: 100 });

类型别名和接口非常相似,在大多数情况下你可以在它们之间自由选择。几乎所有的 interface 功能都可以在 type 中使用,关键区别在于不能重新开放类型以添加新的属性,而接口始终是可扩展的。

alt text

此外,还有以下的区别:

  • 在 TypeScript 4.2 之前,类型别名命名可能 会出现在错误消息中,有时代替等效的匿名类型(可能需要也可能不需要)。接口在错误消息中将始终被命名。
  • 类型别名不能参与声明合并,但接口可以。
  • 接口只能用于声明对象的形状,不能重命名基本类型。
  • 接口名称将始终以其原始形式出现在错误消息中,但只有在按名称使用时才会出现。

类型断言

有时可能无法准确判断出具体的类型,比如说接收到一串二进制数组,想要将他反序列化为一个指定的数据类型,那么此时就需要一个类型断言。与类型注解类似,类型断言会在编译时移除,不会影响代码的运行行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 1. 定义目标业务数据结构
interface UserProfile {
id: number;
username: string;
roles: string[];
}

// 2. 模拟反序列化函数(通常底层返回 any 或 unknown)
function deserializeBinary(buffer: ArrayBuffer): unknown {
const text = new TextDecoder().decode(buffer);
return JSON.parse(text);
}

// 3. 接收二进制数据并使用类型断言
function handleIncomingData(binaryData: ArrayBuffer) {
const rawData = deserializeBinary(binaryData);

// 方式 A:使用 `as` 语法进行类型断言(推荐)
const user = rawData as UserProfile;

// 方式 B:双重断言(当从无关类型强制转换时使用)
// const user = rawData as unknown as UserProfile;

console.log(user.id, user.username, user.roles);
}

TypeScript 只允许将类型断言为更具体或更不具体的类型。这个规则阻止了一些“不可能”的强制转换,比如:

1
const x = "hello" as number;

字面值

除了通用的 string 和 number 类型之外,我们还可以在类型位置引用特定的字符串和数字。将字面类型组合成联合类型,可以表达更有用的概念,例如,只接受一组特定已知值的函数:

1
2
3
4
5
function printText(s: string, alignment: "left" | "right" | "center") {
// ...
}
printText("Hello, world", "left");
printText("G'day, mate", "centre");

boolean 类型本身实际上只是 true | false 的联合类型的别名。

字面值推断还有如下情况,此时的 method 被推断为 string 类型,假设 handleRequest 只能接收字面值 GET 和 POST。那么就会出现报错。

1
2
3
4
5
6
7
function handleRequest(url: string, method: "GET" | "POST") {
// ...
}

// Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.
const req = { url: "https://example.com", method: "GET" };
handleRequest(req.url, req.method);

修改之后就可以解决这个问题

1
2
3
4
// 改变 1:直接将复制改为 GET 字面值
const req = { url: "https://example.com", method: "GET" as "GET" };
// 改变 2:将传入的参数改为 GET 字面值
handleRequest(req.url, req.method as "GET");
1
2
const req = { url: "https://example.com", method: "GET" } as const;
handleRequest(req.url, req.method);

as const 后缀的作用类似于 const,但是针对的是类型系统,确保所有属性都被赋予字面量类型,而不是更一般的类型,如 string 或 number。

null 和 undefined

JavaScript 有两个基本值,用于表示缺失或未初始化的值:null 和 undefined。TypeScript 也有两个相应的类型,名称相同。这些类型的特性取决于是否打开了 strictNullChecks 选项。

  • strictNullChecks 打开:如果 strictNullChecks 打开,当一个值为 null 或 undefined 时,你需要在使用该值的方法或属性之前进行检查。就像在使用可选属性之前检查 undefined 一样,我们可以使用缩小类型来检查可能为 null 的值:

还有一个非空断言操作符(后缀 !)用于在不进行任何显式检查的情况下去除类型中的 null 和 undefined。在任何表达式后面写 ! 实际上是断言该值不是 null 或 undefined:

1
2
3
4
function liveDangerously(x?: number | null) {
// 没有错误
console.log(x!.toFixed());
}

与其他类型断言一样,这不会改变你的代码的运行行为,因此只有在你知道该值不可能为 null 或 undefined 时才使用 !。

1
2
liveDangerously();      // 运行时抛出 TypeError: Cannot read properties of undefined (reading 'toFixed')
liveDangerously(null); // 运行时抛出 TypeError: Cannot read properties of null (reading 'toFixed')

类型安全

类型守卫

ts 的哲学就是在尽可能的不在 js 上新增语法,这部分类型守卫和 js 的逻辑类似,知识在相关条件判断上添加了类型分析。

1
2
3
4
5
6
7
8
9
10
11
12
function printAll(strs: string | string[] | null) {
if (typeof strs === "object") {
for (const s of strs) {
// 'strs' is possibly 'null'.
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
} else {
// 什么都不做
}
}

但是,存在有一个遗留问题,null 的类型使用 typeof 的结果是 object,无法排除 null 导致报错

1
2
let strs = null;
console.log(typeof strs); // Output: "object"

因此防御性的写法为:

1
2
3
4
5
6
7
8
9
function printAll(strs: string | string[] | null) {
if (strs && typeof strs === "object") {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
}
}

真值缩小

以下值会被强行转化为 false

  • 0
  • NaN
  • “”(空字符串)
  • 0n(bigint 版本的零)
  • null
  • undefined

此外,还可以通过等式,in,instanceof,赋值等来进行真值缩小。

在 JavaScript 中,x instanceof Foo 检查 x 的原型链是否包含 Foo.prototype

类型谓词

普通函数返回 boolean 时,TypeScript 只能知道返回了 true 或 false,但无法将这个布尔值与入参的类型关联起来。类型谓词作为函数的返回值类型:

1
2
3
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}

告诉编译器——“如果这个函数在运行时返回了 true,那么传进来的参数 pet 的类型就是 Fish”。

在面向对象的 class 内部,可以直接用 this is Type 来收窄当前实例对象的类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class FileSystemObject {
isFile(): this is FileRep {
return this instanceof FileRep;
}
isDirectory(): this is Directory {
return this instanceof Directory;
}
}

class FileRep extends FileSystemObject {
customFileMethod() {}
}

const obj: FileSystemObject = new FileRep();

if (obj.isFile()) {
obj.customFileMethod();
}

never 类型

在类型论中,never 是底类型(Bottom Type),表示永远不会发生的值的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type Shape = Circle | Square;

function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
// 到达 default 分支时,所有可能(Circle 和 Square)都已被穷尽排除
// 此时 shape 的类型被收窄为:never
// 将 never 赋值给 never 是合法的,编译完全通过!
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}

如果此时再添加一个 Triangle 类型,然后赋值给 neverm此时就会报错。

函数进阶

泛型函数

通常我们会编写一些函数,其中输入的类型与输出的类型相关联,或者两个输入的类型以某种方式相关联。让我们考虑一个返回数组的第一个元素的函数:

1
2
3
function firstElement(arr: any[]) {
return arr[0];
}

这个函数完成了它的工作,但不太好的是它的返回类型是 any。如果函数返回数组元素的类型会更好。

在 TypeScript 中,当我们想要描述两个值之间的对应关系时,我们使用泛型。我们可以在函数签名中声明类型参数:

1
2
3
function firstElement<Type>(arr: Type[]): Type | undefined {
return arr[0];
}

通过在函数中添加类型参数 Type,并在两个地方使用它,我们在函数的输入(数组)和输出(返回值)之间建立了一个链接。现在当我们调用它时,会得到更具体的类型:

1
2
3
4
5
6
// s 的类型是 'string'
const s = firstElement(["a", "b", "c"]);
// n 的类型是 'number'
const n = firstElement([1, 2, 3]);
// u 的类型是 undefined
const u = firstElement([]);

同样的,函数也会有类型推断

约束

如果我们想要操作两个参数的共同属性,或者说操作两个入参的子集,那么就需要使用到约束来限制类型参数可以接受的类型的子集。

由于我们将 Type 约束为 { length: number },我们可以访问 a 和 b 参数的 .length 属性。如果没有类型约束,我们将无法访问这些属性,因为这些值可能是没有 length 属性的其他类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function longest<Type extends { length: number }>(a: Type, b: Type) {
if (a.length >= b.length) {
return a;
} else {
return b;
}
}

// longerArray 的类型为 'number[]'
const longerArray = longest([1, 2], [1, 2, 3]);
// longerString 的类型为 'alice' | 'bob'
const longerString = longest("alice", "bob");
// 错误!数字没有 'length' 属性
const notOK = longest(10, 100);

使用受限值

1
2
3
4
5
6
7
8
9
10
11
12
function minimumLength<Type extends { length: number }>(
obj: Type,
minimum: number
): Type {
if (obj.length >= minimum) {
return obj;
} else {
// Type '{ length: number; }' is not assignable to type 'Type'.
// '{ length: number; }' is assignable to the constraint of type 'Type', but 'Type' could be instantiated with a different subtype of constraint '{ length: number; }'.
return { length: minimum };
}
}

泛型函数声明的返回值类型是 Type,这意味着函数承诺返回与调用方传入的具体子类型完全相同类型的值。

然而,return { length: minimum } 返回的仅仅是一个只有 length 属性的基础对象,它并不满足调用方传入的具体类型 Type。

良好的泛型函数指南

  1. 类型参数下推:在可能的情况下,使用类型参数本身而不是对其进行约束。也就是说,让泛型代表具体元素/内部属性,使其返回的值最好是一个具体的属性
  2. 使用较少的类型参数:也就是说各个类型参数的值应该是有所关联的,而不是相互独立的。
  3. 类型参数应该出现两次:如果一个类型参数只出现在一个位置,请仔细考虑是否真的需要它。

可选参数

我们可以在 TypeScript 中使用 ? 将参数标记为可选,这一点和 js 类似

1
2
3
4
5
function f(x?: number) {
// ...
}
f(); // 可以
f(10); // 可以

但是需要确保可选参数的缺失不会引起报错。

函数重载

具体包含若干重载签名,然后一个函数实现,函数实现必须兼容所有的重载签名。

签名:签名指的是函数的基本信息:函数名 + 入参(参数名/类型/顺序/可选性) + 出参(返回值类型)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 1. 重载签名(声明允许哪些调用方式)
function fn(): void; // 允许 0 个参数
function fn(x: string): void; // 允许 1 个字符串参数

// 2. 唯一且通用的实现函数(兼容上面所有情况,参数设为可选 x?)
function fn(x?: string): void {
if (x === undefined) {
console.log('Function called with no parameters');
} else {
console.log(`Function called with parameter: ${x}`);
}
}

// 3. 正常调用
fn(); // ✅ 输出: Function called with no parameters
fn("hello"); // ✅ 输出: Function called with parameter: hello

剩余参数

除了使用可选参数或重载来创建可以接受各种固定参数数量的函数之外,我们还可以使用剩余参数定义可以接受不确定数量实参的函数。

剩余参数位于其他参数之后,使用 … 语法:

1
2
3
4
5
function multiply(n: number, ...m: number[]) {
return m.map((x) => n * x);
}
// 'a' 的值为 [10, 20, 30, 40]
const a = multiply(10, 1, 2, 3, 4);

剩余实参

相反地,我们可以使用扩展语法从可迭代对象(例如数组)中提供可变数量的实参。例如,数组的 push 方法接受任意数量的实参:

1
2
3
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
arr1.push(...arr2);

参数解构

你可以使用参数解构将作为参数提供的对象方便地解构到函数体中的一个或多个局部变量中。在 JavaScript 中,它的样子如下:

1
2
3
4
5
6
7
8
9
10
11
// 未解构前
function sum(obj) {
return obj.a + obj.b + obj.c;
}

// 解构后
function sum({ a, b, c }) {
console.log(a + b + c);
}

sum({ a: 10, b: 3, c: 9 }); // 输出: 22

对象

可选属性

我们可以通过在属性名称末尾添加问号(?)来将这些属性标记为可选。如果读取未赋值的可选属性,返回的是 undefined。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface PaintOptions {
shape: Shape;
xPos?: number;
yPos?: number;
}

function paintShape(opts: PaintOptions) {
// ...
}

const shape = getShape();
paintShape({ shape });
paintShape({ shape, xPos: 100 });
paintShape({ shape, yPos: 100 });
paintShape({ shape, xPos: 100, yPos: 100 });

只读属性

在 TypeScript 中,属性也可以标记为 readonly。虽然在运行时不会改变任何行为,但标记为 readonly 的属性在类型检查期间无法被写入。

1
2
3
4
5
6
7
8
interface SomeType {
readonly prop: string;
}

function doSomething(obj: SomeType) {
// 我们可以读取‘obj.prop’的值。
console.log(`prop 的值为 '${obj.prop}'。`);
}

TypeScript 中的 readonly 只是一个“编译期视图约束”,而不是真正让内存中的数据变成不可变的常量(Immutable)。简单来说就是只是给这个变量加上了只读锁,可以通过多种办法来绕开只读限制,比如:

  • 别名引用(Aliasing / 借壳修改)
  • 类型断言(Type Assertion / as 强制覆写)
  • 浅层只读带来的“内部属性穿透”(Shallow Readonly)

比如下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
interface Person {
name: string;
age: number;
}

interface ReadonlyPerson {
readonly name: string;
readonly age: number;
}

let writablePerson: Person = {
name: "Person McPersonface",
age: 42,
};

// 可行
let readonlyPerson: ReadonlyPerson = writablePerson;

console.log(readonlyPerson.age); // 输出 '42'
writablePerson.age++;
console.log(readonlyPerson.age); // 输出 '43'

这是通过别名引用来绕开 readonly 的限制,将这段代码进行编译为 js 代码,我们再进行观察:

1
2
3
4
5
6
7
8
9
10
"use strict";
let writablePerson = {
name: "Person McPersonface",
age: 42,
};
// 可行
let readonlyPerson = writablePerson;
console.log(readonlyPerson.age); // 输出 '42'
writablePerson.age++;
console.log(readonlyPerson.age); // 输出 '43'

可以看到,typescript 编译为 JavaScript 之后,并没有进行限制,typescript 的限制只是在编译期间对变量进行限制,并没有在内存上给 readonly 上物理锁。

同样的在类里面,readonly 属性只能在属性声明时初始化,或者在构造函数(constructor)中赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Greeter {
// 将 info 改为一个对象(引用类型)
readonly info: { name: string } = { name: "world" };
readonly greeting: string = "Hello";

constructor(otherName?: string) {
if (otherName !== undefined) {
this.info.name = otherName; // 修改引用类型内部的属性
}
}
}

let greeter1 = new Greeter();
greeter1.greeting = "Hi"; // 修改只读属性 greeting 的值(会报错,因为 greeting 是只读的)
greeter1.info.name = "TypeScript"; // 修改引用类型内部的属性
console.log(greeter1.greeting + " " + greeter1.info.name); // 输出: Hello TypeScript

索引签名

1
2
3
4
5
6
interface StringArray {
[index: number]: string;
}

const myArray: StringArray = getStringArray();
const secondItem = myArray[1];

上面的例子中,我们有一个 StringArray 接口,它具有一个索引签名。这个索引签名表示当使用 number 值对 StringArray 进行索引时,它将返回 string 类型的值。

但是在 TypeScript 中,一旦你定义了 [key: string]: T(字符串索引签名),该对象内的所有现有显式属性、以及后续通过字符串访问的属性,其类型都必须能够赋值给 T(与 T 兼容)。

因为在 JS 中,存在有:

1
obj.age === obj["age"]

obj.age 这种点操作符访问属性,本质上在 JS 引擎里就是使用字符串 “age” 作为 Key 进行查找(即 obj[“age”])。如果定义不同类型就会产生类型错误。

解决方法有:

  1. 将索引签名的值类型定义为联合类型
1
2
3
4
5
6
7
interface UserProfile {
name: string;
age: number;
isVip: boolean;
// 兜底:所有动态新增的扩展字段可以是 string | number | boolean
[key: string]: string | number | boolean;
}
  1. 将动态属性隔离到独立的 data / extra 字段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface UserProfile {
name: string;
age: number;
// 把未知的动态键值对收敛到一个专用容器里
extraData: {
[key: string]: string;
};
}

const user: UserProfile = {
name: "Alice",
age: 25,
extraData: {
hobbies: "swimming",
city: "Beijing",
},
};
  1. 使用交叉类型(&)组合固定属性与通用字典
1
2
3
4
5
6
7
8
9
10
11
12
type BaseInfo = {
name: string;
age: number;
};

// 动态字典部分
type DynamicMap = {
[key: string]: unknown; // 或者 string | number
};

// 要求同时满足 BaseInfo 和 DynamicMap 类型
type FullUser = BaseInfo & DynamicMap;

拓展类型

在 interface 上使用 extends 关键字可以让我们有效地复制其他命名类型的成员,并添加任何我们想要的新成员。这可以减少我们必须编写的类型声明的样板代码量,并且可以表明多个对同一属性的不同声明可能相关联。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Colorful {
color: string;
}

interface Circle {
radius: number;
}

interface ColorfulCircle extends Colorful, Circle {}

const cc: ColorfulCircle = {
color: "red",
radius: 42,
};

交叉类型

在 TypeScript 中,除了使用 interface 来扩展已有类型外,还提供了另一种构造方式,称为交叉类型(intersection types),主要用于组合现有的对象类型。交叉类型使用 & 运算符进行定义。

1
2
3
4
5
6
7
8
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}

type ColorfulCircle = Colorful & Circle;

类

基础定义和 js 基本一致。

继承

implements

implements 子句来检查一个类是否满足特定的接口。如果类未能正确实现接口,将会发出错误提示。

类也可以实现多个接口,例如 class C implements A, B {。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface Pingable {
ping(): void;
}

interface Pongable {
pong():void;
}

class Sonar implements Pingable, Pongable {
ping() {
console.log("ping!");
}

pong(){
console.log("pong!")
}
}

extends

类可以从基类进行 extends 继承。派生类具有其基类的所有属性和方法,并且还可以定义额外的成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

class Animal {
move() {
console.log("继续前进!");
}
}

class Dog extends Animal {
woof(times: number) {
for (let i = 0; i < times; i++) {
console.log("汪!");
}
}
}

const d = new Dog();
// 基类方法
d.move();
// 派生类方法
d.woof(3);

此外还可以对方法进行覆写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Base {
greet() {
console.log("你好,世界!");
}
}

class Derived extends Base {
greet(name?: string) {
if (name === undefined) {
super.greet();
} else {
console.log(`你好,${name.toUpperCase()}`);
}
}
}

const d = new Derived();
d.greet();
d.greet("reader");

declare

当 target >= ES2022 或 useDefineForClassFields 为 true 时,类字段在父类构造函数完成后进行初始化,覆盖了父类设置的任何值。这在你只想为继承字段声明更准确的类型时可能会成为问题。为了处理这些情况,你可以使用 declare 来告诉 TypeScript 此字段声明不会产生运行时效果。

简单来说就是因为初始化的顺序不一致,优先从基类开始初始化,后续派生类的初始化意外的覆盖掉了基类已经初始化好了的值。通过 declare 用来跳过派生类的赋值,编译为 js 的效果就是直接抹除了 declare 声明的变量。

按照 JavaScript 的定义,类的初始化顺序如下:

  • 初始化基类字段
  • 运行基类构造函数
  • 初始化派生类字段
  • 运行派生类构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface Animal {
dateOfBirth: any;
}

interface Dog extends Animal {
breed: any;
}

class AnimalHouse {
resident: Animal;
constructor(animal: Animal) {
this.resident = animal;
}
}

class DogHouse extends AnimalHouse
// 不会生成 JavaScript 代码,
// 只是确保类型正确
declare resident: Dog;
constructor(dog: Dog) {
super(dog);
}
}

成员可见性

  • public
  • protected(自己和子类可见)
  • private
  1. private 和 protected 只在类型检查期间执行,in 或简单的属性查询这样的 JavaScript 运行时构造仍然可以访问 private 或 protected 成员
  2. private 还允许在类型检查期间使用方括号表示法进行访问。这使得对 private 声明的字段的访问在单元测试等情况下更容易,缺点是这些字段是_软私有的_,不严格执行私有化。与 TypeScript 的 private 不同,JavaScript 的私有字段(#)在编译后仍然保持私有,并且不提供先前提到的方括号访问等逃逸口,使得它们成为_硬私有_字段。

静态成员

静态成员和普通成员几乎一致,都有相同的可见性,都可以被继承。但是静态类型有特殊的名称不能被使用,由于类本身是可以使用 new 调用的函数,因此不能使用某些静态名称。诸如 name、length 和 call 的函数属性不能作为 static 成员定义。

this

在传统的 JavaScript 方法中,this 不是在定义时绑定的,而是在调用时动态决定的(运行时绑定)。

c.getName 只是一个函数指针。当你执行 obj.getName() 时,调用该函数的主体是 obj(点前面是 obj),因此函数内部的 this 自动变成了 obj。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyClass {
name = "MyClass";
getName() {
return this.name;
}
}

const c = new MyClass();
const obj = {
name: "obj",
getName: c.getName, // 把 c 的函数拿出来赋值给 obj
};

console.log(obj.getName()); // 输出 "obj"

TypeScript 提供了一些方法来减轻或防止这种类型的错误。

  • 箭头函数:箭头函数没有自己的 this,它会在实例创建时永久捕获当下的 this(即该类实例),且无法被改变。这能保证正确性,但是每次新建都要持有一份函数副本,会有较大的内存开销。

  • this 参数:在方法定义中添加 this 参数,以静态地强制执行正确的方法调用。调用可能不便,但是只用初始化一个函数实例。

this 类型

在类中,一个特殊的类型 this 动态地指向当前类的类型。

1
2
3
4
5
6
7
8
9
class Box {
contents: string = "";
set(value: string) {

(method) Box.set(value: string): this
this.contents = value;
return this;
}
}

其中 this 为 BOX 类型。下面方法中的 this 就会被推断为 DeriverBox 类型,导致 sameAs 函数失效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Box {
content: string = "";
sameAs(other: this) {
return other.content === this.content;
}
}

class DerivedBox extends Box {
otherContent: string = "?";
}

const base = new Box();
const derived = new DerivedBox();
derived.sameAs(base);
// Argument of type 'Box' is not assignable to parameter of type 'DerivedBox'.
// Property 'otherContent' is missing in type 'Box' but required in type 'DerivedBox'.