尚硅谷TypeScript教程(李立超老师)学习笔记

一、TypeScript简介

1、TypeScript是以JavaScript为基础构造的语言,是JavaScript的超集

2、扩展了JavaScript,并添加了类型。

3、TS代码需要通过编译器编译为JS,然后再交由JS解析器执行。

4、TS完全兼容JS,任何的JS代码都可以直接当成JS使用。

5、相较于JS而言,TS拥有了静态类型,更加严格的语法,更加强大的功能;

     TS可以在代码执行前就完成代码的检查,减小了运行时异常的的出现的几率;

    TS代码可以编译为任意版本的JS代码,可有效解决不同JS运行环境的兼容问题;

    同样的功能,TS的代码量要大于JS,但由于TS的代码结构更加清晰,变量类型更加明确,在后期代码的维护中TS远胜于JS。

二、TypeScript开发环境搭建

1、下载Node.js

64位:https://nodejs.org/en/

2、安装Node.js

安装路径尽量是纯英文的,不要出现特殊符号或中文。

一直点击Next,进行安装。

安装完成后,win+R打开cmd(或使用PowerShall),输入node-v,如果出现版本号,则说明安装成功。

3、使用npm全局安装TypeScript

输入:npm i -g typescript

npm是node的包管理器。

安装完成后,可在命令行输入tsc,检测typescript安装成功。

 4、创建一个ts文件

在实际开发过程中,命名最后以字母开头,不要以数字开头。

  • code/chapter01/01_helloTS.ts
console.log('Hello TS');

5、使用tsc对ts文件进行编译

在文件夹路径栏中输入cmd打开命令行。

在命令行中输入tsc+文件名对该文件进行编译。

完成后在该文件夹下会出现一个01_helloTS.js文件,即编译器已经将ts代码编译为js代码。

 

 二、TS的基本类型

1、类型声明

(1)类型声明是TS中一个非常重要的特点,通过类型声明可以指定TS中变量(参数、形参)的类型。

(2)指定类型后,当为变量赋值时,TS编译器会自动检查值是否符合类型声明,符合则复制,否则报错。

(3)类型的声明给变量设置了类型,使得变量只能存储某种类型的值,而不能随意存储。

//声明一个变量a,同时指定它的类型为number
let a: number;

// a的类型设置为了number,在以后的使用过程中,a的值只能是number
a = 10;
a= 'a'; 

 

// 变量类型声明

// 先声明,在赋值
let 变量名: 类型;
变量名 = 值;

// 声明完变量直接赋值
let 变量名: 类型 = 值;

// 其实,如果变量的声明和赋值是同时进行的,TS可以自动对变量进行类型检测,此后只能对变量赋第一次赋值时的类型的值
let 变量名 = 值;

(4)TS拥有自动的类型判断机制:当对变量的声明和赋值是同时进行的,TS编译器会自动判断变量的类型。

         所以如果变量的声明和赋值是同时进行的,可省略类型声明。

2、TS中的类型

类型例子描述
number1,-33,2.5任意数字
string“hi”,“hi”,hi任意字符串
booleantrue,false布尔值true或false
字面量其本身限制变量的值就是该字面量的值
any*任意类型
unknown*类型安全的any
void空值(undefined)没有值(或undefined)
never没有值不能是任何值
object{name:‘孙悟空’}任意的JS对象
array[1,2,3]任意JS数组
tuple[4,5]元素,TS新增类型,固定长度数组
enumenum{A,B}枚举,TS中新增类型

(1)字面量

字面量类似于常量,只能赋一次值,不能再修改了。

let a: 10;
a = 10;  // 这是可以的
a = 11;  // 这会报错

可以使用 | 来连接多个类型,又称为联合类型。可以用来限制变量赋值在某几个值之间:

// 可以使用 | 来连接多个类型,又称为联合类型。用来限制变量在某几个值之间。
let b: "male" | "female";
b = "male";  
b = "female";  // b = "male"或b = "female"都可以,不会报错。
b = "hello";  // b不能赋值为hello。只能赋值为male或female。

注:| 不仅可以连接字面量,也可以连接其他类型作为联合类型,如:

let c: boolean | string;  // c既能被赋值为布尔值,也能被赋值为字符串。
c = true;
c = "hello";

(2)any

① any 表示的是任意类型,一个变量设置为类型any后相当于对该变量关闭了TS的类型检测。在TS中不建议使用any类型。

② 声明变量时如果不指定类型,则TS解析器会自动判断变量的类型为any(隐式的any)。

一个类型为any的变量,可以赋值给任意变量,且不会报错。

let d : any;
// let d;    (隐式的any)
d = 10;
d = 'hello';
d = true;

(3)unknown

unknown表示未知类型的值。

与any的区别:一个类型为unknown的变量,赋值给其他类型变量时编译器会报错。

一个类型为unknown的变量,不能直接赋值给其他变量。

类型断言

  • 类型断言,字面理解就是确定变量的数据类型。
  • 有些情况下,变量的类型对于我们来说是很明确,但是TS编译器却并不清楚,此时,可以通过类型断言来告诉编译器变量的类型,断言有两种形式:
//类型断言,可以用来告诉解析器变量的实际类型
//语法一: 变量名 as 类型
s = e as string;
//语法二: <类型>变量名
s = <string>e

(4) void

void 用来表示空,用作函数的返回值类型,表示函数没有返回值。除了将void类型作为函数返回值类型外,在其他地方使用void类型是无意义的。

function fn():void{
    
}

(5)never

never 表示永远不会返回结果。

never类型可以作为函数的返回值类型,表示该函数无法返回一个值。

function fn2():never{
    throw new Error('报错了!!!');
    //该函数永远无法执行到末尾,返回值类型为"never"
}

(6)object

object 表示一个js对象。在开发时一般不使用,因为在JS中一切皆为对象。

在实际开发中,我们更想限制的是一个对象中包含的属性,而不是限制它是不是对象。

//object表示一个js对象
//{}用来指定对象中可以包含哪些属性
//语法:  必须赋值的属性 {属性名:属性值,属性名:属性值}
let a: { name: string, age: number }
a = {name:'孙悟空', age : 30};

// ?:表示属性是可选的,即在赋值时,可以不对该属性进行赋值
let b :{name:string,age?:number}
b = {name:'猪八戒'}

假如我们要设置这样一个对象,只要求其必须具有name属性,其他属性可以随意添加。

语法:[propName:string]:any

let c : {name:string,[propName:string]:any},
c={name:'沙和尚',age:18,gender:'男'}

此外,{ } 还可以用于设置函数结构的类型声明。

语法:{形参:类型,形参:类型,...} => 返回值

// 设置函数结构的类型声明:
// 语法:(形参:类型,形参:类型 ...) => 返回值
let d (a:number,b:number):=>number 
// d = function (n1:number,n2:number) :number {
//     return 10
// }

(7)array

string[  ]:表示字符串数字

number:[ ] 表示数值数组

//string[]表示字符串数组
let e :string[];
e = ['a','b','c'];
//number[]表示数值数组
let f = number[];

 还可以这样声明数组:Array<number>

let g :Array<number>;    //相当于let g : number[]
g = [1,2,3]

(8)tuple

元组,就是固定长度的数组,即其中的元素个数是固定的。

语法:[类型,类型...]

let h :[string,number];
h = ['a',10]

(9)enum枚举

将所有可能的情况一个个列出来。

/enum枚举
enum Gender {
    male = 0,
    female = 1
}
let i : {name:string,gender:0|1};
i = {
    name:'孙悟空',
    gender:Gender.male
}
console.log(i.gender === Gender.male);   //运行后输出true

除了用 | 连接两个类型以外,还可以用 & 进行连接,表示该变量要同时具有用&连接的所有类型的属性。

/ &表示同时
let j :{name:string} & {age:number};
j = {name:'孙悟空',age: 30}

(10)类型的别名

可以简化类型的使用

type myType =  1 | 2 | 3 | 4 | 5;
let k: myType;
k = 1;
k = 5;

3、TS编译选项

3.1 自动编译文件

(1)编译文件时,使用 -w 指令后,TS编译器会自动监视文件的变化,并在文件发生变化时对文件进行重新编译。

tsc app.ts -w

 缺点:一个文件就要开一个窗口

3.2 自动编译整个项目

如果直接使用tsc指令,则可以自动将当前项目下的所有ts文件编译为js文件。

但是能直接使用tsc命令的前提时,要先在项目根目录下创建一个ts的配置文件 tsconfig.json

具体操作如下:(VSCode)

(1) 在文件目录下进行cmd命令行,输入命令:

tsc --init

 文件夹中将自动生成tsconfig.json文件

(2) 配置选项:

① include

指定所需要编译的文件

默认值:["**/*"]

"include":["src/**/*", "tests/**/*"]
//所有src目录和tests目录下的文件都会被编译

② exclude

定义需要排除在外的目录

默认值:[“node_modules”, “bower_components”, “jspm_packages”]

"exclude": ["./src/hello/**/*"]
//src下hello目录下的文件都不会被编译

③ extends

定义被继承的配置文件

"extends": "./configs/base"
//当前配置文件中会自动包含config目录下base.json中的所有配置信息

④ files

指定被编译文件的列表,只有需要编译的文件少时才会用到。

//列表中的文件都会被TS编译器所编译
"files": [
    "core.ts",
    "sys.ts",
    "types.ts",
    "scanner.ts",
    "parser.ts",
    "utilities.ts",
    "binder.ts",
    "checker.ts",
    "tsc.ts"
  ]

⑤ compilerOptions

编译选项是配置文件中非常重要也比较复杂的配置选项,在compilerOptions中包含多个子选项,用来完成对编译的配置。

target

  • 设置ts代码编译的目标版本
  • 可选值:ES3(默认)、ES5、ES6/ES2015、ES7/ES2016、ES2017、ES2018、    ES2019、ES2020、ESNext。
  • 示例
    "compilerOptions": {
        "target": "ES6"
    }
    
    //所编写的ts代码将会被编译为ES6版本的js代码

lib

  • 指定代码运行时所包含的库(宿主环境)
  • 可选值:ES5、ES6/ES2015、ES7/ES2016、ES2017、ES2018、ES2019、ES2020、ESNext、DOM、WebWorker、ScriptHost …
  • 示例:
    "compilerOptions": {
        "target": "ES6",
        "lib": ["ES6", "DOM"],
        "outDir": "dist",
        "outFile": "dist/aa.js"
    }
    

module

  • 设置编译后代码使用的模块化系统。
  • 可选值:CommonJS、UMD、AMD、System、ES2020、ESNext、None。
  • 示例
    "compilerOptions": {
        "module": "CommonJS"
    }
    

outDir

  • 编译后文件的所在目录。
  • 默认情况下,编译后的js文件会和ts文件位于相同的目录,设置outDir后可以改变编译后文件的位置。
  • 示例:
//设置后编译后的js文件将会生成到dist目录
"compilerOptions": {
    "outDir": "dist"
}

outFile

  • 将所有的文件编译为一个js文件。
  • 默认会将所有的编写在全局作用域中的代码合并为一个js文件,如果module制定了None、System或AMD则会将模块一起合并到文件之中。
  • 示例:
"compilerOptions": {
    "outFile": "dist/app.js"
}

rootDir

  • 指定代码的根目录,默认情况下编译后文件的目录结构会以最长的公共目录为根目录,通过rootDir可以手动指定根目录。
  • 示例:
"compilerOptions": {
    "rootDir": "./src"
}

allowJs

  • 是否对js文件编译。

 checkJs

  • 是否对js文件进行检查。
  • 示例:
"compilerOptions": {
    "allowJs": true,
    "checkJs": true
}

removeComments

  • 是否删除注释。
  • 默认值:false

 noEmit

  • 不对代码进行编译。
  • 默认值:false。

 sourceMap

  • 是否生成sourceMap。
  • 默认值:false。

⑥ 严格检查

strict:启用所有的严格检查,默认值为true,设置后相当于开启了所有的严格检查。

alwaysStrict:总是以严格模式对代码进行编译。

noImplicitAny:禁止隐式的any类型。

noImplicitThis:禁止类型不明确的this。

strictBindCallApply:严格检查bind、call和apply的参数列表。

strictFunctionTypes:严格检查函数的类型。

strictNullChecks:严格的空值检查。

strictPropertyInitialization:严格检查属性是否初始化。

⑦ 额外检查

noFallthroughCasesInSwitch:检查switch语句包含正确的break。

noImplicitReturns:检查函数没有隐式的返回值。

noUnusedLocals:检查未使用的局部变量。

noUnusedParameters:检查未使用的参数。

⑧ 高级

allowUnreachableCode

  • 检查不可达代码。
  • 可选值:true,忽略不可达代码。 false,不可达代码将引起错误。  

noEmitOnError

  • 有错误的情况下不进行编译。
  • 默认值:false。

 三、使用webpack打包ts代码

通常情况下,实际开发中我们都需要使用构件工具对代码进行打包,TS同样也可以结合构件工具一起使用,下边以webpack为例介绍一下如何结合构件工具使用TS。

(1)初始化项目

创建一个新的文件夹

进入项目根目录,执行命令 npm init - y,初始化一个项目并创建package.json文件。

使用tsc --init创建ts的配置未见tsconfig.json。

创建src/index.ts文件,用来编写ts代码

(2)下载构建工具

npm i -D webpack webpack-cli webpack-dev-server typerscript ts-loader html-webpack-plugin clean-webpack-plugin

 一共安装了七个包:

  • webpack:构件工具webpack
  • webpack-cli:webpack的命令行工具
  • webpack-dev-server:webpack的开发服务器
  • typescript:ts编译器
  • ts-loader:ts加载器,用于在webpack中编译ts文件
  • html-webpack-plugin:webpack中html插件,用来自动创建html文件
  • clean-webpack-plugin:webpack中的清除插件,每次构建都会先清除目录

(3)在根目录下配置webpack.config.js

//引入一个包
const path = require('path');

module.exports = {
    //入口(相对路径)
    entry:"./src/index.ts",
    //输出
    output:{
        //指定打包文件所在目录
        path:path.resolve(__dirname,"dist"),
        //打包后文件的名字
        filename:'bundle.js'
    },
    //指定webpack打包时要使用的模块
    module:{
        //loader的配置
        rules:[
            {
            //test指定规则生效的文件
            //哟昂莱匹配.ts结尾的文件
            test:/\.ts$/,
            use:'ts-loader',
            // 排除node_modules代码不编译
            exclude:/node-modules/
        }
    ],
    },

}

(4)编写tsconfig.json配置文件

{
    "compilerOptions": {
        "module" : "ES2015",
        "target":"ES2015",
        "strict" : true
    }
}

(5)package.json文件中添加参数

 (6)运行npm start

修改代码后,会自动对项目进行重新编译,实时更新

以上是一些基本的配置,但是在实际开发中,webpack在配置开发环境与生产环境时,配置的有些东西不太相同,所以我们应该分开写我们生产环境和开发环境的webpack配置。

除了webpack,开发中还经常需要结合babel来对代码进行转换以使其可以兼容到更多的浏览器,通过以下步骤可以将babel引入到项目中。

① 安装依赖包

npm i -D @babel/core @babel/preset-env babel-loader core-js
  • @babel/core:babel的核心工具
  • @babel/preset-env:babel的预定义环境
  • #babel-loader:babel在webpack中的加载器
  • core-js:用来使老版本的浏览器支持新版ES语法

② 修改配置文件webpack.config.js

如此一来,使用ts编译后的文件将会再次被babel处理,使得代码可以在大部分浏览器中直接使用,可以在配置选项的targets中指定要兼容的浏览器版本。

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");

module.exports = {
    // 指定入口文件
    entry: "./src/index.ts",

    // 指定打包文件所在目录
    output: {
        path: path.resolve(__dirname, "dist"),
        filename: "bundle.js",
        //告诉webpack不使用箭头函数
        //关闭webpack的箭头函数,可选
        environment: {
            arrowFunction: false
          },
        clean: true, //自动将上次打包目录资源清空
    },

    // 用来设置引用模块
    resolve: {
        extensions: [".ts", ".js"],
    },

    // 配置webpack的loader
    module: {
        rules: [
            {
                test: /\.ts$/,
                use: [
                    //配置babel
                    {
                        //指定加载器
                        loader: "babel-loader",
                        //设置babel
                        option: {
                            //设置预定义环境
                            presets: [
                                [                       
                                    //指定环境的插件
                                    "@babel/preset-env",
                                    //配置信息
                                    {
                                        //姚建荣的目标浏览器
                                        targets: {
                                            chrome: "58",
                                            ie: "11"
                                        },
                                        //指定corejs的版本
                                        corejs: "3",
                                        //使用corejs的方式“usage”表示按需加载
                                        useBuiltIns: "usage",
                                    },
                                ],
                            ],
                        },
                    },
                    {
                        loader: 'ts-loader',
                    }
                ],
                exclude: /node_modules/,
            },
        ],
    },

    // 配置webpack的插件
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({

        }),
    ],
    mode: 'development'
}

四、面向对象

简言之,面向对象就是程序之中所有的操作都需要通过对象来完成。

举例来说:

  • 操作浏览器要使用windows对象
  • 操作网页要使用document对象
  • 操作控制台要使用console对象

1、类(class)

要想面向对象,操作对象,首先便要拥有对象,那么下一个问题就是如何创建对象。

要想创建对象,必须要先定义类,所谓的类可以理解为对象的模型,程序中可以根据类创建指定类型的对象。

举例来说:可以通过Person类来创建人的对象,通过Dog类创建狗的对象,通过Car类来创建汽车的对象,不同的类可以用来创建不同的对象。

(1)定义类:

class 类名{
	属性名: 类型
}

类包含属性和方法。

① 属性

属性包括实例属性和类属性(静态属性)。

实例属性需要通过类创建的对象的实例来访问,而类属性通过在实例属性前添加关键字static来定义,可以通过类名来直接访问。

//使用class关键字来定义一个类
// 对象中主要包含了两个部分:属性   方法
class Person {
    // 定义实例属性,需要通过对象的实例去访问
    name: string = '孙悟空';
    // 在属性前加readonly关键字,可以将属性设置为只读属性,后续不可修改。
    // readonly name string = ''

    //在属性前使用static关键字可以定义雷属性(静态属性)
    static age: number = 18;
}


//通过对象的实例去访问实例属性
const per = new Person();
per.name = '玛卡巴卡';
console.log(per);

//通过类来直接访问类属性
console.log(Person.age);

在属性前加readonly关键字,可以将属性设置为只读属性,后续不可修改。

② 方法

class 类名 {
	方法名(){
		...
	}
}
//使用class关键字来定义一个类
// 对象中主要包含了两个部分:属性   方法
class Person {
    // 定义实例属性,需要通过对象的实例去访问
    name: string = '孙悟空';
    // 在属性前加readonly关键字,可以将属性设置为只读属性,后续不可修改。
    // readonly name string = ''

    //在属性前使用static关键字可以定义雷属性(静态属性)
    static age: number = 18;

    // 定义方法
    //直接写就是实例方法
    sayHello() {
        console.log('晚安,玛卡巴卡');
    }

    // 如果方法以static开头,则该方法是类方法,即静态方法,不需要创建对象就能访问,即可以通过类来直接访问。。
    static sayGoodbye(){
        console.log('再见,唔西迪西');
        
    }
}


//通过对象的实例去访问实例属性
const per = new Person();
per.name = '玛卡巴卡';
console.log(per);

//通过类来直接访问类属性
console.log(Person.age);

//通过对象的实例去访问实例方法
per.sayHello();

//通过类来直接访问类方法
Person.sayGoodbye();

2、构造函数和this

构造函数语法:

class 类名{
	constructor() {
		...
	}
}

构造函数在对象创建时调用

调用new类名()就相当于调用该类中的构造函数,即调用new Dog( )就相当于调用Dog类中的构造函数。

在构造函数中,当前对象就是当前新建的那个对象。

例如dog1 = new Dog( ),当前对象就是指的dog1。

class Dog{
    name: string;
    age: number;

    // 构造函数 constructor
    // 构造函数在对象创建时调用
    constructor(){
        console.log("构造函数执行了~")
        // 在实例方法中,this就表示当前的实例
        // 在构造函数中,当前对象就是当前新建的那个对象
        console.log(this);
    }

    bark(){
        console.log('汪汪汪!');
    }
}

const dog1 = new Dog();  // 调用new Dog()就相当于调用Dog类中的构造函数
const dog2 = new Dog();

在构造函数中,可以通过this向新建的对象中添加属性。

为了使新建的对象拥有个性化的属性,可以通过构造函数来传递参数,赋值给新建对象的属性:

class Dog {
    name:string;
    age:number;
    //构造函数会在对象创建时调用
    constructor(name:string,age:number){
        // 在实例方法中,this就表示当前的实例
        //在构造函数中当前对象就是当前新建的那个对象
        // 可以通过this向新建的对象中添加属性
        this.name = name;
        this.age = age;
       console.log(this);   //表示dog1 dog2
       
    }

    bark(){
        // alert('汪汪汪');
        // 在方法中可以通过this来表示当前调用方法的对象
        console.log(this);   //dog1调用的bark,此时this就是dog1
        
    }
}

const dog1 = new Dog('小黑',2);
console.log(dog1);
const dog2 = new Dog('小白',3)
console.log(dog2);

dog1.bark()

this就表示当前对象

3、继承简介

通过继承,可以将多个类中共有的代码写在一个父类中,这样只需要写一次即可让所有的子类都同时拥有父类中的属性和方法。

(function () {
    //定义一个表示狗的类
    class Dog {
        name: string;
        age: number;

        constructor(name: string, age: number) {
            this.name = name;
            this.age = age;
        }

        sayHello() {
            console.log('汪汪汪~~~');

        }
    }

    //定义一个表示猫的类
    class Cat {
        name: string;
        age: number;

        constructor(name: string, age: number) {
            this.name = name;
            this.age = age;
        }

        sayHello() {
            console.log('喵喵喵~~~');

        }
    }


    const dog = new Dog('旺财', 5)
    console.log(dog);
    dog.sayHello();
    
    const cat = new Cat('咪咪',2);
    console.log(cat);
    cat.sayHello();
    

})();

可以发现,Dog和Cat两类中的内容几乎完全相同,显得代码非常冗余,可以新建一个Animal类。

如果希望在子类中添加一些父类中没有的属性和方法,直接添加就好:

(function () {
    // 定义一个Animal类
    class Animal {
        name: string;
        age: number;

        constructor(name: string, age: number) {
            this.name = name;
            this.age = age;
        }

        sayHello() {
            console.log('动物在叫~~~');
        }
    }
    // 定义一个表示狗的类,使得Dog类继承Animal类,
    // 此时Animal被称为父类,Dog类被称为子类。
    // 使用继承后,子类将会拥有父类所有的方法和属性。
    // 通过集成可以将多个类中共有的代码写在一个父类中
    // 这样只需要写一次即可让所有的子类都同时拥有父类中的属性和方法
    // 如果希望在子类中添加一些父类中没有的属性或方法直接加就行
    class Dog extends Animal {
        run(){
            console.log(`${this.name}在跑~~~`);
        }
    }

    class Cat extends Animal {

    }
    

    const dog = new Dog('旺财', 5);
    console.log(dog);
    dog.run();

    const cat = new Cat('咪咪',2);
    console.log(cat);
    
})();

方法重写:

如果在子类中添加了和父类相同(重名)的方法,则在该子类中,子类方法会覆盖掉父类方法,父类中本来的方法不会改变。

class Dog extends Animal {
        run(){
            console.log(`${this.name}在跑~~~`);
        }

        sayHello() {
            console.log('旺旺旺~~~')
        }
    }

const dog = new Dog('旺财', 5);
dog.run();
dog.sayHello();

4、super关键字

super表示父类。

(function () {
    class Animal {
        name: string;
        constructor(name: string) {
            this.name = name;
        }

        sayHello() {
            console.log('动物在叫~~~');

        }
    }

    class Dog extends Animal {
        sayHello() {
            //在类的方法中,super就表示当前类的父类
            super.sayHello()
        }
    }

    const dog = new Dog('旺财');
    dog.sayHello();
})();

super的常用之处:

  • 如果在子类中写了构造函数,在子类的构造函数中必须对父类的构造函数进行调用,否则就会报错
  • 原因是在子类中重写构造函数时,父类的构造函数会被覆盖,不会执行。为了保证父类的构造函数正确执行以对父类属性初始化,就需要在子类的构造函数中调用父类的构造函数。
class 子类名 extends 父类名{
	子类新加属性名: 类型;
	
	constructor(父类构造函数参数, 子类需传的参数){
		super(父类构造函数参数);
		this.子类新加属性名 = 子类需传的参数;
	}
}
(function () {
    class Animal {
        name: string;
        constructor(name: string) {
            this.name = name;
        }

        sayHello() {
            console.log('动物在叫~~~');

        }
    }

    class Dog extends Animal {
        age:number;
        constructor(name:string,age:number){
             // 如果在子类中写了构造函数,在子类构造函数中必须对父类的构造函数进行调用
            super(name);   //调用父类构造函数
            this.age = age;
        }
        // sayHello() {
        //     //在类的方法中,super就表示当前类的父类
        //     super.sayHello()
        // }
    }

    const dog = new Dog('旺财',3);
    dog.sayHello();
})();

5、抽象类

在实际中,我们不希望父类被用来创建对象,这时需要在声明父类时在前面加上 abstract 关键字,使其成为一个抽象类。

抽象类和其他类区别不大,只是不能用来创建对象,只能被继承

抽象类就是专门用来被继承的类。

抽象类汇总可以声明抽象方法。

抽象方法在声明时使用 abstract 开头,没有方法体,只能定义在抽象类中,该抽象类的子类必须对抽象方法进行重写,否则就会报错。

(function () {
    //以abstract 开头的类是抽象类
    // 抽象类和其他类区别不大,只是不能用来创建对象
    // 抽象类就是专门被用来继承的类。

    // 抽象类中可以添加抽象方法
    abstract class Animal {
        name: string;
        constructor(name: string) {
            this.name = name;
        }

        //定义一个抽象方法
        // 抽象方法使用abstract开头,没有方法体
        // 抽象方法只能定义在抽象类中,子类必须对抽象方法进行重写
        abstract sayHello():void ;
    }

    class Dog extends Animal {
        sayHello() {
            //在子类中,必须对抽象方法进行重写
            console.log('旺旺旺');
        }
    }

    class Cat extends Animal {
         sayHello() {
             console.log('喵喵!!!');
             
         }
    }
    const dog = new Dog('旺财');
    dog.sayHello();
    const cat = new Cat('喵喵');

    // let animal = new Animal();

})();

6、接口

接口用来定义一个类结构,用来定义一个类中应该包含哪些属性和方法,同时接口也可以当成类型声明去使用。

/* 接口就是用来定义一个类结构
           用来定义一个类中应该包含哪些属性和方法
           同时接口也可以当成类型声明去使用
     */

    interface myInterface {
        name: string,
        age: number,
    }

    const obj : myInterface = {
       name:'孙悟空',
       age: 10
    };

接口可以重复声明,相当于对该接口的补充声明。

interface myInterface {
        name: string,
        age: number,
    }

    // 接口可以重复声明

    interface myInterface {
        gender:string
    }

    const obj : myInterface = {
       name:'孙悟空',
       age: 10,
       gender:'男',
    };

 接口和抽象类的不同点:

  • 接口中所有属性都不能有实际的值,接口只定义对象的结构,而不考虑实际值,在接口中,所有的方法都是抽象方法。
  • 而抽象类中可以有抽象方法(不允许有方法体),也可以有实质的方法和可赋值的属性。
  • 定义类时,可以使类去实现一个接口,实现接口就是使类满足接口的要求。
interface myInter {
        name:string;
        sayHello():void;    //接口中的方法不允许有方法体,只能声明
    }

  /*   定义类时,可以使类去实现一个接口
            实现接口就是使类去满足接口的要求
     */
    class MyClass implements myInter{
        name:string;
        constructor(name:string){
            this.name = name;
        }
        sayHello(){
            console.log('大家好~~~');     
        }
    }

 接口其实就是定义了一个规范,对类作出限制。

7、属性的封装

 class Person {
        name:string;
        age:number;

        constructor(name:string,age:number){
            this.name = name;
            this.age = age
        }
    }
    const person = new Person('孙悟空',18)

现在属性是在对象中设置的,可以随意进行修改。

person.name = '猪八戒';
person.age = 38;
console.log(person);    //Person {name: '猪八戒', age: 38}

 属性可以任意被修改会导致对象中的数据变得非常不安全。

如果我们不想让对象的属性被任意修改,可以在属性前添加属性的修饰符:

  • public 修饰的属性可以在任意位置访问(修改)默认值。
  • protected 受保护的属性,只能在当前类和和当前类的子类中访问(修改)。
  • private 私有属性,只能在当前类内部(不包含子类)进行访问(修改)

 有了setter和getter方法,属性的访问修改控制权就掌握在了自己手中。为了避免非法数据,可以在set方法中添加判断语句,数据合法才会被赋值:

getter方法用来读取属性;
setter方法用来设置属性;
它们被称为属性的存取器。

(function () {
    //定义一个表示人的类
    class Person {
        /* TS可以在属性前添加属性的修饰符
                public 修饰的属性可以在任意位置访问(修改)默认值
                protected 受保护的属性,只能在当前类和和当前类的子类中访问(修改)
                private 私有属性,只能在当前类内部(不包含子类)进行访问(修改)
                   --通过在类中添加方法使得私有属性可以被外部访问
         */

        private name: string;
        protected age: number;

        constructor(name: string, age: number) {
            this.name = name;
            this.age = age
        }

        // 定义方法,用来获取Name属性
        getName() {
            return this.name;
        }

        // 定义方法,用来设置name属性
        setName(value: string) {
            this.name = value
        }
        setAge(value: number) {
            //判断年龄是否合法
            if (value >= 0){
                this.age = value
            }
        }


    }
    const person = new Person('孙悟空', 18)
    person.setName('猪八戒')

    /* 
    *  现在属性是直接在对象中设置的
    *     属性可以任意被修改将会导致对象中的数据变得非常不安全
    */
    console.log(person.getName());

})();

 上述getter方法和setter方法虽然能够保证数据安全,但是有点过于繁琐。在TS中,提供了一种更为方便的属性存取方式:

get 私有属性名() {
	...
}

在类中添加get 属性名()方法,可以使属性外部更加方便地被访问
外部通过【对象.属性名】访问属性时,就会自动调用该方法: 

(function () {
    //定义一个表示人的类
    class Person {
        /* TS可以在属性前添加属性的修饰符
                public 修饰的属性可以在任意位置访问(修改)默认值
                protected 受保护的属性,只能在当前类和和当前类的子类中访问(修改)
                private 私有属性,只能在当前类内部(不包含子类)进行访问(修改)
                   --通过在类中添加方法使得私有属性可以被外部访问
         */

        private _name: string;
        protected _age: number;

        constructor(name: string, age: number) {
            this._name = name;
            this._age = age
        }

        //TS中设置getter方法
        get name(){
            console.log('get name()执行啦');
            
            return this._name;
        }

        set name(value:string){
            this._name = value;
        }

        get age(){
            return this._age;
        }

        set age (value:number){
            if(value >= 0) {
                this._age = value;
            }
          
        }

    }
    const person = new Person('孙悟空', 18)

    /* 
    *  现在属性是直接在对象中设置的
    *     属性可以任意被修改将会导致对象中的数据变得非常不安全
    */
   person.name = '猪八戒';
   person.age = 38;
   console.log(person);

})();

属性声明更为方便的写法:

可以直接将属性定义在构造函数中

class C {
        // 可以直接将属性定义在构造函数中
        constructor(public name:string,public age:number){

        }
      }
      const c = new C('玛卡巴卡',20);

 这段代码相当于

class C {
     name : string;
     age : number;
     constructor(name:string,age:number) {
        this.name = name;
        this.age = age;
       }
   }

const  c = new C('玛卡巴卡',18)

8、泛型

在定义函数或类时,如果遇到类型不明确,就可以使用泛型。

语法:

// 类型明确时,函数的定义语法
function fn1(a: number): number {
    return a;
}

// 类型不明确时,
function fn2<T>(a: T): T {
    return a;
}

其中T可以换成任意字符。

类型不明确的时候也可以用any,不会报错。但是使用any会默认把编译器的类型检查关闭,不安全。此外,也无法体现上式代码中参数类型和返回值类型相同的特点。

function fn <T>(a:T):T{
    return a 
}

// 直接调用具有泛型的参数
//此方法使用了TS中的类型自动推断
fn(10);
//有时候类型复杂、推断不出来,也可以手动指定。
fn<string>('hello');

指定多个泛型:

function fn2<T,K>(a:T,b:K):T{
    return a;
}
fn2<number,string>(123,'hello');

对泛型所属的类型作出限制:

interface Inter{
    mylength :number;
}

// T extends Inter表示泛型T必须是Inter的一个实现类(或子类)
function fn3<T extends Inter>(a:T):number {
    return a.mylength;
}
fn3({mylength:123})

泛型在声明类时也可以使用:

// 泛型在声明类时也可以使用
class myClass<T> {
    name:T;
    constructor(name:T){
        this.name = name;
    }
}
const mc = new myClass<string>('孙悟空');
console.log(mc);

五、练习案例

1、项目搭建

(1)通过执行命令npm init -y ,初始化一个项目并创建package.json文件,修改配置。

(2)使用tsc --init 创建ts的配置文件tsconfig.json。

(3)终端输入npm --init 指令,并修改配置文件 webpack.config.js。

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const {CleanWebpackPlugin} = require("clean-webpack-plugin");

module.exports = {
  entry: "./src/index.ts",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "bundle.js",
    environment: {
      arrowFunction: false, // 关闭webpack的箭头函数,可选
    },
  },
  resolve: {
    extensions: [".js", ".ts"],
  },
  module: {
    rules: [
      //设置ts文件的处理
      {
        test: /\.ts$/,
        use: [
            {
              loader: "babel-loader",
              // 设置babel
              options: {
                // 设置预定义的环境
                presets: [
                  [
                    // 指定环境的插件
                    "@babel/preset-env",
                    // 配置信息
                    {
                      // 要兼容的目标浏览器
                      targets: {
                        chrome: "58",
                        ie: "11",
                      },
                      // 指定corejs的版本
                      corejs: "3",
                      // 使用corejs的方式 "usage" 表示按需加载
                      useBuiltIns: "usage",
                    },
                  ],
                ],
              },
            },
            {
              loader: "ts-loader",
            },
          ],
          exclude: /node_modules/,
      },
      //设置less文件的处理
      {
        test:/\.less$/,
        use:[
            "style-loader",
            "css-loader",
            //引入postcss
            {
                loader:'postcss-loader',
                options:{
                    postcssOptions:{
                        plugins:[
                            [
                                "postcss-preset-env",
                                {
                                    browser:'last 2 versions'      //兼容两个最新两个版本的浏览器
                                }
                            ]
                        ]
                    }
                }
            },
            "less-loader"
        ]
      }
      ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/index.html",
    }),
    new CleanWebpackPlugin()
  ],
  mode:'development'
};

(4)安装依赖:

  • npm i -D webpack webpack-cli webpack-dev-server
  • npm i -D ts-loader typescript
  • npm i -D html-webpack-plugin clean-webpack-plugin
  • npm i -D less less-loader css-loader style-loader
  • npm i -D postcss postcss-loader postcss-preset-env

2、项目界面(搭建静态页面结构)

(1)npm start 自动启动开发服务器

(2)src/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>贪食蛇</title>
    <link href="favicon.ico" rel="shortcut icon">
</head>
<body>

    <!-- 创建游戏的主容器 -->
    <div id="main">
        <!-- 设置游戏舞台 -->
        <div id="stage">

            <!-- 设置蛇 -->
            <div id="snake">
                <!-- snake内部的div 表示蛇的各部分 -->
                <div></div>
            </div>

            <!-- 设置食物 -->
            <div id="food">
                <!-- 添加四个小div,来设置事物的样式 -->
                <div></div>
                <div></div>
                <div></div>
                <div></div>
            </div>
        </div>
        <!-- 设置游戏积分牌 -->
        <div id="score-panel">
            <div>
                SCORE:<span id="score">0</span>
            </div>
            <div>
                LEVEL:<span id="level">1</span>
            </div>
        </div>
    </div>
    
</body>
</html>

 (3)src/style/index.less

// 设置变量
@bg-color: #b7d4a8;

//清除默认样式
* {
    margin: 0;
    padding: 0;
    //改变盒子模型的计算方式
    box-sizing: border-box;
}

body {
    font: bold 20px "Courier";
}

//设置主窗口样式
#main {
    width: 360px;
    height: 420px;
    background-color: @bg-color;
    margin: 100px, auto;
    border: 10px solid black;
    border-radius: 20px;
    //开启弹性盒模型
    display: flex;
    //设置主轴方向(自上向下排列)
    flex-flow: column;
    //设置侧轴的对齐方式
    align-items: center;
    //设置主轴的对齐方式
    justify-content: space-around;
}

//游戏的舞台
#stage {
    width: 304px;
    height: 304px;
    border: 2px solid black;
    //开启相对定位
    position: relative;

    // 设置蛇的样式
    #snake {
        &>div {
            width: 10px;
            height: 10px;
            background-color: #000;
            border: 1px solid @bg-color;
            // 开启绝对定位
            position: absolute;
        }
    }

    // 设置食物的样式
    #food {
        width: 10px;
        height: 10px;
        position: absolute;
        left: 40px;
        top: 100px;
        display: flex;
        //设置横轴为主轴,wrap表示会自动换行
        flex-flow: row wrap;
        // 设置主轴和侧轴空白空间分配到元素之间
        justify-content: space-between;
        align-content: space-around;

        &>div {
            width: 4px;
            height: 4px;
            background-color: black;
            // 使得四个div旋转45度
            transform: rotate(45deg);
        }
    }


}

// 记分牌
#score-panel {
    width: 300px;
    //设置主轴的对齐方式
    display: flex;
    justify-content: space-between;
}

3、完成Food类

  • src/moduls/Food.ts
// 定义事物类Food
class Food {
    //定义一个属性表示事物所对应的元素
    element: HTMLElement;
    constructor() {
        //获取页面中food元素,并将其赋值给element
        this.element = document.getElementById('food')!;
    }

    // 定义一个获取食物X轴坐标的方法
    get X() {
        return this.element.offsetLeft;
    }
    // 定义一个获取食物Y轴坐标的方法
    get Y() {
        return this.element.offsetTop;
    }

    // 修改食物的位置
    change() {
        // 生成一个随机的位置
        // 食物的位置,最小是0,最大是290
        // 蛇移动一次就是一格,一格的大小就是10,所以要求食物的坐标是整10(10的倍数)
        let top = Math.round(Math.random() * 29) * 10;
        let left = Math.round(Math.random() * 29) * 10;
        this.element.style.left = left + 'px';
        this.element.style.top = top + 'px';
    }
}


export default Food;

4、完成ScorePanel类

  • src/modules/ScorePanel.ts
//定义表示积分牌的类
class ScorePanel {
    //score和level用来记录分数和登记
    score = 0;
    level = 1;
    // 分数和顶级所在的元素,在构造函数中进行初始化
    scoreEle:HTMLElement;
    levelEle:HTMLElement;

    //设置一个变量:限制等级
    maxLeverl : number;
    //设置一个变量:表示多少分时升级
    upScore : number;
    constructor(maxLever:number = 10,upScore:number = 10){
        this.scoreEle = document.getElementById('score')!;
        this.levelEle = document.getElementById('level')!;
        this.maxLeverl = maxLever;
        this.upScore = upScore;
    }
   
    //设置一个加分的方法
    addScore(){
        //使分数自增
        this.scoreEle.innerHTML = ++this.score + '';
        //,判断分数是多少设置升级条件
        if(this.score % this.upScore === 0){
            this.levelUp();
        }
    }

    //提升等级的方法
    levelUp(){
        if(this.level < this.maxLeverl){
            this.levelEle.innerHTML = ++this.level + '';
        }
        
    }
}

export default ScorePanel;

5、完成Snake类

  • src/modules/Snake.ts
class Snake {
    //表示蛇头的元素
    head:HTMLElement;
    //蛇的身体,包括蛇头,是一个集合
    bodies : HTMLCollection;
    //获取蛇的容器
    element : HTMLElement;
    constructor(){
        this.head = document.querySelector('#snake>div') as HTMLElement;
        this.bodies = document.getElementById('snake')!.getElementsByTagName('div');
        this.element = document.getElementById('snake')!;
    }

    // 获取蛇的坐标(蛇头)
    get X(){
        return this.head.offsetLeft;
    }
    get Y(){
        return this.head.offsetTop;
    }

    // 设置蛇头坐标
    set X(value:number){
        this.head.style.left = value + 'px';
    }

    set Y(value:number){
        this.head.style.top = value + 'px';
    }

    // 设置蛇增加身体的方法
    addBody(){
        // 向element里面添加div
        this.element.insertAdjacentHTML("beforeend",'<div></div>');
    }

}
export default Snake;

6、GameControl键盘事件

  • src/modules/GameControl.ts
// 引入其他的类
import Snake from './Snake';
import Food from './Food';
import ScorePanel from './ScorePanel';

//游戏控制器,控制其他所有类
class GameControl{
// 定义三个属性
// 蛇
snake:Snake;
//食物
food:Food;
// 记分牌
scorePanel:ScorePanel;

// 创建一个属性来存储蛇的移动方向(也就是按键方向)
direction :string = '';

constructor(){
    this.snake = new Snake();
    this.food = new Food();
    this.scorePanel = new ScorePanel();
    this.init();
}

// 游戏的初始化方法,调用后游戏即开始
init(){
    //绑定键盘按键按下的事件
    document.addEventListener('keydown',this.keyDownHandler.bind(this));   //此时里面的this都表示GameControl对象
}
    
    //创建一个键盘按下的响应函数
    keyDownHandler(event:KeyboardEvent){
        // 需要检查event.key的值是否合法(用户是否按了正确的按键)
        //修改direction属性
        this.direction = event.key;
        
    };

}
export default GameControl;

7、GameControl使蛇移动

  • src/modules/GameControl.ts
// 引入其他的类
import Snake from './Snake';
import Food from './Food';
import ScorePanel from './ScorePanel';

//游戏控制器,控制其他所有类
class GameControl {
    // 定义三个属性
    // 蛇
    snake: Snake;
    //食物
    food: Food;
    // 记分牌
    scorePanel: ScorePanel;

    // 创建一个属性来存储蛇的移动方向(也就是按键方向)
    direction: string = '';
    // 创建一个属性,用来记录游戏是否结束
    isLive = true;


    constructor() {
        this.snake = new Snake();
        this.food = new Food();
        this.scorePanel = new ScorePanel();
        this.init();
    }

    // 游戏的初始化方法,调用后游戏即开始
    init() {
        //绑定键盘按键按下的事件
        document.addEventListener('keydown', this.keyDownHandler.bind(this));   //此时里面的this都表示GameControl对象
        //调用run方式,使蛇移动
        this.run();
    }


    //创建一个键盘按下的响应函数
    keyDownHandler(event: KeyboardEvent) {
        // 需要检查event.key的值是否合法(用户是否按了正确的按键)
        //修改direction属性
        this.direction = event.key;

    }

    //创建一个控制蛇移动的方法
    run() {
        /* 根据方向(this.direction)来使蛇的位置改变
              向上  top减少
              向下  top增加
              向左  left减少
              向右  left增加
        */
        // 获取蛇现在的坐标
        let X = this.snake.X;
        let Y = this.snake.Y;

        // 根据按键方向来修改X值和Y值
        switch (this.direction) {
            case "ArrowUp":
            case "Up":
                // 向上移动
                Y -= 10;
                break;
            case "ArrowDown":
            case "Down":
                // 向下移动
                Y += 10;
                break;
            case "ArrowLeft":
            case "Left":
                //向左移动
                X -= 10;
                break;
            case "ArrowRight":
            case "Right":
                //向右移动
                X += 10;
                break;
        }

        //修改蛇的X和Y值
        this.snake.X = X;
        this.snake.Y = Y;

        //开启一个定时调用
        this.isLive && setTimeout(this.run.bind(this), 300 -(this.scorePanel.level -1)*30)   //this.scorePanel.level表示当前等级
       
    }
}



export default GameControl;

8、蛇撞墙和吃食检测

  • src/modules/GameControl.ts
// 引入其他的类
import Snake from './Snake';
import Food from './Food';
import ScorePanel from './ScorePanel';

//游戏控制器,控制其他所有类
class GameControl {
    // 定义三个属性
    // 蛇
    snake: Snake;
    //食物
    food: Food;
    // 记分牌
    scorePanel: ScorePanel;

    // 创建一个属性来存储蛇的移动方向(也就是按键方向)
    direction: string = '';
    // 创建一个属性,用来记录游戏是否结束
    isLive = true;


    constructor() {
        this.snake = new Snake();
        this.food = new Food();
        this.scorePanel = new ScorePanel();
        this.init();
    }

    // 游戏的初始化方法,调用后游戏即开始
    init() {
        //绑定键盘按键按下的事件
        document.addEventListener('keydown', this.keyDownHandler.bind(this));   //此时里面的this都表示GameControl对象
        //调用run方式,使蛇移动
        this.run();
    }


    //创建一个键盘按下的响应函数
    keyDownHandler(event: KeyboardEvent) {
        // 需要检查event.key的值是否合法(用户是否按了正确的按键)
        //修改direction属性
        this.direction = event.key;

    }

    //创建一个控制蛇移动的方法
    run() {
        /* 根据方向(this.direction)来使蛇的位置改变
              向上  top减少
              向下  top增加
              向左  left减少
              向右  left增加
        */
        // 获取蛇现在的坐标
        let X = this.snake.X;
        let Y = this.snake.Y;

        // 根据按键方向来修改X值和Y值
        switch (this.direction) {
            case "ArrowUp":
            case "Up":
                // 向上移动
                Y -= 10;
                break;
            case "ArrowDown":
            case "Down":
                // 向下移动
                Y += 10;
                break;
            case "ArrowLeft":
            case "Left":
                //向左移动
                X -= 10;
                break;
            case "ArrowRight":
            case "Right":
                //向右移动
                X += 10;
                break;
        }

        //检查蛇是否吃到了食物
        this.checkEat(X, Y)

        //修改蛇的X和Y值
        try {
            this.snake.X = X;
            this.snake.Y = Y;
        } catch (e) {
            //进入到catch,说明出现了异常,游戏结束,弹出提示信息
            alert(e + 'GAME OVER!!!');
            //将isLive设置为false
            this.isLive = false;


        }



        //开启一个定时调用
        this.isLive && setTimeout(this.run.bind(this), 300 - (this.scorePanel.level - 1) * 30)   //this.scorePanel.level表示当前等级

    }

    //定义一个方法,用来检查蛇是否吃到食物
    checkEat(X: number, Y: number) {
        // 蛇的坐标与食物的坐标重合,就表示吃到了食物
        if (X === this.food.X && Y === this.food.Y) {
            console.log('吃到食物啦');
            // 吃到食物后,食物的位置需要重置
            this.food.change();
            // 分数增加
            this.scorePanel.addScore();
            //蛇要增加一节
            this.snake.addBody();
        }

    }
}



export default GameControl;

9、身体的移动

  • src/modules/GameControl.ts
class Snake {
    //表示蛇头的元素
    head: HTMLElement;
    //蛇的身体,包括蛇头,是一个集合
    bodies: HTMLCollection;
    //获取蛇的容器
    element: HTMLElement;
    constructor() {
        this.head = document.querySelector('#snake>div') as HTMLElement;
        this.bodies = document.getElementById('snake')!.getElementsByTagName('div');
        this.element = document.getElementById('snake')!;
    }

    // 获取蛇的坐标(蛇头)
    get X() {
        return this.head.offsetLeft;
    }
    get Y() {
        return this.head.offsetTop;
    }

    // 设置蛇头坐标
    set X(value: number) {
        // 如果新值和旧值相同,则直接返回不修改
        if (this.X === value) {
            return;
        }

        // X的值的合法范围0-290之间
        if (value < 0 || value > 290) {
            //进入判断说明蛇撞墙了
            throw new Error('蛇撞墙了!')
        }
        //修改X时,是在修改水平坐标,蛇在左右移动,蛇在向左移动时,不能向右掉头,反之亦反
        if (this.bodies[1] && (this.bodies[1] as HTMLElement).offsetLeft === value) {
            // console.log('水平方向发生了掉头');
            // 如果发生了掉头,让蛇向反方向继续移动
            if (value > this.X) {
                //如果新值value大于旧值X,则说明蛇在向右走,此时发生掉头,应该使蛇继续向左走
                value = this.X - 10;
            } else {
                value = this.X + 10;
            }
        }

        //移动身体
        this.moveBody();
        this.head.style.left = value + 'px';
        //检查有没有撞到自己
        this.checkHeadBody();
    }

    set Y(value: number) {
        // 如果新值和旧值相同,则直接返回不修改
        if (this.Y === value) {
            return;
        }

        // Y的值的合法范围0-290之间
        if (value < 0 || value > 290) {
            //进入判断说明蛇撞墙了
            throw new Error('蛇撞墙了!')
        }

        if (this.bodies[1] && (this.bodies[1] as HTMLElement).offsetTop === value) {
            // console.log('水平方向发生了掉头');
            // 如果发生了掉头,让蛇向反方向继续移动
            if (value > this.Y) {
                //如果新值value大于旧值X,则说明蛇在向下走,此时发生掉头,应该使蛇继续向上走
                value = this.Y - 10;
            } else {
                value = this.Y + 10;
            }
        }


        //移动身体
        this.moveBody();
        this.head.style.top = value + 'px';
        //检查有没有撞到自己
        this.checkHeadBody();
    }

    // 设置蛇增加身体的方法
    addBody() {
        // 向element里面添加div
        this.element.insertAdjacentHTML("beforeend", '<div></div>');
    }

    // 添加一个蛇身体移动的方法
    moveBody() {
        // 将后边的身体设置为前边身体的位置
        /* 
        举例子:
            第4节= 第3节的位置
            第3节= 第2节的位置
            第2节= 第1节的位置(蛇头) 
        */

        // 遍历获取所有的身体(从后往前)
        for (let i = this.bodies.length - 1; i > 0; i--) {
            //获取前边身体的位置
            let X = (this.bodies[i - 1] as HTMLElement).offsetLeft;
            let Y = (this.bodies[i - 1] as HTMLElement).offsetTop;
            // 将整个值设置到当前身体上
            (this.bodies[i] as HTMLElement).style.left = X + 'px';
            (this.bodies[i] as HTMLElement).style.top = Y + 'px';
        }
    }
    // 检查蛇头是否撞到自己的方法
    checkHeadBody() {
        //  获取所有的身体,检查其是否和蛇头的坐标发生重叠
        for (let i = 1; i < this.bodies.length; i++) {
            let bd = this.bodies[i] as HTMLElement;
            if (this.X === bd.offsetLeft && this.Y === bd.offsetTop) {
                // 进入判断,说明蛇头撞到身体,游戏结束
                throw new Error('撞到自己了~~~')
            }
        }
    }

}
export default Snake;
  • src/index.ts
//引入样式
import "./style/index.less"
import GameControl from './moduls/GameControl';


// const food = new Food();
// console.log(food.X,food.Y);
// food.change();
// console.log(food.X,food.Y);



new GameControl();

  • 3
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值