一、简介
Vue3+TypeScript从入门到进阶(一)——Vue3简介及介绍——附沿途学习案例及项目实战代码
二、Vue2和Vue3区别
Vue3+TypeScript从入门到进阶(二)——Vue2和Vue3的区别——附沿途学习案例及项目实战代码
三、Vue知识点学习
Vue3+TypeScript从入门到进阶(三)——Vue3基础知识点(上)——附沿途学习案例及项目实战代码
Vue3+TypeScript从入门到进阶(四)——Vue3基础知识点(中)——附沿途学习案例及项目实战代码
Vue3+TypeScript从入门到进阶(五)——Vue3基础知识点(下)——附沿途学习案例及项目实战代码
四、TypeScript知识点
一、JavaScript和TypeScript
JavaScript一门优秀的语言
我始终相信:任何新技术的出现都是为了解决原有技术的某个痛点。
JavaScript是一门优秀的编程语言吗?
著名的Atwood定律:
-
Stack Overflow的创立者之一的 Jeff Atwood 在2007年提出了著名的 Atwood定律。 -
any application that can be written in JavaScript, will eventually be written in JavaScript. -
任何可以使用JavaScript来实现的应用都最终都会使用JavaScript实现。
其实我们已经看到了,这句话正在一步步被应验:
-
Web端的开发我们一直都是使用JavaScript; -
移动端开发可以借助于ReactNative、Weex、Uniapp等框架实现跨平台开发; -
小程序端的开发也是离不开JavaScript; -
桌面端应用程序我们可以借助于Electron来开发; -
服务器端开发可以借助于Node环境使用JavaScript来开发。
JavaScript的痛点
并且随着近几年前端领域的快速发展,让JavaScript迅速被普及和受广大开发者的喜爱,借助于JavaScript本身的强大,也让使用JavaScript开发的人员越来越多。
优秀的JavaScript没有缺点吗?
-
其实上由于各种历史因素,JavaScript语言本身存在很多的缺点; -
比如ES5以及之前的使用的var关键字关于作用域的问题; -
比如最初JavaScript设计的数组类型并不是连续的内存空间; -
比如直到今天JavaScript也没有加入类型检测这一机制;
JavaScript正在慢慢变好
-
不可否认的是,JavaScript正在慢慢变得越来越好,无论是从底层设计还是应用层面。 -
ES6、7、8等的推出,每次都会让这门语言更加现代、更加安全、更加方便。 -
但是知道今天,JavaScript在类型检测上依然是毫无进展(为什么类型检测如此重要,我后面会聊到)
类型带来的问题
首先你需要知道,编程开发中我们有一个共识:错误出现的越早越好
-
能在写代码的时候发现错误,就不要在代码编译时再发现(IDE的优势就是在代码编写过程中帮助我们发现错误)。 -
能在代码编译期间发现错误,就不要在代码运行期间再发现(类型检测就可以很好的帮助我们做到这一点)。 -
能在开发阶段发现错误,就不要在测试期间发现错误,能在测试期间发现错误,就不要在上线后发现错误。
现在我们想探究的就是如何在 代码编译期间 发现代码的错误:
- JavaScript可以做到吗?不可以,我们来看下面这段经常可能出现的代码问题。
类型错误
这是我们一个非常常见的错误:
当然,你可能会想:我怎么可能犯这样低级的错误呢?
但是,如果我们可以给JavaScript加上很多限制,在开发中就可以很好的避免这样的问题了:
-
比如我们的getLength函数中str是一个必传的类型,没有调用者没有传编译期间就会报错; -
比如我们要求它的必须是一个String类型,传入其他类型就直接报错; -
那么就可以知道很多的错误问题在编译期间就被发现,而不是等到运行时再去发现和修改;
类型思维的缺失
我们已经简单体会到没有类型检查带来的一些问题,JavaScript因为从设计之初就没有考虑类型的约束问题,所以造成了前端开发人员关于类型思维的缺失:
所以我们经常会说JavaScript不适合开发大型项目,因为当项目一旦庞大起来,这种宽松的类型约束会带来非常多的安全隐患,多人员开发它们之间也没有良好的类型契约。
JavaScript添加类型约束
为了弥补JavaScript类型约束上的缺陷,增加类型约束,很多公司推出了自己的方案:
而现在,无疑TypeScript已经完全胜出:
-
Vue2.x的时候采用的就是flow来做类型检查; -
Vue3.x已经全线转向TypeScript,98.3%使用TypeScript进行了重构; -
而Angular在很早期就使用TypeScript进行了项目重构并且需要使用TypeScript来进行开发; -
而甚至Facebook公司一些自己的产品也在使用TypeScript;
学习TypeScript不仅仅可以为我们的代码增加类型约束,而且可以培养我们前端程序员具备类型思维。
认识TypeScript
虽然我们已经知道TypeScript是干什么的了,也知道它解决了什么样的问题,但是我们还是需要全面的来认识一下TypeScript到底是什么?
我们来看一下TypeScript在GitHub和官方上对自己的定义:
-
GitHub说法:TypeScript is a superset of JavaScript that compiles to clean JavaScript output. -
TypeScript官网:TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. -
翻译一下:TypeScript是拥有类型的JavaScript超集,它可以编译成普通、干净、完整的JavaScript代码。
怎么理解上面的话呢?
-
我们可以将TypeScript理解成加强版的JavaScript。 -
JavaScript所拥有的特性,TypeScript全部都是支持的,并且它紧随ECMAScript的标准,所以ES6、ES7、ES8等新语法标准,它都是支持的; -
并且在语言层面上,不仅仅增加了类型约束,而且包括一些语法的扩展,比如枚举类型(Enum)、元组类型(Tuple)等; -
TypeScript在实现新特性的同时,总是保持和ES标准的同步甚至是领先; -
并且TypeScript最终会被编译成JavaScript代码,所以你并不需要担心它的兼容性问题,在编译时也不需要借助于Babel这样的工具; -
所以,我们可以把TypeScript理解成更加强大的JavaScript,不仅让JavaScript更加安全,而且给它带来了诸多好用的好用特性;
TypeScript的特点
官方对TypeScript有几段特点的描述,我觉得非常到位(虽然有些官方,了解一下),我们一起来分享一下:
始于JavaScript,归于JavaScript
-
TypeScript从今天数以百万计的JavaScript开发者所熟悉的语法和语义开始。使用现有的JavaScript代码,包括流行的JavaScript库,并从JavaScript代码中调用TypeScript代码; -
TypeScript可以编译出纯净、 简洁的JavaScript代码,并且可以运行在任何浏览器上、Node.js环境中和任何支持ECMAScript 3(或更高版本)的JavaScript引擎中;
TypeScript是一个强大的工具,用于构建大型项目
拥有先进的 JavaScript
众多项目采用TypeScript
正是因为有这些特性,TypeScript目前已经在很多地方被应用:
-
源码在很早就使用TypeScript来进行了重写,并且开发Angular也需要掌握TypeScript; -
Vue3源码也采用了TypeScript进行重写,在前面阅读源码时我们看到大量TypeScript的语法; -
包括目前已经变成最流行的编辑器VSCode也是使用TypeScript来完成的; -
包括在React中已经使用的ant-design的UI库,也大量使用TypeScript来编写; -
目前公司非常流行Vue3+TypeScript、React+TypeScript的开发模式; -
包括小程序开发,也是支持TypeScript的;
前端学不动系列
在之前deno的issue里面出现了一个问题:
大前端的发展趋势
大前端是一群最能或者说最需要折腾的开发者:
-
客户端开发者:从Android到iOS,或者从iOS到Android,到RN,甚至现在越来越多的客户端开发者接触前端相关知识(Vue、React、Angular、小程序); -
前端开发者:从jQuery到AngularJS,到三大框架并行:Vue、React、Angular,还有小程序,甚至现在也要接触客户端开发(比如RN、Flutter); -
目前又面临着不仅仅学习ES的特性,还要学习TypeScript; -
新框架的出现,我们又需要学习新框架的特性,比如vue3.x、react18等等;
但是每一样技术的出现都会让惊喜,因为他必然是解决了之前技术的某一个痛点的,而TypeScript真是解决了JavaScript存在的很多设计缺陷,尤其是关于类型检测的。
并且从开发者长远的角度来看,学习TypeScript有助于我们前端程序员培养 类型思维,这种思维方式对于完成大型项目尤为重要。
二、TypeScript的安装和使用
1、TypeScript的安装
在前面我们提到过,TypeScript最终会被编译成JavaScript来运行,所以我们需要搭建对应的环境:
- 我们需要在电脑上安装TypeScript,这样就可以通过TypeScript的Compiler将其编译成JavaScript;
所以,我们需要先可以先进行全局的安装:
# 安装命令
npm install typescript -g
# 查看版本
tsc --version
2、TypeScript的运行环境
如果我们每次为了查看TypeScript代码的运行效果,都通过经过两个步骤的话就太繁琐了:
是否可以简化这样的步骤呢?
上面我提到的两种方式,可以通过两个解决方案来完成:
方式一:webpack配置
方式二:安装ts-node
npm install ts-node -g
另外ts-node需要依赖 tslib 和 @types/node 两个包:
npm install tslib @types/node -g
现在,我们可以直接通过 ts-node 来运行TypeScript的代码:
ts-node math.ts
三、TypeScript基础知识点
1、变量的声明
我们已经强调过很多次,在TypeScript中定义变量需要指定 标识符 的类型。
所以完整的声明格式如下:
- 声明了类型后TypeScript就会进行类型检测,声明的类型可以称之为类型注解;
var/let/const 标识符: 数据类型 = 赋值;
比如我们声明一个message,完整的写法如下:
let message: string = "Hello World";
如果我们给message赋值其他类型的值,那么就会报错:
声明变量的关键字
在TypeScript定义变量(标识符)和ES6之后一致,可以使用var、let、const来定义。
var myname: string = "why";
let myage: number = 20;
const myheight: number = 1.88;
当然,在tslint中并不推荐使用var来声明变量:
- 可见,在TypeScript中并不建议再使用var关键字了,主要原因和ES6升级后let和var的区别是一样的,var是没有块级作用域的,会引起很多的问题,这里不再展开探讨。
变量的类型推导(推断)
在开发中,有时候为了方便起见我们并不会在声明每一个变量时都写上对应的数据类型,我们更希望可以通过TypeScript本身的特性帮助我们推断出对应的变量类型:
如果我们给message赋值123:
这是因为在一个变量第一次赋值时,会根据后面的赋值内容的类型,来推断出变量的类型:
- 上面的message就是因为后面赋值的是一个string类型,所以message虽然没有明确的说明,但是依然是一个string类型;
2、JavaScript和TypeScript的数据类型
我们经常说TypeScript是JavaScript的一个超级:
JavaScript类型 – number类型
数字类型是我们开发中经常使用的类型,TypeScript和JavaScript一样,不区分整数类型(
int)和浮点型(double),统一为number类型。
let num = 100;
num = 20;
num = 6.66;
如果你学习过ES6应该知道,ES6新增了二进制和八进制的表示方法,而TypeScript也是支持二进制、八进制、十六进制的表示:
let num1: number = 100
let num2: number = 0b100
let num3: number = 0o100
let num4: number = 0x100
JavaScript类型 – boolean类型
boolean类型只有两个取值:true和false,非常简单
let flag: boolean = true
flag = 20 > 30
JavaScript类型 – string类型
string类型是字符串类型,可以使用单引号或者双引号表示:
let message1: string = 'hello world'
let message2: string = "Hello World"
同时也支持ES6的模板字符串来拼接变量和字符串:
const name = "why"
const age = 18
const height = 1.88
let message3 = `name:${name} age:${age} height:${height}`
console.log(message3)
JavaScript类型 – Array类型
数组类型的定义也非常简单,有两种方式:
const names1: Array<string> = []
const names2: string[] = []
如果添加其他类型到数组中,那么会报错:
JavaScript类型 – Object类型
object对象类型可以用于描述一个对象:
const myInfo:object = {
name: "why",
age: 18
}
console.log(info.name)
但是从myinfo中我们不能获取数据,也不能设置数据:
JavaScript类型 – Symbol类型
在ES5中,如果我们是不可以在对象中添加相同的属性名称的,比如下面的做法:
通常我们的做法是定义两个不同的属性名字:比如identity1和identity2。
但是我们也可以通过symbol来定义相同的名称,因为Symbol函数返回的是不同的值:
const title1 = Symbol("title")
const title2 = Symbol('title')
const info = {
[title1]: "程序员",
[title2]: "老师"
}
JavaScript类型 – null和undefined类型
在 JavaScript 中,undefined 和 null 是两个基本数据类型。
在TypeScript中,它们各自的类型也是undefined和null,也就意味着它们既是实际的值,也是自己的类型:
let n1: null = null
let n2: undefined = undefined
TypeScript类型 - any类型
在某些情况下,我们确实无法确定一个变量的类型,并且可能它会发生一些变化,这个时候我们可以使用any类型(类似于Dart语言中的dynamic类型)。
any类型有点像一种讨巧的TypeScript手段:
let message: any = "Hello World"
message = 123
message = true
message = {
}
console.log(message)
const arr: any[] = []
如果对于某些情况的处理过于繁琐不希望添加规定的类型注解,或者在引入一些第三方库时,缺失了类型注解,这个时候我们可以使用any:
- 包括在Vue源码中,也会使用到any来进行某些类型的适配;
TypeScript类型 - unknown类型
unknown是TypeScript中比较特殊的一种类型,它用于描述类型不确定的变量。
function foo() {
return "abc"
}
function bar() {
return 123
}
let flag = true
let result: unknown
if (flag) {
result = foo()
} else {
result = bar()
}
let message: string = result
let num: number = result
console.log(result)
export {}
TypeScript类型 - void类型
void通常用来指定一个函数是没有返回值的,那么它的返回值就是void类型:
- 我们可以将null和undefined赋值给void类型,也就是函数可以返回null或者undefined
function sum(num1: number, num2: number) {
console.log(num1 + num2)
}
这个函数我们没有写任何类型,那么它默认返回值的类型就是void的,我们也可以显示的来指定返回值是void:
function sum(num1: number, num2: number) :void {
console.log(num1 + num2)
}
TypeScript类型 - never类型
never 表示永远不会发生值的类型,比如一个函数:
function foo(): never {
while(true) {
}
}
function bar(): never {
throw new Error()
}
function handleMessage(message: string | number | boolean) {
switch (typeof message) {
case 'string':
console.log("string处理方式处理message")
break
case 'number':
console.log("number处理方式处理message")
break
case 'boolean':
console.log("boolean处理方式处理message")
break
default:
const check: never = message
}
}
never有什么样的应用场景呢?这里我们举一个例子,但是它用到了联合类型,后面我们会讲到:
TypeScript类型 - tuple类型
tuple是元组类型,很多语言中也有这种数据类型,比如Python、Swift等。
const info: [string, number, number] = ["why", 18, 1.88]
const name = info[0]
console.log(name.length)
那么tuple和数组有什么区别呢?
Tuples的应用场景
那么tuple在什么地方使用的是最多的呢?
- tuple通常可以作为返回的值,在使用的时候会非常的方便;
function useState(state: any) {
let currentState = state
const changeState = (newState: any) => {
currentState = newState
}
const tuple: [any, (newState: any) => void] = [currentState, changeState]
return tuple
}
const [counter, setCounter] = useState(10);
setCounter(1000)
const [title, setTitle] = useState("abc")
3、函数的参数类型
函数是JavaScript非常重要的组成部分,TypeScript允许我们指定函数的参数和返回值的类型。
参数的类型注解
- 声明函数时,可以在每个参数后添加类型注解,以声明函数接受的参数类型:
function sum(num1: number, num2: number) {
return num1 + num2
}
函数的返回值类型
我们也可以添加返回值的类型注解,这个注解出现在函数列表的后面:
function sum(num1: number, num2: number) :number {
return num1 + num2
}
和变量的类型注解一样,我们通常情况下不需要返回类型注解,因为TypeScript会根据 return 返回值推断函数的返回类型:
- 某些第三方库处于方便理解,会明确指定返回类型,但是这个看个人喜好;
匿名函数的参数
匿名函数与函数声明会有一些不同:
function foo(message: string) {
}
const names = ["abc", "cba", "nba"]
names.forEach(function(item) {
console.log(item.split(""))
})
我们并没有指定item的类型,但是item是一个string类型:
对象类型
如果我们希望限定一个函数接受的参数是一个对象,这个时候要如何限定呢?
function printPoint(point: {x: number, y: number}) {
console.log(point.x);
console.log(point.y)
}
printPoint({x: 123, y: 321})
export {}
在这里我们使用了一个对象来作为类型:
-
在对象我们可以添加属性,并且告知TypeScript该属性需要是什么类型; -
属性之间可以使用 , 或者 ; 来分割,最后一个分隔符是可选的; -
每个属性的类型部分也是可选的,如果不指定,那么就是any类型;
可选类型
对象类型也可以指定哪些属性是可选的,可以在属性的后面添加一个?:
function printPoint(point: {x: number, y: number, z?: number}) {
console.log(point.x)
console.log(point.y)
console.log(point.z)
}
printPoint({x: 123, y: 321})
printPoint({x: 123, y: 321, z: 111})
export {}
联合类型
TypeScript的类型系统允许我们使用多种运算符,从现有类型中构建新类型。
我们来使用第一种组合类型的方法:联合类型(Union Type)
function printID(id: number|string|boolean) {
console.log(id)
}
printID(123)
printID("abc")
printID(true)
使用联合类型
传入给一个联合类型的值是非常简单的:只要保证是联合类型中的某一个类型的值即可
那么我们怎么处理这样的问题呢?
function printID(id: number|string|boolean) {
if (typeof id === 'string') {
console.log(id.toUpperCase())
} else {
console.log(id)
}
}
可选类型补充
其实上,可选类型可以看做是 类型 和 undefined 的联合类型:
function foo(message?: string) {
console.log(message)
}
foo()
类型别名
在前面,我们通过在类型注解中编写 对象类型 和 联合类型,但是当我们想要多次在其他地方使用时,就要编写多次。
比如我们可以给对象类型起一个别名:
type IDType = string | number | boolean
type PointType = {
x: number
y: number
z?: number
}
function printId(id: IDType) {
}
function printPoint(point: PointType) {
}
类型断言as
有时候TypeScript无法获取具体的类型信息,这个我们需要使用类型断言(Type Assertions)。
- 比如我们通过 document.getElementById,TypeScript只知道该函数会返回 HTMLElement ,但并不知道它具体的类型:
// 1.类型断言 as
const el = document.getElementById("why") as HTMLImageElement
el.src = "url地址"
TypeScript只允许类型断言转换为 更具体 或者 不太具体 的类型版本,此规则可防止不可能的强制转换:
非空类型断言!
当我们编写下面的代码时,在执行ts的编译阶段会报错:
- 这是因为传入的message有可能是为undefined的,这个时候是不能执行方法的;
function printMessageLength(message?: string) {
if (message) {
console.log(message.length)
}
}
printMessageLength("aaaa")
printMessageLength("hello world")
但是,我们确定传入的参数是有值的,这个时候我们可以使用非空类型断言:
- 非空断言使用的是 ! ,表示可以确定某个标识符是有值的,跳过ts在编译阶段对它的检测;
function printMessageLength(message?: string) {
console.log(message!.length)
}
printMessageLength("aaaa")
printMessageLength("hello world")
可选链的使用
可选链事实上并不是TypeScript独有的特性,它是ES11(ES2020)中增加的特性:
type Person = {
name: string
friend?: {
name: string
age?: number,
girlFriend?: {
name: string
}
}
}
const info: Person = {
name: "why",
friend: {
name: "kobe",
girlFriend: {
name: "lily"
}
}
}
console.log(info.name)
console.log(info.friend?.name)
console.log(info.friend?.age)
console.log(info.friend?.girlFriend?.name)
??和!!的作用
有时候我们还会看到 !! 和 ?? 操作符,这些都是做什么的呢?
!!操作符:
-
将一个其他类型转换成boolean类型; -
类似于Boolean(变量)的方式;
??操作符:
空值合并操作符(??)是一个逻辑操作符,当操作符的左侧是 null 或者 undefined 时,返回其右侧操作数,否则返回左侧操作数;
const message = "Hello World"
const flag = !!message
console.log(flag)
let message: string|null = "Hello World"
const content = message ?? "你好啊, 李银河"
console.log(content)
字面量类型
除了前面我们所讲过的类型之外,也可以使用字面量类型(literal types):
const message: "Hello World" = "Hello World"
那么这样做有什么意义呢?
- 默认情况下这么做是没有太大的意义的,但是我们可以将多个类型联合在一起;
type Alignment = 'left' | 'right' | 'center'
let align: Alignment = 'left'
align = 'right'
align = 'center'
字面量推理
我们来看下面的代码:
这是因为我们的对象再进行字面量推理的时候,info其实是一个 {url: string, method: string},所以我们没办法将一个 string 赋值给一个 字面量 类型。
type Method = 'GET' | 'POST'
function request(url: string, method: Method) {}
type Request = {
url: string,
method: Method
}
const options = {
url: "https://www.coderwhy.org/abc",
method: "POST"
} as const
const options: Request = {
url: "https://www.coderwhy.org/abc",
method: "POST"
}
request(options.url, options.method)
export {}
4、类型缩小
什么是类型缩小呢?
-
类型缩小的英文是 Type Narrowing; -
我们可以通过类似于 typeof padding === “number” 的判断语句,来改变TypeScript的执行路径; -
在给定的执行路径中,我们可以缩小比声明时更小的类型,这个过程称之为 缩小; -
而我们编写的 typeof padding === "number 可以称之为 类型保护(type guards);
常见的类型保护有如下几种:
-
typeof -
平等缩小(比如===、!==) -
instanceof -
in -
等等…
typeof
在 TypeScript 中,检查返回的值typeof是一种类型保护:因为 TypeScript 对如何typeof操作不同的值进行编码。
type IDType = number | string
function printID(id: IDType) {
if (typeof id === 'string') {
console.log(id.toUpperCase())
} else {
console.log(id)
}
}
平等缩小
我们可以使用Switch或者相等的一些运算符来表达相等性(比如===, !==, ==, and != ):
type Direction = "left" | "right" | "top" | "bottom"
function printDirection(direction: Direction) {
}
instanceof
JavaScript 有一个运算符来检查一个值是否是另一个值的“实例”:
function printTime(time: string | Date) {
if (time instanceof Date) {
console.log(time.toUTCString())
} else {
console.log(time)
}
}
class Student {
studying() {}
}
class Teacher {
teaching() {}
}
function work(p: Student | Teacher) {
if (p instanceof Student) {
p.studying()
} else {
p.teaching()
}
}
const stu = new Student()
work(stu)
in
Javascript 有一个运算符,用于确定对象是否具有带名称的属性:in运算符
- 如果指定的属性在指定的对象或其原型链中,则in 运算符返回true;
type Fish = {
swimming: () => void
}
type Dog = {
running: () => void
}
function walk(animal: Fish | Dog) {
if ('swimming' in animal) {
animal.swimming()
} else {
animal.running()
}
}
const fish: Fish = {
swimming() {
console.log("swimming")
}
}
walk(fish)
5、TypeScript函数类型
在JavaScript开发中,函数是重要的组成部分,并且函数可以作为一等公民(可以作为参数,也可以作为返回值进行传递)。
那么在使用函数的过程中,函数是否也可以有自己的类型呢?
- 我们可以编写函数类型的表达式(Function Type Expressions),来表示函数类型;
function foo() {}
type FooFnType = () => void
function bar(fn: FooFnType) {
fn()
}
bar(foo)
type AddFnType = (num1: number, num2: number) => number
const add: AddFnType = (a1: number, a2: number) => {
return a1 + a2
}
TypeScript函数类型解析
在上面的语法中 (num1: number, num2: number) => void,代表的就是一个函数类型:
在某些语言中,可能参数名称num1和num2是可以省略,但是TypeScript是不可以的:
参数的可选类型
我们可以指定某个参数是可选的:
function foo(x: number, y?: number) {
}
foo(20, 30)
foo(20)
这个时候这个参数x依然是有类型的,它是什么类型呢? number | undefined
默认参数
从ES6开始,JavaScript是支持默认参数的,TypeScript也是支持默认参数的:
function foo(y: number, x: number = 20) {
console.log(x, y)
}
foo(30)
这个时候y的类型其实是 undefined 和 number 类型的联合。
剩余参数
从ES6开始,JavaScript也支持剩余参数,剩余参数语法允许我们将一个不定数量的参数放到一个数组中。
function sum(initalNum: number, ...nums: number[]) {
let total = initalNum
for (const num of nums) {
total += num
}
return total
}
console.log(sum(20, 30))
console.log(sum(20, 30, 40))
console.log(sum(20, 30, 40, 50))
可推导的this类型
TypeScript是如何处理this呢?我们先来看一个例子:
const info = {
name: "why",
eating() {
console.log(this.name + " eating")
}
}
info.eating()
export {}
上面的代码是可以正常运行的,也就是TypeScript在编译时,认为我们的this是可以正确去使用的:
- TypeScript认为函数 sayHello 有一个对应的this的外部对象 info,所以在使用时,就会把this当做该对象。
不确定的this类型
但是对于某些情况来说,我们并不知道this到底是什么?
function sayHello() {
console.log(this.name)
}
const info = {
name: "why",
sayHello
}
info.sayHello()
这段代码运行会报错的:
-
这里我们再次强调一下,TypeScript进行类型检测的目的是让我们的代码更加的安全; -
所以这里对于 sayHello 的调用来说,我们虽然将其放到了info中,通过info去调用,this依然是指向info对象的; -
但是对于TypeScript编译器来说,这个代码是非常不安全的,因为我们也有可能直接调用函数,或者通过别的对象来调用函数;
指定this的类型
这个时候,通常TypeScript会要求我们明确的指定this的类型:
type ThisType = { name: string };
function eating(this: ThisType, message: string) {
console.log(this.name + " eating", message);
}
const info = {
name: "why",
eating: eating,
};
info.eating("哈哈哈");
eating.call({name: "kobe"}, "呵呵呵")
eating.apply({name: "james"}, ["嘿嘿嘿"])
export {};
函数的重载
在TypeScript中,如果我们编写了一个add函数,希望可以对字符串和数字类型进行相加,应该如何编写呢?
我们可能会这样来编写,但是其实是错误的:
function add(a1: number | string, a2: number | string) {
if (typeof a1 === "number" && typeof a2 === "number") {
return a1 + a2
} else if (typeof a1 === "string" && typeof a2 === "string") {
return a1 + a2
}
}
add(10, 20)
那么这个代码应该如何去编写呢?
sum函数的重载
比如我们对sum函数进行重构:
- 在我们调用sum的时候,它会根据我们传入的参数类型来决定执行函数体时,到底执行哪一个函数的重载签名;
function add(num1: number, num2: number): number;
function add(num1: string, num2: string): string;
function add(num1: any, num2: any): any {
if (typeof num1 === 'string' && typeof num2 === 'string') {
return num1.length + num2.length
}
return num1 + num2
}
但是注意,有实现体的函数,是不能直接被调用的:
联合类型和重载
我们现在有一个需求:定义一个函数,可以传入字符串或者数组,获取它们的长度。
这里有两种实现方案:
-
方案一:使用联合类型来实现; -
方案二:实现函数重载来实现;
function getLength(args: string | any[]) {
return args.length
}
console.log(getLength("abc"))
console.log(getLength([123, 321, 123]))
function getLength(args: string): number;
function getLength(args: any[]): number;
function getLength(args: any): number {
return args.length
}
console.log(getLength("abc"))
console.log(getLength([123, 321, 123]))
在开发中我们选择使用哪一种呢?
6、TypeScript的类
认识类的使用
在早期的JavaScript开发中(ES5)我们需要通过函数和原型链来实现类和继承,从ES6开始,引入了class关键字,可以更加方便的定义和使用类。
TypeScript作为JavaScript的超集,也是支持使用class关键字的,并且还可以对类的属性和方法等进行静态类型检测。
实际上在JavaScript的开发过程中,我们更加习惯于函数式编程:
但是在封装某些业务的时候,类具有更强大封装性,所以我们也需要掌握它们。
类的定义我们通常会使用class关键字:
类的定义
我们来定义一个Person类:
使用class关键字来定义一个类;
我们可以声明一些类的属性:在类的内部声明类的属性以及对应的类型
类可以有自己的构造函数constructor,当我们通过new关键字创建一个实例时,构造函数会被调用;
- 构造函数不需要返回任何值,默认返回当前创建出来的实例;
类中可以有自己的函数,定义的函数称之为方法;
class Person {
name: string
age: number
constructor(name: string, age: number) {
this.name = name
this.age = age
}
eating() {
console.log(this.name + " eating")
}
}
const p = new Person("why", 18)
console.log(p.name)
console.log(p.age)
p.eating()
export {}
类的继承
面向对象的其中一大特性就是继承,继承不仅仅可以减少我们的代码量,也是多态的使用前提。
我们使用extends关键字来实现继承,子类中使用super来访问父类。
我们来看一下Student类继承自Person:
class Person {
name: string = ""
age: number = 0
eating() {
console.log("eating")
}
}
class Student extends Person {
sno: number = 0
studying() {
console.log("studying")
}
}
class Teacher extends Person {
title: string = ""
teaching() {
console.log("teaching")
}
}
const stu = new Student()
stu.name = "coderwhy"
stu.age = 10
console.log(stu.name)
console.log(stu.age)
stu.eating()
类的成员修饰符
在TypeScript中,类的属性和方法支持三种修饰符: public、private、protected
-
public 修饰的是在任何地方可见、公有的属性或方法,默认编写的属性就是public的; -
private 修饰的是仅在同一类中可见、私有的属性或方法; -
protected 修饰的是仅在类自身及子类中可见、受保护的属性或方法;
public是默认的修饰符,也是可以直接访问的,我们这里来演示一下protected和private。
class Person {
private name: string = ""
getName() {
return this.name
}
setName(newName) {
this.name = newName
}
}
const p = new Person()
console.log(p.getName())
p.setName("why")
export {}
class Person {
protected name: string = "123"
}
class Student extends Person {
getName() {
return this.name
}
}
const stu = new Student()
console.log(stu.getName())
export {}
只读属性readonly
如果有一个属性我们不希望外界可以任意的修改,只希望确定值后直接使用,那么可以使用readonly:
class Person {
readonly name: string
age?: number
readonly friend?: Person
constructor(name: string, friend?: Person) {
this.name = name
this.friend = friend
}
}
const p = new Person("why", new Person("kobe"))
console.log(p.name)
console.log(p.friend)
if (p.friend) {
p.friend.age = 30
}
getters/setters
在前面一些私有属性我们是不能直接访问的,或者某些属性我们想要监听它的获取(getter)和设置(setter)的过程,这个时候我们可以使用存取器。
class Person {
private _name: string
constructor(name: string) {
this._name = name
}
set name(newName) {
this._name = newName
}
get name() {
return this._name
}
}
const p = new Person("why")
p.name = "coderwhy"
console.log(p.name)
静态成员
前面我们在类中定义的成员和方法都属于对象级别的, 在开发中, 我们有时候也需要定义类级别的成员和方法。
在TypeScript中通过关键字static来定义:
class Student {
static time: string = "20:00"
static attendClass() {
console.log("去学习~")
}
}
console.log(Student.time)
Student.attendClass()
抽象类abstract
我们知道,继承是多态使用的前提。
-
所以在定义很多通用的调用接口时, 我们通常会让调用者传入父类,通过多态来实现更加灵活的调用方式。 -
但是,父类本身可能并不需要对某些方法进行具体的实现,所以父类中定义的方法,,我们可以定义为抽象方法。
什么是 抽象方法? 在TypeScript中没有具体实现的方法(没有方法体),就是抽象方法。
-
抽象方法,必须存在于抽象类中; -
抽象类是使用abstract声明的类;
抽象类有如下的特点:
抽象类演练
function makeArea(shape: Shape) {
return shape.getArea()
}
abstract class Shape {
abstract getArea(): number
}
class Rectangle extends Shape {
private width: number
private height: number
constructor(width: number, height: number) {
super()
this.width = width
this.height = height
}
getArea() {
return this.width * this.height
}
}
class Circle extends Shape {
private r: number
constructor(r: number) {
super()
this.r = r
}
getArea() {
return this.r * this.r * 3.14
}
}
const rectangle = new Rectangle(20, 30)
const circle = new Circle(10)
console.log(makeArea(rectangle))
console.log(makeArea(circle))
类的类型
类本身也是可以作为一种数据类型的:
class Person {
name: string = "123"
eating() {
}
}
const p = new Person()
const p1: Person = {
name: "why",
eating() {
}
}
function printPerson(p: Person) {
console.log(p.name)
}
printPerson(new Person())
printPerson({name: "kobe", eating: function() {}})
export {}
7、TypeScript接口的使用
接口的声明
在前面我们通过type可以用来声明一个对象类型:
type InfoType = {name: string, age: number}
对象的另外一种声明方式就是通过接口来声明:
interface IInfoType {
name: string,
age: number
}
可选属性
接口中我们也可以定义可选属性:
interface IInfoType {
name: string
age: number
friend?: {
name: string
}
}
const info: IInfoType = {
name: "why",
age: 18,
friend: {
name: "kobe"
}
}
console.log(info.friend?.name)
console.log(info.name)
info.age = 20
只读属性
接口中也可以定义只读属性:
- 这样就意味着我们再初始化之后,这个值是不可以被修改的;
interface IInfoType {
readonly name: string
age: number
friend?: {
name: string
}
}
const info: IInfoType = {
name: "why",
age: 18,
friend: {
name: "kobe"
}
}
console.log(info.friend?.name)
console.log(info.name)
info.age = 20
索引类型
前面我们使用interface来定义对象类型,这个时候其中的属性名、类型、方法都是确定的,但是有时候我们会遇到类似下面的对象:
interface IndexLanguage {
[index: number]: string
}
const frontLanguage: IndexLanguage = {
0: "HTML",
1: "CSS",
2: "JavaScript",
3: "Vue"
}
interface ILanguageYear {
[name: string]: number
}
const languageYear: ILanguageYear = {
"C": 1972,
"Java": 1995,
"JavaScript": 1996,
"TypeScript": 2014
}
函数类型
前面我们都是通过interface来定义对象中普通的属性和方法的,实际上它也可以用来定义函数类型:
interface CalcFn {
(n1: number, n2: number): number
}
function calc(num1: number, num2: number, calcFn: CalcFn) {
return calcFn(num1, num2)
}
const add: CalcFn = (num1, num2) => {
return num1 + num2
}
calc(20, 30, add)
当然,除非特别的情况,还是推荐大家使用类型别名来定义函数:
type CalcFn = (n1: number, n2: number) => numbers
接口继承
接口和类一样是可以进行继承的,也是使用extends关键字:
- 并且我们会发现,接口是支持多继承的(类不支持多继承)
interface ISwim {
swimming: () => void
}
interface IFly {
flying: () => void
}
interface IAction extends ISwim, IFly {
}
const action: IAction = {
swimming() {
},
flying() {
}
}
接口的实现
接口定义后,也是可以被类实现的:
interface ISwim {
swimming: () => void
}
interface IEat {
eating: () => void
}
class Animal {
}
class Fish extends Animal implements ISwim, IEat {
swimming() {
console.log("Fish Swmming")
}
eating() {
console.log("Fish Eating")
}
}
class Person implements ISwim {
swimming() {
console.log("Person Swimming")
}
}
function swimAction(swimable: ISwim) {
swimable.swimming()
}
swimAction(new Fish())
swimAction(new Person())
swimAction({swimming: function() {}})
交叉类型
前面我们学习了联合类型:
type Direction = "left" | "right" | "center"
还有另外一种类型合并,就是交叉类型(Intersection Types):
-
交叉类似表示需要满足多个类型的条件; -
交叉类型使用 & 符号;
我们来看下面的交叉类型:
type WType = number & string
交叉类型的应用
所以,在开发中,我们进行交叉时,通常是对对象类型进行交叉的:
interface ISwim {
swimming: () => void
}
interface IFly {
flying: () => void
}
type MyType1 = ISwim | IFly
type MyType2 = ISwim & IFly
const obj1: MyType1 = {
flying() {
}
}
const obj2: MyType2 = {
swimming() {
},
flying() {
}
}
interface和type区别
我们会发现interface和type都可以用来定义对象类型,那么在开发中定义对象类型时,到底选择哪一个呢?
- 如果是定义非对象类型,通常推荐使用type,比如Direction、Alignment、一些Function;
如果是定义对象类型,那么他们是有区别的:
字面量赋值
我们来看下面的代码:
interface IPerson {
name: string
age: number
height: number
}
function printInfo(person: IPerson) {
console.log(person)
}
const info = {
name: "why",
age: 18,
height: 1.88,
address: "广州市"
}
printInfo(info)
这是因为TypeScript在字面量直接赋值的过程中,为了进行类型推导会进行严格的类型限制。
- 但是之后如果我们是将一个 变量标识符 赋值给其他的变量时,会进行freshness擦除操作。
8、TypeScript枚举类型
枚举类型是为数不多的TypeScript特性有的特性之一:
enum Direction {
LEFT,
RIGHT,
TOP,
BOTTOM
}
function turnDirection(direction: Direction) {
switch (direction) {
case Direction.LEFT:
console.log("改变角色的方向向左")
break;
case Direction.RIGHT:
console.log("改变角色的方向向右")
break;
case Direction.TOP:
console.log("改变角色的方向向上")
break;
case Direction.BOTTOM:
console.log("改变角色的方向向下")
break;
default:
const foo: never = direction;
break;
}
}
turnDirection(Direction.LEFT)
turnDirection(Direction.RIGHT)
turnDirection(Direction.TOP)
turnDirection(Direction.BOTTOM)
枚举类型的值
枚举类型默认是有值的,比如上面的枚举,默认值是这样的:
当然,我们也可以给枚举其他值:
我们也可以给他们赋值其他的类型:
enum Direction {
LEFT = "LEFT",
RIGHT = "RIGHT",
TOP = "TOP",
BOTTOM = "BOTTOM"
}
let name: string = "abc"
let d: Direction = Direction.BOTTOM
function turnDirection(direction: Direction) {
console.log(direction)
switch (direction) {
case Direction.LEFT:
console.log("改变角色的方向向左")
break;
case Direction.RIGHT:
console.log("改变角色的方向向右")
break;
case Direction.TOP:
console.log("改变角色的方向向上")
break;
case Direction.BOTTOM:
console.log("改变角色的方向向下")
break;
default:
const foo: never = direction;
break;
}
}
turnDirection(Direction.LEFT)
turnDirection(Direction.RIGHT)
turnDirection(Direction.TOP)
turnDirection(Direction.BOTTOM)
9、TypeScript泛型的使用
认识泛型
软件工程的主要目的是构建不仅仅明确和一致的API,还要让你的代码具有很强的可重用性:
什么是类型的参数化?
- 我们来提一个需求:封装一个函数,传入一个参数,并且返回这个参数;
如果我们是TypeScript的思维方式,要考虑这个参数和返回值的类型需要一致
function foo(arg: number):number {
return arg
}
上面的代码虽然实现了,但是不适用于其他类型,比如string、boolean、Person等类型:
function foo(arg: any):any {
return arg
}
泛型实现类型参数化
虽然any是可以的,但是定义为any的时候,我们其实已经丢失了类型信息:
我们需要在这里使用一种特性的变量 - 类型变量(type variable),它作用于类型,而不是值:
function sum<Type>(num: Type): Type {
return num
}
这里我们可以使用两种方式来调用它:
sum<number>(20)
sum<{name: string}>({name: "why"})
sum<any[]>(["abc"])
sum(50)
sum("abc")
泛型的基本补充
当然我们也可以传入多个类型:
function foo<T, E, O>(arg1: T, arg2: E, arg3?: O, ...args: T[]) {
}
foo<number, string, boolean>(10, "abc", true)
平时在开发中我们可能会看到一些常用的名称:
-
T:Type的缩写,类型 -
K、V:key和value的缩写,键值对 -
E:Element的缩写,元素 -
O:Object的缩写,对象
泛型接口
在定义接口的时候我们也可以使用泛型:
interface IPerson<T1 = string, T2 = number> {
name: T1
age: T2
}
const p: IPerson = {
name: "why",
age: 18
}
泛型类
我们也可以编写一个泛型类:
class Point<T> {
x: T
y: T
z: T
constructor(x: T, y: T, z: T) {
this.x = x
this.y = y
this.z = y
}
}
const p1 = new Point("1.33.2", "2.22.3", "4.22.1")
const p2 = new Point<string>("1.33.2", "2.22.3", "4.22.1")
const p3: Point<string> = new Point("1.33.2", "2.22.3", "4.22.1")
const names1: string[] = ["abc", "cba", "nba"]
const names2: Array<string> = ["abc", "cba", "nba"]
泛型约束
有时候我们希望传入的类型有某些共性,但是这些共性可能不是在同一种类型中:
interface ILength {
length: number
}
function getLength<T extends ILength>(arg: T) {
return arg.length
}
getLength("abc")
getLength(["abc", "cba"])
getLength({length: 100})
命名空间namespace
命名空间在TypeScript早期时,称之为内部模块,主要目的是将一个模块内部再进行作用域的划分,防止一些命名冲突的问题。
export namespace Time {
export function format(time: string) {
return "2022-02-22"
}
}
export namespace Price {
export function format(time: number) {
return "222.22"
}
}
10、TypeScript其他内容补充
类型的查找
之前我们所有的typescript中的类型,几乎都是我们自己编写的,但是我们也有用到一些其他的类型:
const el = document.getElementById("why") as HTMLImageElement
大家是否会奇怪,我们的HTMLImageElement类型来自哪里呢?甚至是document为什么可以有getElementById的方法呢?
- 其实这里就涉及到typescript对类型的管理和查找规则了。
我们这里先给大家介绍另外的一种typescript文件:.d.ts文件
那么typescript会在哪里查找我们的类型声明呢?
-
内置类型声明; -
外部定义类型声明; -
自己定义类型声明;
内置类型声明
内置类型声明是typescript自带的、帮助我们内置了JavaScript运行时的一些标准化API的声明文件;
- 包括比如Math、Date等内置类型,也包括DOM API,比如Window、Document等;
内置类型声明通常在我们安装typescript的环境中会带有的;
- https://github.com/microsoft/TypeScript/tree/main/lib
外部定义类型声明和自定义声明
外部类型声明通常是我们使用一些库(比如第三方库)时,需要的一些类型声明。
这些库通常有两种类型声明方式:
方式一:在自己库中进行类型声明(编写.d.ts文件),比如axios
方式二:通过社区的一个公有库DefinitelyTyped存放类型声明文件
-
该库的GitHub地址:https://github.com/DefinitelyTyped/DefinitelyTyped/ -
该库查找声明安装方式的地址:https://www.typescriptlang.org/dt/search?search= -
比如我们安装react的类型声明: npm i @types/react --save-dev
什么情况下需要自己来定义声明文件呢?
声明变量-函数-类
declare let whyName: string
declare let whyAge: number
declare let whyHeight: number
declare function whyFoo(): void
declare class Person {
name: string
age: number
constructor(name: string, age: number)
}
console.log(whyName)
console.log(whyAge)
console.log(whyHeight)
whyFoo()
const p = new Person("why", 18)
console.log(p)
声明模块
我们也可以声明模块,比如lodash模块默认不能使用的情况,可以自己来声明这个模块:
declare module 'lodash' {
export function join(arr: any[]): void
}
声明模块的语法: declare module ‘模块名’ {}。
- 在声明模块的内部,我们可以通过 export 导出对应库的类、函数等;
declare文件
在某些情况下,我们也可以声明文件:
declare module '*.vue' {
import { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
declare module '*.jpg'
declare module '*.jpeg'
declare module '*.png'
declare module '*.svg'
declare module '*.gif'
declare命名空间
比如我们在index.html中直接引入了jQuery:
- CDN地址: https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js
我们可以进行命名空间的声明:
declare namespace $ {
export function ajax(settings: any): any
}
在main.ts中就可以使用了:
axios.get("http://123.207.32.32:8000/home/multidata").then(res: any => {
console.log(res)
})
tsconfig.json文件
tsconfig.json是用于配置TypeScript编译时的配置选项:
- https://www.typescriptlang.org/tsconfig
我们这里讲解几个比较常见的:
五、项目实战
见后续文章
六、项目打包和自动化部署
见后续文章
七、沿途学习代码地址及案例地址
1、沿途学习代码地址
https://gitee.com/wu_yuxin/vue3-learning.git
2、项目案例地址
https://gitee.com/wu_yuxin/vue3-ts-cms.git
八、知识拓展
1、ES6数组与对象的解构赋值详解
数组的解构赋值
基本用法
ES6允许按照一定的模式,从数组和对象中提取值,对变量进行赋值,这被称之为解构(Destructuring)
var a = 1;
var b = 2;
var c = 3;
var [a,b,c] = [1,2,3];
本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。
下面是一些使用嵌套数组进行解构的例子:
let [foo,[[bar],baz]] = [1,[[2],3]];
foo
bar
baz
let [,,third] = ["foo","bar","baz"];
third
let [head,...tail] = [1,2,3,4];
head
tail
let [x,y,...z] = ['a'];
x
y
z
默认值
解构赋值允许制定默认值
var [foo = true] = [];
foo
[x,y='b'] = ['a'];
注意,ES6内部使用严格相等运算符(===),判断一个位置是否有值。
所以,如果一个数组成员不严格等于undefined,默认值是不会生效的。
var [x=1] = [undefined];
x
var [x=1] = [null];
x
如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值:
function f(){
console.log('aaa');
}
let [x=f()] = [1];
上面的代码中,因为x能取到值,所以函数f()根本不会执行。上面的代码其实等价于下面的代码:
let x;
if([1][0] === undefined){
x = f();
}else{
x = [1][0];
}
默认值可以引用解构赋值的其他变量,但该变量必须已经声明:
let [x=1,y=x] = [];
let [x=1,y=x] = [2];
let [x=1,y=x] = [1,2];
let [x=y,y=1] = [];
上面最后一个表达式,因为x用到默认值是y时,y还没有声明。
对象的解构赋值
1、最简单的案例
看下面的案例
let person = {
name: 'yhb',
age: 20
}
let { name, age } = person
console.log(name,age)
如上面注释中所说,声明了变量 name和age,然后分别从对象person中寻找与变量同名的属性,并将属性的值赋值给变量
所以,这里的关键,就是首先要知道对象中都有哪些属性,然后再使用字面量的方式声明与其同名的变量
2、属性不存在怎么办 如果不小心声明了一个对象中不存在的属性怎么办?
或者,实际情况下,可能是我们就是想再声明一个变量,但是这个变量也不需要从对象中获取值,这个时候,此变量的值就是 undefined
let person = {
name: 'yhb',
age: 20
}
let { name, age,address } = person
console.log(name,age,address)
此时,可以给变量加入一个默认值
let { name, age,address='北京' } = person
3、属性太受欢迎怎么办
当前声明了 name 和 age 变量,其值就是person对象中name和age属性的值,如果还有其他变量也想获取这两个属性的值怎么办?
let { name, age, address = '北京' } = person
console.log(name, age, address)
let { name, age } = person
console.log(name, age)
上面的方法肯定不行,会提示定义了重复的变量 name 和 age
那怎么办呢?
难道只能放弃结构赋值,使用老旧的方式吗?
let l_name=person.name
let l_age=person.age
console.log(l_name,l_age)
其实不然!
let {name:l_name,age:l_age}=person
console.log(l_name,l_age)
说明:
声明变量 l_name 并从对象person中获取name属性的值赋予此变量 声明变量 l_age, 并从对象person中获取age属性的值赋予此变量 这里的重点是下面这行代码
let {name:l_name,age:l_age}=person
按照创建对象字面量的逻辑,name 为键,l_name 为值。但注意,这里是声明变量,并不是创建对象字面量,所以争取的解读应该是
声明变量 l_name,并从person 对象中找到与 name 同名的属性,然后将此属性的值赋值给变量 l_name
所以,我们最后输出的是变量 l_name和l_age
console.log(l_name,l_age)
当然这种状态下,也是可以给变量赋予默认值的
let { name:l_name, age:l_age, address:l_address='北京' }=person
4、嵌套对象如何解构赋值
let person = {
name: 'yhb',
age: 20,
address: {
province: '河北省',
city: '保定'
}
}
let {address}=person
let {province}=address
console.log(province)
上面代码一层层的进行结构赋值,也可以简写为如下形式
let {address:{province}}=person
从peson 对象中找到 address 属性,取出其值赋值给冒号前面的变量 address,然后再将 变量address 的值赋值给 冒号 后面的变量 {province},相当于下面的写法
let {province}=address
字符串的解构赋值
1、字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。
const [a, b, c, d, e] = 'hello';
a
b
c
d
e
类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值。
let {length : len} = 'hello';
len
2、JavaScript的 …(展开运算符)
三个连续的点具有两个含义:展开运算符(spread operator)和剩余运算符(rest operator)。
展开运算符
展开运算符允许迭代器在接收器内部分别展开或扩展。迭代器和接收器可以是任何可以循环的对象,例如数组、对象、集合、映射等。你可以把一个容器的每个部分分别放入另一个容器。
const newArray = ['first', ...anotherArray];
剩余参数
剩余参数语法允许我们将无限数量的参数表示为数组。命名参数的位置可以在剩余参数之前。
const func = (first, second, ...rest) => {};
用例
定义是非常有用的,但是很难仅从定义中理解概念。我认为用日常用例会加强对定义的理解。
复制数组
当我们需要修改一个数组,但又不想改变原始数组(其他人可能会使用它)时,就必须复制它。
const fruits = ['apple', 'orange', 'banana'];
const fruitsCopied = [...fruits];
console.log(fruits === fruitsCopied);
fruits.map(fruit => fruit);
它正在选择数组中的每个元素,并将每个元素放在新的数组结构中。我们也可以使用 map 操作符实现数组的复制并进行身份映射。
唯一数组
如果我们想从数组中筛选出重复的元素,那么最简单的解决方案是什么?
Set 对象仅存储唯一的元素,并且可以用数组填充。它也是可迭代的,因此我们可以将其展开到新的数组中,并且得到的数组中的值是唯一的。
const fruits = ['apple', 'orange', 'banana', 'banana'];
const uniqueFruits = [...new Set(fruits)];
fruits.filter((fruit, index, arr) => arr.indexOf(fruit) === index);
串联数组 可以用 concat 方法连接两个独立的数组,但是为什么不再次使用展开运算符呢?
const fruits = ['apple', 'orange', 'banana'];
const vegetables = ['carrot'];
const fruitsAndVegetables = [...fruits, ...vegetables];
const fruitsAndVegetables = ['carrot', ...fruits];
const fruitsAndVegetables = fruits.concat(vegetables);
fruits.unshift('carrot');
将参数作为数组进行传递
当传递参数时,展开运算符能够使我们的代码更具可读性。在 ES6 之前,我们必须将该函数应用于 arguments。现在我们可以将参数展开到函数中,从而使代码更简洁。
const mixer = (x, y, z) => console.log(x, y, z);
const fruits = ['apple', 'orange', 'banana'];
mixer(...fruits);
mixer.apply(null, fruits);
数组切片
使用 slice 方法切片更加直接,但是如果需要的话,展开运算符也可以做到。但是必须一个个地去命名其余的元素,所以从大数组中进行切片的话,这不是个好方法。
const fruits = ['apple', 'orange', 'banana'];
const [apple, ...remainingFruits] = fruits;
const remainingFruits = fruits.slice(1);
将参数转换为数组 Javascript 中的参数是类似数组的对象。你可以用索引来访问它,但是不能调用像 map、filter 这样的数组方法。参数是一个可迭代的对象,那么我们做些什么呢?在它们前面放三个点,然后作为数组去访问!
const mixer = (...args) => console.log(args);
mixer('apple');
将 NodeList 转换为数组 参数就像从 querySelectorAll 函数返回的 NodeList 一样。它们的行为也有点像数组,只是没有对应的方法。
[...document.querySelectorAll('div')];
Array.prototype.slice.call(document.querySelectorAll('div'));
复制对象 最后,我们介绍对象操作。复制的工作方式与数组相同。在以前它可以通过 Object.assign 和一个空的对象常量来实现。
const todo = { name: 'Clean the dishes' };
const todoCopied = { ...todo };
console.log(todo === todoCopied);
Object.assign({}, todo);
合并对象 合并的唯一区别是具有相同键的属性将被覆盖。最右边的属性具有最高优先级。
const todo = { name: 'Clean the dishes' };
const state = { completed: false };
const nextTodo = { name: 'Ironing' };
const merged = { ...todo, ...state, ...nextTodo };
Object.assign({}, todo, state, nextTodo);
需要注意的是,合并仅在层次结构的第一级上创建副本。层次结构中的更深层次将是相同的引用。
将字符串拆分为字符 最后是字符串。你可以用展开运算符把字符串拆分为字符。当然,如果你用空字符串调用 split 方法也是一样的。
const country = 'USA';
console.log([...country]);
country.split('');
3、export ‘defineEmit’ (imported as ‘defineEmit’) was not found in ‘vue’
在学习vue3的顶层编写方式时的父子组件通信的时候,我们会看到一些比较老(2020、2021年初)的博客里面会有使用defineEmit的,但是如果我们用比较新版本的Vue3的话,就会报错。原因是,新版本的Vue3将defineEmit改成了defineEmits了
九、其他知识学习
1、Webpack学习
Webpack从入门到进阶(一)—附沿路学习案例代码
Webpack从入门到进阶(二)—附沿路学习案例代码
Webpack从入门到进阶(三)—附沿路学习案例代码
2、数据可视化-echarts
数据可视化-echarts入门、常见图表案例、超详细配置解析及项目案例
3、Vue2学习
Vue项目开发-仿蘑菇街电商APP
Vue 知识点汇总(上)–附案例代码及项目地址
Vue 知识点汇总(下)–附案例代码及项目地址
4、JavaScript面向对象和设计模式
JavaScript面向对象编程浅析
JavaScript设计模式浅析
5、微前端学习
SingleSpa及qiankun入门、源码分析及案例
|