目标
了解Modules使用的方法
了解掌握BOM相关api
掌握DOM相关api
了解相关代码规范的内容
Modules
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Modules
ES6之前JavaScript并没有官方的模块化方案,随着网页应用的规模不断扩大,我们需要把js代码拆分到不同的文件,进行多人协作开发,但这样带来很多的问题,比如不同js文件之间很容易出现命名冲突,为了解决这类问题,通常需要借助于匿名函数来创建私有作用域,现在ES6提供了官方的模块化机制。
模块机制有以下特点,与面向对象的概念类似:
每个模块都有自己的作用域,外部模块不能直接访问
模块可以导出变量或方法给外部模块访问,导出的方法可以访问自身模块的内部成员
模块可以导入其他模块导出的方法或变量
下面我们来定义几个简单模块体验一下
scripts/user.js
// 该模块只导出一个默认成员 export default class User { constructor() { this.name = '' } setName(name) { if (name.length >= 2) this.name = name } }
scripts/store.js
// 导入user模块的默认导出成员 import User from './user.js' // _users是模块内部私有成员,不会和其他模块出现命名冲突 let _users = [] // 导出addUser方法 export function addUser(name) { let user = new User() user.setName(name) _users.push(user) } // 导出getUserCount方法 export function getUserCount() { return _users.length }
scripts/main.js
// 通过相对路径导入其他模块,有多个导出方法,可以使用*导出到一个命名空间store import * as store from './store.js' // 或者使用下面解构的写法 // import { addUser, getUserCount } from './store.js' // _name和_users都是模块内部私有成员 let _name = 'Tom' let _users = 0 // 调用store模块导出的方法 addUserstore.addUser(_name) // 调用store模块导出的方法 getUserCount_users = store.getUserCount() console.log(_users)
然后我们在html中通过 script 标签引入入口模块 scripts/main.js
<script src="scripts/main.js" type="module"></script>
注意,这里需要添加 type="module" 来声明该文件是一个模块化JS脚本,通过上面的例子我们可以看到,我么只需要加载一个入口模块 scripts/main.js,其他的依赖会形成树状的依赖关系依次加载。
模块化是JavaScript规模化的基础。
现代浏览器已经能够很好的支持原生模块了,但是为了兼容性和加载效率,我们在实际项目中通常还是会借助于打包工具如 webpack,将多个模块打包成一个文件,在后面的课程中我们会用到。
思考
除了modules,还有其他哪些模块化方案,自行查找资料了解
BOM操作
BOM(browser object model–浏览器对象模型),属于约定俗称,在各自浏览器中都有各自的实现,并没有统一的标准。但大多数的接口都是通用的。BOM提供了独立于内容而与浏览器窗口进行交互的对象,由于BOM主要用于管理窗口与窗口之间的通讯,因此其核心对象是window。BOM由一系列相关的对象构成,并且每个对象都提供了很多方法与属性,常见的对象有:document、location、navigator、screen、history。
window.location
location 接口表示其链接到的对象的位置(URL),所做的修改反映在与之相关的对象上,我们可以通过它来读取、控制当前页面的地址,比如重定向或者重新加载页面
属性
学习location之前我们有必要先来了解一下URL的组成结构,一个常见的URL可能是下面的样子
https://demo.example.com:8080/api/update?name=Tom#hash
1
它可以拆分成下面的部分
href:对应整个URL
protocol:https:
hostname:demo.example.com
port:8080
host:demo.example.com:8080
origin:https://demo.example.com:8080
pathname:/api/update
search:?name=Tom
hash:#hash
location对象包含上面这些属性,如果我们要修改当前页面的地址,也就是重定向到一个新的页面,可以直接修改 location.href,如:
location.href = '/index.html'
这会引起页面的刷新,切换到 /index.html 地址
也可以直接修改链接的hash,hash片段仅对于浏览器起作用,不会被浏览器提交到服务端,对于hash的修改也不会引起页面的刷新,通常用于浏览器记录一些状态比如链接锚点、前端路由等等,如
location.hash = '#test'
方法
平时会用到的方法主要是 reload(),它会让我们重新加载当前页面
window.navigator
该对象表示用户代理的状态和标识,它允许脚本查询它和注册自己进行一些活动,我们可以在浏览器的控制台输入 navigator 来查看它的属性和方法,常用的有
userAgent:客户端代理字符串
language:当前环境的语言
window.screen
返回关于浏览器渲染窗口有关的信息
window.history
该对象允许操作浏览器的曾经在标签页或者框架里访问的会话历史记录,我们通常使用它来控制浏览记录的前进、后退,例如:
history.back() //页面后退 history.forward() // 页面前进 history.go(-1) // 后退一页
DOM操作
DOM(Document Object Model — 文档对象模型)是用来呈现以及与任意 HTML 或 XML文档交互的API。DOM 是载入到浏览器中的文档模型,以节点树的形式来表现文档,每个节点代表文档的构成部分(例如:页面元素、字符串或注释等等)。我们可以通过js调用浏览器提供的API,进行DOM的创建、查询、删除、替换等操作,还可以监听DOM的操作(如鼠标点击、键盘输入)事件,让我们对用户的操作做出响应,如今借助于React之类的框架,我们在实际开发中已经很少需要直接操作DOM了,但是仍然有必要了解一下DOM的基本设计和常用操作。
查询
查询DOM有很多种方式,最常用的是通过 id 或者CSS选择器,自己运行示例代码体验一下
<ul id="list"> <li class="item">1</li> <li class="item">2</li> <li class="item">3</li></ul>
let list = document.getElementById('list') // 命中ul,参数为ID名,注意不带#,不是CSS选择器console.log(list)let first = document.querySelector('.item') // 命中第一个 .itemconsole.log(first)let second = document.querySelector('.item:nth-child(2)') // 可以使用复杂的CSS选择器,命中第二个 .item,console.log(second)let items = document.querySelectorAll('.item') // 得到的是一个包含所有 .item 的 NodeList 集合for (let el of items) { console.log(el)}
创建
我们可以通过 document.createElement 来创建一个DOM元素,然后还可以给它添加子元素
let list = document.createElement('ul')list.id = 'list'for (let i = 0; i < 3; i++) { let item = document.createElement('li') item.className = 'item' item.innerText = `${i + 1}` list.appendChild(item) // 将创建好的元素添加到父节点}document.body.appendChild(list) // 将list添加到body
需要注意的是,createElement 创建好的DOM元素还只是存放在内存中,如果想要显示在页面上,需要插入到页面的指定位置,如上面的代码,最后我们将创建好的 list 节点追加到 body 里面。
删除
我们可以先通过查询得到目标元素的DOM对象,然后调用它的父元素的 removeChild 方法来删除它
let item = document.querySelector('.item')item.parentNode.removeChild(item)let list = document.getElementById('list')list.innerHTML = '' // 可以清空list下面的所有子节点document.body.removeChild(list)
替换
let second = document.querySelector('.item:nth-child(2)')let list = document.getElementById('list')let newItem = document.createElement('li')newItem.className = 'new-item'newItem.innerText = 'new-item'list.replaceChild(newItem, second)
事件监听
网页中最常见的就是鼠标事件和键盘事件,我们可以通过 addEventListener 来监听
鼠标事件
<button id="btn">Click</button>
let btn = document.getElementById('btn')// 监听鼠标点击事件btn.addEventListener('click', event => { console.log('button clicked!')})
键盘事件
<input type="text" id="input" />
let input = document.getElementById('input')input.addEventListener('keypress', event => { // event中包含每一次按键的信息 console.log(event)})input.addEventListener('change', event => { // change事件会在输入框失去焦点之后触发 console.log(event.target.value)})
JSON
JSON是一种数据格式,JSON 不属于 JavaScript,它们只是拥有相同的语法而已。JSON 也不是只能在 JavaScript 中使用,它是一种通用数据格式。很多语言都有解析和序列化 JSON 的内置能力。
JSON 语法支持表示 3 种类型的值。
简单值:字符串、数值、布尔值和 null 可以在 JSON 中出现,就像在 JavaScript 中一样。特殊值 undefined 不可以。
对象:第一种复杂数据类型,对象表示有序键/值对。每个值可以是简单值,也可以是复杂类型。
数组:第二种复杂数据类型,数组表示可以通过数值索引访问的值的有序列表。数组的值可以是任意类型,包括简单值、对象,甚至其他数组。
JSON 没有变量、函数或对象实例的概念。JSON 的所有记号都只为表示结构化数据,虽然它借用了JavaScript 的语法,但是千万不要把它跟 JavaScript 语言混淆。
parse()
let stringJson = { "name": "Bob"}let obj = JSON.parse(stringJson)
stringify
let obj = { name: "Bob",}let stringJson = JSON.stringify(obj)
练习
改造之前的qq音乐首页,直接读取JSON文件内的数据,动态的渲染进页面。
TypeScript
JavaScript是一种动态类型语言,可以随意改变变量的类型,这种特性一方面增加了灵活性,另一方面也使得JavaScript代码变得很容易失控,出现各种类型的错误。TypeScript是微软开源的脚本语言,它在JavaScript的基础上增加了强大类型系统,最终编译成纯净的js执行,TypeScript的类型系统可以让我们在开发阶段避免非常多的类型错误,而且IDE可以提供非常强大的API提示,使得它非常适合构建各种类型和规模的JavaScript应用。现在TypeScript已经成为构建大型Web项目的首选语言,知名项目如 VS Code、Angular、Vue@3 等,都是采用TypeScript开发,我们后面的项目都会采用TypeScript作为示例教学。
TypeScript所包含的内容比较多,这里我们只讲解一些基本的用法,更详细内容需要同学们自行学习,详情请参考课后任务部分
环境搭建
首先更改npm源,只需要执行一次,不用每次都执行
npm config set registry https://registry.npm.taobao.org
npm 全局安装 TypeScript
npm i typescript -g
检测是否安装成功
tsc -v
如果提示了TypeScript版本号,则表示安装成功
创建TS项目
一般我们会为每一个TS项目创建一个新目录,每个项目根目录需要包含一个 tsconfig.json 文件,这个文件是TS编译选项的配置文件,主要作用就是告诉TS编译器应该如何编译我们的工程,具体介绍可以参考下面的链接
https://www.tslang.cn/docs/handbook/tsconfig-json.html
在我们的示例代码中,也包含了 tsconfig.json 的简单配置,针对不同类型的TS项目,配置选项会有所不同,当我们配置好之后,只需要在该目录执行命令 tsc 即可编译该工程下面的TS代码文件到JS,我们还可以通过添加 -w 参数,即 tsc -w ,让TS编译器处于监控状态,一旦该工程有新的TS文件改变,会自动进行编译,不需要再每次手动执行编译。
基础类型
TypeScript支持与JavaScript几乎相同的数据类型,此外还提供了实用的枚举类型方便我们使用,我们可以给变量手动加上类型声明,也可以让TS根据变量的类型自动推导,类型一旦声明,不允许直接修改。
需要注意,不管是基础类型还是高级类型,TS中的类型标记只存在于开发阶段,目的是给编译器检查类型使用是否正确,辅助开发人员发现不必要的错误,提升开发效率和质量,在运行时阶段,这些类型就不存在了。
// 手动添加类型声明let isDone: boolean = false// 根据true的值自动推导isTrue为boolean类型let isTrue = true// number数字类型let num: number = 6// string数字类型let str: string = 'hello'// 数组,使用类型+[]的方式声明let arr_1: number[] = [1, 2, 3]// 可以根据后面的值自动推导arr_2的类型为 string[]let arr_2 = ['a', 'b', 'c']// arr_3无法推导类型,因为后面是一个空数组let arr_3 = []
1
enum 枚举
enum 类型是对JavaScript标准数据类型的一个补充,使用枚举类型可以为一组数值赋予友好的名字,通常用于定义一组有限的状态,比如我们可以定义一个颜色的枚举类型:
enum Color { Red, Green, Blue }let c = Color.Red
any
有一些特殊情况我们无法确定变量将要存储的数据类型,这个时候我们可以使用 any,这样编译器就不会去对它进行类型检查了,例如下面的例子
let notSure: any = 4notSure = 'abc'
注意,应该尽可能避免使用 any,会给程度带来类型黑洞
interface 接口
接口可以用来描述结构化的数据,下面我们来体验一下,我们习惯于使用大写字母I开头来命名Interface
interface IUser { name: string age?: number // 用?来声明一个可选属性}function addUser(user: IUser) { // do something}// 类型匹配IUseraddUser({ name: 'Frank' })// 类型匹配IUseraddUser({ name: 'Frank', age: 21 })// 类型不匹配IUser,可选属性age为number类型addUser({ name: 'Frank', age: 'abc' })
函数
我们需要为函数的参数添加类型声明
function addUser(firstName: string, lastName: string): string { return `${firstName} ${lastName}`}
如果有参数是可选的,可以通过 ? 来声明,但是只能放到参数列表的最后面
// lastName 是一个可选参数function addUser(firstName: string, lastName?: string): string { return `${firstName} ${lastName || ''}`}addUser('Jack')addUser('Jack', 'Lee')
如果返回值是可推导的,可以省略返回值的类型声明
function addUser(firstName: string, lastName: string) { // 从return值可以推导出,返回值是一个string类型 return `${firstName} ${lastName}`}
泛型
在软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑可重用性,组件不仅能够支持当前的数据类型,同时也能够支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。有时候我们需要创建一些功能相同只是数据类型不同的组件,如果为所有需要支持的类型都创建一份代码,显然是非常不合理的。
在 C# 之类的语言中,可以使用 泛型 来创建可重用的组件,一个组件可以支持多种数据类型,这样用户就可以以自己的数据类型来使用组件,相当于类型也作为一种变量来传递。
参考下面的例子,假如我们要创建一个函数,根据我们传入的数据,返回一个包含5个元素的数组
function fill<T>(el: T): T[] { return new Array(5).fill(el)}// arr_1 此时是number[]类型let arr_1 = fill(10)// arr_2 此时是string[]类型let arr_2= fill('abc')
泛型在前端的一个典型使用场景是对于网络请求返回结果的类型声明上面,如下面的例子,我们定义一个通用的网络请求方法,用户可以使用泛型来告诉TS这次网络请求返回的数据结构
interface IUser { name: string age: number}// T是类型变量,用来标注返回值的类型async function request<T>(url: string) { let res = await fetch(url) let json: T = await res.json() // res.json()返回的数据是any类型,这里我们把它主动标记为用户传入的类型T return json}async function run() { // users是IUser[]类型 let users = await request<IUser[]>('/api/user/list') console.log(users) // user是IUser类型 let user = await request<IUser>('/api/user/find?id=1') console.log(user)}run()
下面我们通过 ts-page、ts-todo 两个示例来感受相对完整的代码,并且体验TS带来的重构的便利性。
示例工程只提交了TS源代码,需要先在各自目录下面执行 tsc 命令进行编译,再通过http访问
代码规范
推荐 standard.js
额外任务
TS学习
自行学习下面几个核心章节的内容,该任务不需要提交具体的作业
基础类型
变量声明
接口
类
函数
泛型
枚举
类型推导
模块
API练习
将该文档中所涉及到的API自己编写、运行、调试一遍,弄清楚每一个的基本用法,学有余力的同学可以考虑用TS来编写,该任务不需要提交具体的作业