学习课程:尚硅谷TypeScript教程(李立超老师TS新课)
感谢尚硅谷及李立超老师的无私分享😘😘


TypeScrip简介

TypeScript简写是ts,是微软公司做出来的,兼容JavaScript(js),是js的超集,即ts是以js的基础上拓展构建的。它的出现是为了解决js的一些缺点。

ts特点:

  • 变量具有类型
  • 能用js的平台都能用ts,但是ts并不能直接被js解析器执行,要先编译.ts为.js才能被js解析器执行

搭建开发环境

  1. 去Node管网下载并安装Node.js
  2. 安装cnpm,使用cnpm全局安装typescript:cnpm i -g typescript
  3. 创建一个ts文件并编写ts代码
  4. 使用命令tsc xxx.ts编译上一步编写的ts代码为js,此时会生成一个js文件,如果没有右键刷新即可
  5. 注:使用WebStrom或VScode编写ts代码都可以

ts中的类型


1. 类型的声明

let a: number; // 声明数字变量a
a = 1; // 变量赋值

let b: number = 5; // 声明变量b并赋值

let c = 5; // 声明变量并赋值,初始值的类型就是变量类型
c = 'abc'; // 此时类型不一致会报错

let d1: 10; // 使用字面量赋值给d1,会自动指定类型为字面量的类型,且该变量之后不可再赋值(即这样定义的变量不能再修改了),类似于常量。
let d2: 'male'|'female'; // 限制只能给变量d2赋值'male'或'male'。
let d3: string|boolean; // 限制只能给变量d3赋值的类型
// 用|来连接多个类型的变量的类型的叫联合类型

function sum(a: number,b: number){ // 声明函数时指明形参的变量类型
	return a+b;
}

function sum(a: number,b: number): number{ // 声明函数时指明形参的变量类型,并指定函数返回值类型
	return a+b;
}

2. 类型说明

类型例子描述
number1,-33,2.5任意数字
string"hi',"hi",
booleantrue、false布尔值true或false
字面量T其本身限制变量的值就是该字面量的值
any*任意类型,该变量没有类型的限制
unknown*类型安全的any,类型不确定时尽量用unknown而尽量不用any
void空值(undefined)没有值(或undefined)
never没有值不能是任何值
object{name:'孙悟空)任意的JS对象
array[1,2,3]任意jS数组
tuple[4,5]就是固定的长度数组
enumenum{A,|B}枚举,TS中新增类型
let a; // 声明变量时没有指定变量类型,则默认该变量为any类型

// object类型表示一个js对象
let a: object;
a = {};

// 使用{}方式来指定为object类型,限定变量的对象的结构,只能给该变量的指定属性赋值且不能增减属性值
let b: {name: string, age: number};
b = {name: '孙悟空', age:18}
// 或者另一种写法,等同于上面的两行代码:
let b: {name: string} & {age: number};
b = {name: '孙悟空', age:18}

// 在对象中的属性后使用"?"表示该属性是可选的,不赋值则该对象没有该属性
let c: {name: string, age?: number};
c = {name: '孙悟空', age:18}
c = {name: '孙悟空'}

// 限定属性名的类型和属性值的类型。属性名类型一般都是string
let d: {name: string, [xxx: string]: number};
d = {name:'孙悟空', age=18, student_number:1} 
// 上面代码意思:必须要有name属性,其他属性的属性名为string类型,属性值为number类型即可

// 限定变量类型为函数,且限定函数形参类型和返回值类型
// 语法:(a: 形参类型, b: 形参类型)=>返回值类型
let e: (a: number, b: number)=>number;
e = function (n1: number, n2: number): number{
	return n1+n2;
}

// 数组类型
let f: string[];
f = ['a','ab','abc'];
// 或
let g: number[];
// 或
let g: Array<number>;

// 元组:固定长度的数组,限定元素个数
let h: [string,string,number];
h = ['a', 'ab', 5];

// 枚举,定义一个枚举类型并使用
enum Gender{
	Female=0, // 女
	Male=1 // 男
}
let i: {name: string, gender: Gender};
i = {name:"孙悟空", gender:Gender.Male};

// 类型的别名/自定义类型
type my_type1 = string;
type my_type2 = 1 | 2 | 3 | 4 | 5;

let j: my_type1; // 相当于let j: string;
let k: my_type2; // 相当于let k: 1 | 2 | 3 | 4 | 5;

3. 函数返回值类型

// 显式指明函数无返回值
function fn(): void{
}

// 显式指明函数返回值类型为string
function fn(): string{
}

// 显式指明函数返回值类型为string或number
function fn(): string | number{
}

any类型的变量可以赋值给其他任意类型,但unkown类型不能随意赋值给其他任意类型的变量,除非赋值前做类型判断,或者使用类型断言,demo如下:

let a_string: string; 
let b_any: any; 
let c_unkown: unkown; 
a_string = 'a';
b_any= 'b';
c_unkown= 'c';

a_string = b_any; // 不报错
a_string = c_unkown;  // 报错

// 类型判断,如果a_string这个变量的类型是string类型
if(typeof c_unkown === "string"){
	a_string = c_unkown;
}

// 也可以使用"类型断言"来实现上面的功能;
a_string = c_unkown as string;
// 或
a_string = <string>c_unkown;

从其他文件导入变量/从其他模块导入变量:import { hi } from './m.js'


ts编译选项/ts配置文件


创建ts配置文件

关键字:typescript的配置文件/TypeScript的配置文件详解

  • 单个文件自动编译(了解即可):tsc file_name.ts -w -w(watch),执行之后会编译且编译器会一直监听当前文件的变化,有变化会自动编译
  • 当前目录下所有文件自动编译:
    1. 在项目目录新建文件"tsconfig.json",并在里面写入空对象{}
    2. 在当前目录下执行命令tsc,该目录内的所有文件都会被编译
    3. 执行tsc -w,编译且监听该目录内所有ts文件自动编译

tsconfig.json配置详解

{
	/*
	include: 包含在此列表中目录的.ts文件都要编译,**表示任意目录,*表示任意文件
	'./src/**/*' 代表配置文件目录下的src目录下的任意目录下的任意文件,都要被编译,src下的文件也会被编译。如果指定了,则该目录必须要存在,否则会报错。比如这里这么配置后,如果src目录不存在,则会编译报错

	exclude: 排除在外的,不需要被编译的目录。exclude的默认值: ["node_modules","bower_commponents","jspm_packages"]

	extends: 定义被继承的配置文件,即本文件配置。
	"extends": "./configs/base"表示本配置文件会继承./configs/base.json中的配置

	files: 要编译的文件列表

	compilerOptions: 编译器的选项
		target: 用来指定.ts被编译为的ES版本,默认是ES3。也可以写ES2017,ES2020,ESNext(最新版),如果需要target的可选列表,随便写一个错误值进行编译,报错会提醒并给出可选列表
		module:  指定要使用的模块化规范
		lib: 库,一般不用改,比如需要dom操作需要写["dom",]才能写document.xxx
		outDir: 指定编译后生成的文件的目录
		outFile: 设置该配置后,所有全局作用域中的代码都会合并到同一个文件中。即指定编译时把所有文件都合并到一个文件中的文件路径。本配置用的少,了解即可。
		allowJs: 默认值是false。是否对.js进行编译,是的话,会把.js文件编译,并在outDir指定的编译文件夹生成编译后的.js文件
		checkJs: 默认值是false。是否检查.js文件的语法是否符合.ts的语法规则,
		removeComments: 默认值是false。编译时是否移除注释
		noEmit: 默认值是false。编译时不编译后的.js文件
		noEmitOnError: 默认值是false。当有错误时不生成编译后的文件
		alwaysStrict: 默认值是false。用来设置编译后的文件是否使用严格模式,true的话会自动在编译后的文件首行添加一行: "use strict";代表开启严格模式,js的语法模式更严格,性能更好
		noImplicitAny: 默认值是false。是否禁止出现隐式的any类型(默认定义一个变量如果没有设置类型,则默认该变量为any类型,即定义了一个隐式类型为any的变量,如果本配置为true,则禁止定义隐式类型为any的变量)
		noImplicitThis: 默认值是false。是否禁止出现不明确类型的this
		strictNullChecks: 默认值是false。是否严格检查空值,即是否严格检查调用对象是否是空值,如果为true,则编译报错。应该先用if检查对象是否为空再对对象操作,或比如使用obj?.addEventListener(...)的方式,如果obj对象不为空,则添加事件监听器或其他调用
		strict:  相当于所有严格检查的配置项的总开关,如果为true,则alwaysStrict、noImplicitAny、noImplicitThis、strictNullChecks都会开启,一般设置为true
	*/
	"include": [
		"./src/**/*",
	],
	"exclude": [
	],
	"extends": "./configs/base",
	"files": [
		"a.ts",
		"b.ts",
	],
	"compilerOptions": {
		"target": "ES6",
		"module": "",
		"lib": ["dom"],
		"outDir": "./dist",
		"outFile": "./xxx/xx.js",
		"allowJs": true,
		"checkJs": true,
		"removeComments": true,
		"noEmit": true,
		"noEmitOnError": true,
		"strict": true,
		"alwaysStrict": true,
		"noImplicitAny": true,
		"noImplicitThis": true,
		"strictNullChecks": true,
	}

}

使用webpack打包ts代码


使用webpack初始化配置

一般大型项目中,不会直接使用编译命令对项目编译,而是通过webpack对项目进行编译打包。以下是一个demo

  1. 新建一个空项目
  2. 在空项目路径下执行cnpm init -y,初始化项目,生成"package.json"
  3. cnpm i -D webpack webpack-cli typescript ts-loader // i:install -D:--save-dev(即现在安装的依赖是开发用的依赖) ts-loader:webpack加载器,用来整合webpack和ts的,装了它才能在webpack中使用ts
  4. 安装成功后,第二步生成的"package.json"文件中的依赖项列表(devDependencies)就会新增第三步安装的依赖
  5. 编写webpack配置文件"webpack.config.js"
// 引入一个包
const path = require("path")

//webpack中的所有配置信息都应写在module.exports中
module.exports = {
	entry: "./src/index.ts", // 指定入口文件,即项目的主文件,从该文件开始执行,该文件和文件名都是随意自定义的
	output: { // 指定打包文件所在的目录
		path: path.resolve(__dirname, 'dist'), // 指定路径,值是拼接成完路径,等同于项目目录下的dist的绝对路径的全路径
		filename: "bundle.js", // 定义打包后的文件的文件名,任意自定义的文件名都可以
		enviroment: {
			arrowFunction: false, // 告诉webpack不使用箭头函数,因为ie不支持箭头函数
			const: false, // 告诉webpack不使用const关键字,因为ie10不支持const关键字
		}
	},
	module: { // 指定webpack打包时要使用的模块
		rules: [ // 指定要加载的规则
			{
				test: /\.ts$/,// test指定的是规则生效的文件,此处表示所有.ts结尾的文件
				use: 'ts-loader', // 指定用什么loader来处理test的文件。(此处即使用ts-loader来处理所有的.ts文件)
				exclude: /node-modules/ //排除要被use的loader处理的文件目录,此处配置即该目录下不会被ts-loader编译
			}
		]
	}
} // 如果报错,查看项目目录中是否有该包,有的话是否IDE已经识别,如果没有识别就reload一下当前目录,还没有就重启IDE
  1. 编写ts的编译文件
{
	"compilerOptions": {
		"module": "ES2015",
		"target": "ES2015", // 等同于ES6
		"strict": true,
	},
}
  1. 修改根目录下的"package.json",在根属性scripts中添加属性build:"webpack"
  2. 执行cnpm run build编译并打包项目,打包成功后会生成dist目录

安装webpack插件


html-webpack-plugin

插件作用:自动将编译好的js文件引入html

  1. cnpm i -D html-webpack-plugin
  2. 在"webpack.config.js"配置第一步安装的插件,在该文件中添加:
const HTMLWebpackPlugin(变量名任意自定义) = require("html-webpack-plugin")
  1. 在webpack.config.js中的module.exports对象中添加属性:
plugins: [
	new HTMLWebpackPlugin(), // 第二步定义的变量名
]

新建对象也可以写为:

new HTMLWebpackPlugin({
	title: "我的title", // 指定编译后生成的html的title
	template: "./src/index.html", // 指定html模板
})
  1. 编译并打包:cnpm run build

webpack-dev-server

插件作用:在项目里安装一个内置服务器,如果修改了代码,可以试试更新在浏览器中

  1. cnpm i -D webpack-dev-server
  2. 修改根目录下的"package.json",在根属性scripts中添加属性"start":"webpack serve --open chrome.exe",意思是启动webpack serve,并用chrome.exe打开网页
  3. cnpm start
  4. 此时如果代码有修改。浏览器中就会实时更新

clean-webpack-plugin

插件作用: 编译时自动清除dist(编译文件目录)内的文件,使得编译目录内的文件都是最新的

  1. cnpm i -D clean-webpack-plugin
  2. 和html-webpack-plugin配置一样
  3. 在"webpack.config.js"中添加:
const {CleanWebpackPlugin}  = require("clean-webpack-plugin")
  1. 在webpack.config.js中的module.exports.plugins属性的值列表中添加对象new CleanWebpackPlugin():
plugins: [
	new CleanWebpackPlugin(), // 第二步定义的变量名
]
  1. 编译并打包:cnpm run build时,会先清空编译目录再生成编译后文件

babel

插件作用:

  1. 把新语法转换成旧语法
  2. 让新的技术,兼容旧的浏览器,解决兼容性问题
    cnpm i -D @babel/core @bable/preset-env bablel-loader core-js//preset-env:预先设置的环境 core-js:js的一个运行环境
  3. 安装完成后,去"package.json"检查是否安装完成,完成的话会在devDependencies下有相关的依赖名称
  4. 在"webpack.config.js"的module.rules.use的值(列表,如果不是列表则手动改为列表)中添加值
{// 配置babel的对象
	loader: "babel-loader", // 指定加载器
	options: { // babel的配置
		preset: [
			"@babel/preset-env", // 指定环境的插件
			{
				tagets: { // 设置要兼容的浏览器
					"chrome": "88", // 向下兼容到chrome的第88个版本
					"ie": "11", 
				},
				"corejs": "3", // 指定corejs的版本
				"useBuiltIns": "usage" // 使用corejs的方式,"usage"表示按需加载,用到那些代码,编译后才放进去,使用该方式能保证编译后的文件大小是最小的
			}
		]
	}
}

注意:需要将'ts-loader'放在列表的最末尾,因为代码执行是从列表最后面往前面执行,ts-loader先将ts转为js代码后,在通过前面的babel-loader将代码转为其他tagets中指定的兼容版本(可能是旧版本)的代码


模块定义与引入

如果在变量前使用export关键字,则其他文件可以引入该文件内用export关键字装饰的变量,
比如m1.js内容为export const hi = 'abc';其他文件可以import \{ hi \} from './m1.js',但是这么直接使用会报错,需要在"webpack.config.js"添加根属性

resolve: {
	extensions: ['.ts', '.js'] // 凡是扩展名为该列表中的文件,都可以作为模块使用
}

面向对象


类的定义和使用

类的结构

class 类名{
	属性名: 类型;
	constructor(){ // 构造方法
	}
}

定义类,并通过类创建对象

(function(){
	class Person{
		// 定义实例属性。即实例化后,通过实例的对象.属性才可访问。如果是Person.属性就能访问叫静态属性
		name: string;

		// 定义 类属性/静态属性。使用static修饰的属性,只能通过类名.属性名访问
		static age: number = 18;
		
		// 定义只读属性,不能再次给只读属性再次赋值
		readonly hair_color: string = "black";

		static readonly hair_color: string = "black";

		name = "孙悟空" // 让它自动辨别属性


		// 构造函数和this,构造函数会在创建对象时执行
		constructor(){ 
			this.name = "猪八戒";
		}

		// 有参构造函数
		constructor(name: string, age: number){ 
			// this.name = "猪八戒";
			this.name = name;
			this.age = age;
		}

		sayHello(){ // 定义 实例方法
			console.log("hello!");
		}

		static sayHello(){ // 定义 静态方法/类方法
			console.log("hello!");
		}

	}
})();

类的继承

(function(){
	class Student extends Person{ // 继承Person类,子类Student类会拥有Person所有方法和属性
		// 如果在子类中写构造函数,则必须super.调用父类的构造函数,否则会报错
		learning(){
			console.log("学习中");
		}

		sayHello(){ // 子类的方法和父类一样,则覆盖父类的方法,只运行子类的这个方法,不运行父类的方法
			console.log("hello!我是学生");
			super.sayHello(); // super代表当前类的父类。即此处的super=Person这个类
		}


	}

	class Teacher extends Person{
		
	}

	const person = new Person(); // 创建对象
})();

抽象类和抽象方法

  1. 抽象类:如果不希望一个类被创建对象,则使用abtract来修饰该类,则该类就是抽象类,不能被实例化,比如:abtract class Person{}。它是专门用来被继承的。
  2. 抽象方法:在抽象类中可以定义抽象方法(使用abtract来修饰的方法),且抽象方法只能被定义在抽象类中,如果继承了抽象类,则抽象类中的抽象方法必须被重写。抽象方法的写法:abstract sayHello():void;抽象方法没有方法体,即抽象方法内是空的没有任何代码执行,只有在子类实现时才写方法体。

接口的定义与使用

接口:接口是用来定义(或限制)一个类的结构(包含哪些属性和方法),同时也可以当成类型声明去使用。但是接口可以有重名的,如果接口重名,则会自动合并成一个,属性都会合并到一起。如果在接口里写方法,也是不能有方法体,即接口中的所有属性都不能有实际的值,只能定义对象的结构。接口编译成js后就没了,不会保留在js文件中

(function(){
	interface myInterface{
		name: string,
		age: number
	}
	// 如果有重名,最后接口的属性就是name、age、gender
	// interface myInterface{
	// 	gender: string
	// }
	const obj: myInterface = {
		name: "孙悟空",
		age: 18
	}


	// 效果和下面是一样的

	type myType = {
		name: string,
		age: number
	}
	const obj: myType = {
		name: "孙悟空",
		age: 18
	}

	// 接口demo,定义一个接口,并实现该接口:
	interface myInter{
		name: string;
		sayHello(): void;
	}

	class MyClass impletments myInter{
		name: string;
		costructor(name:string){
			this.name = name;
		}
		sayHello(){
			console.log("大家好");
		};
	}
})();

接口和抽象类的区别

接口实现"有没有"的问题,抽象类是实现"必须有"的问题,比如必须有吃饭睡觉忠诚的才叫狗狗,但有的狗狗可以有算数的技能这就可以用接口来实现。所以会算数的狗狗可以继承"必须有"的特点(即继承抽象类),然后实现"有没有"的特点(有没有算数技能)。这就是区别

(function(){
	abstract class Dog{
		abstract eat();
		abstract sleep();
		abstract 忠诚();
	}

	interface 算数技能{
		算数();
	}

	class 会算数的狗 extends Dog impletments 算数技能{ // 必须有Dog的特点,而且可以有算数的技能。单继承多实现,共有的特点使用继承,特有的特点使用接口,此时即拥有了算数,又拥有了其他狗狗共有的特点
		eat();
		sleep();
		忠诚();
		算数();
	}
})();

封装

封装:对类属性的封装。用于禁止对象/类外部直接访问对象的属性。
做法:给属性前面使用private修饰,然后用get、set方法访问(get、set方法也被称为属性存储器),如果不想属性被赋值就取消set方法就行了,获取属性值同理。
封装修饰词(权限同java):public、protect(只能在当前类或子类中使用。比如父类属性使用protect,子类可以访问,但不能通过实例.访问)、private。

(function(){
	class Person{
		private name: string;
		private age: number;
		getName(){
			return this.name;
		}
		setName(value: string){
			this.name = value;
		}

		getAge(){
			return this.age;
		}
		setAge(value: number){
			if(value>=0){
				this.age = value;
			}
		}

		// ts中设置getter方法的方式: get 属性名(){}
		get name(){ // 使用此方式外部要调用时,使用"对象.name"就可以调用此方法了
			return this.name
		}
		set name(value: string){ // 同上
			this.name = value;
		}
	}

	class A{
		constructor(public name: string, public age: number){
		// 可以把属性写在构造函数中,这样等同于单独把属性写出来
		}
	}
	// 等价于下面class A
	class A{
		name: string;
		age: number;
		constructor(name: string, age: number){
			this.name=name;
			this.age=age;
		}
	}
	const a = new A("xx",11);
})();

泛型

泛型:在定义函数或类时,如果遇到类型不明确的时候使用泛型

(function(){
	function fn<T>(a: T): T{// 定义了一个泛型T(可以任意字母),然后参数是T,返回值也和参数相同也是T。
		return a;
	}
	// 调用带有泛型的函数
	fn(10); // 不指定泛型时,ts自动识别泛型类型
	fn<string>("abc"); // 手动指定泛型类型

	// 指定多个泛型
	function fn2<T,K>(a: T, b: K): T{
		return a;
	}
	fn2<string, number>("abc");

	interface Inter{
		length: number;
	}
	function fn3<T extends Inter>(a: T){// 定义了一个泛型,且规定该泛型必须实现/继承Inter这个接口/类,即规定了a参数必须有length这个属性
		return a.length;
	}
})();

项目实战:贪吃蛇


搭建开发环境

  1. 使用WebStorm创建一个空项目
  2. 复制"package.json"、"tsconfig.json"、"webpack.config.js"到项目的根目录后(这几个文件在上面有讲到),在package.json中修改项目名,即修改name的值。
  3. 执行npm i安装配置文件中的依赖(如果用cnpm i安装很久,就使用npm i,李立超老师说在WebStorm中尽量避免使用cnpm),依赖都会安装在根目录的node_modules目录中
  4. 在项目根目录中,创建文件"./src/index.html"、"./src/index.ts"
  5. 在"index.ts"中写console.log("123"),执行npm run build来编译测试是否正常,没错误的话会在./dist目录下生成编译后的文件
  6. 安装less:npm i -D less less-loader css-loader style-loader,css-loader是用来处理css的代码的,style-loader是用来将css代码引入到项目中的
  7. 安装postcss:npm i -D postcss postcss-loader postcss-preset-env // 实现对css代码版本的控制,以及css对浏览器兼容的设置
  8. 在"webpack.config.js"中的rules列表中添加一个对象,用于配置对less文件的处理
{
	test: /\.less$/,
	use: [
		"style-loader",
		"css-loader",
		{
			loader: "postcss-loader",
			options: {
				postcssOptions: {
					plugins: [
						[
							"postcss-preset-env",
							{
								browers: "last 2 version" // 兼容2个最新版本
							}
						]
					]
				}
			}
		},
		"less-loader",
	]
}
  1. 新建文件"./src/style/index.less",写入内容body\{background-color:green;\}
  2. "index.ts"中引入less文件import "./src/style/index.less"
  3. 使用npm start编译项目并测试

编写"分数面板"类

创建文件"./src/moduls/ScorePanel.ts"

class ScorePanel{
	score = 0;
	scoreEle: HTMLElement;
	maxLevel: number;

	...
	
	constructor(maxLevel: number = 10){// 给形参设置默认值
		this.maxLevel = maxLevel;
	}

	addScore(){
		this.score++;
		this.scoreEle.innerHTML = this.score + ""; // 给元素赋值
	}
}

编写"食物"类

创建文件"./src/moduls/Food.ts"

class Food{
	element: HTMLElement
	constructor(){
		this.element = document.getElementById("food")!; // 最后的"!"是告诉编译器获取的该元素不可能为空,否则编译器会因为获取不到该元素,然后报错
	}
	// 定义获取食物X坐标的方法
	get X(){
		return this.element.offsetLeft;
	}
	// 定义获取食物Y坐标的方法
	get Y(){
		return this.element.offsetTop;
	}
	change(){
		this.element.style.left = "80px"; // 修改x坐标,移动食物位置
		this.element.style.top = "80px"; // 修改y坐标
	}
}
export default Food; // 最后一行把Food类作为默认模块暴露出去,外部的其他文件就可以通过`import Food from './moduls/Food';`的方式来引用Food类了。
// export default的用法在别的文章有详解,搜索即可

编写"蛇"类

创建文件"./src/moduls/Snacke.ts"

class Snacke{
	X: number;
	Y: number;
	head: HTMLElement; // 蛇头
	bodies: HTMLCollection; // 蛇身(好多div)
	element: HTMLElement; // 蛇的容器(包含好多div的那个容器)

	constructor(){
		this.element = document.getElementById("snake")!;
		this.head = document.querySelector("#snake > div") as HTMLElement; // 选择id为snake元素中的第一个div标签,且该标签必须是一个HTMLElement
		this.bodies = element.getElementByTagName("div");
	}

	addBody(){
		this.element.insertAdjacentHTML("beforend","<div></div>"); // 在element元素标签的结束之前,添加一个div(即在element之中的最后添加一个div元素)
	}
	set X(value: number){
		if(value<0 || value>290){
			throw new Error("蛇撞墙了"); // 创建异常并抛出异常
		}

		// 在别的文件中捕获异常:
		// try{
		// 	this.snake.X = X;
		// }catch (e){
		// 	alert(e.message);
		// }
	}
	...
	moveBody(){
		// 每刷新一次时,将最后一节身体设置为和前一节一样,蛇就移动起来了
		for(let i=this.bodies.lenth-1; i>0; i--){
			// 获取前一节身体的位置并赋值给当前一节
			lastBodyElement = this.bodies[i-1] as HTMLElement;
			thisBodyElement = this.bodies[i] as HTMLElement;
			let X = lastBodyElement.offsetLeft;
			let Y = lastBodyElement.offsetTop;
			thisBodyElement.style.left = X + "px";
			thisBodyElement.style.top = Y + "px";


		}
	}
}

编写"游戏控制"类

创建文件"./src/moduls/GameControl.ts"

class GameControl{
	isLive = true;
	...
	keydownHandler(event: KeyBoardEvent){
		console.log(event.key);
	}
	init(){
		document.addEventListener("keydown", this.keydownHandler.bind(this)); 
		// 绑定事件-绑定按钮按下时的监听事件。
		// 注意:keydownHandler中使用this时,this指的是document,因为是document调用的keydownHandler,所以如果直接传keydownHandler,在keydownHandler中是不能通过this获取到GameControl这个对象的。
		// 解决方法:使用bind创建一个新的keydownHandler给addEventListener当回调使用,再将当前对象作为this传给新建的keydownHandler,所以这个新的keydownHandler就可以使用当前对象作为this
	}
	run(){ // 让蛇跑起来的方法
		// 修改XY坐标
		isLive && setTimeout(this.run.bind(this), 300); // 如果没有死,就定时每300毫秒执行一下当前对象的run方法
	}
	...
}

Q.E.D.


做一个热爱生活的人