JavaScript
对象
1)Function
匿名函数
(function (param) {
// body
return result;
})
// 例
(function(a,b){
return a + b;
})
第一种场景:定义完毕后立刻调用
(function(a, b) {
return a + b
})(1, 2)
第二种场景:作为其它对象的方法,例如:
页面有一个元素
<p id="p1">
点我一下
</p>
此元素默认有一个 onclick 方法,会在鼠标单击这个元素后被执行,onclick 方法刚开始是 null,需要赋值后才能使用
document.getElementById("p1").onclick = (function(){
console.log("鼠标单击了...");
});
箭头函数
(参数) => {
// 函数体
return 结果;
}
// 举例
document.getElementById("p1").onclick = () => console.log("aa");
函数的本质是对象
举例查看属性
function abc() {
console.log('bb')
}
console.dir(abc)
- 函数作为方法参数
function a() {
console.log('a')
}
function b(fn) { // fn 将来可以是一个函数对象
console.log('b')
fn() // 调用函数对象
}
b(a)
- 函数作为方法返回值
function c() {
console.log("c")
function d() {
console.log("d")
}
return d
}
c()()
var 与 let 的区别
如果函数外层引用的是 let 变量,那么外层普通的 {} 也会作为作用域边界,最外层的 let 也占一个 script 作用域
let x = 10
if(true) {
let y = 20
function b() {
console.log(x,y)
}
console.dir(b)
}
如果函数外层引用的是 var 变量,外层普通的 {} 不会视为边界
var x = 10
if(true) {
var y = 20
function b() {
console.log(x,y)
}
console.dir(b)
}
如果 var 变量出现了重名,则他俩会被视为同一作用域中的同一个变量
var e = 10
if(true) {
var e = 20
console.log(e) // 打印 20
}
console.log(e) // 因为是同一个变量,还是打印 20
如果是 let,则视为两个作用域中的两个变量
let e = 10
if(true) {
let e = 20
console.log(e) // 打印 20
}
console.log(e) // 打印 10
要想里面的 e 和外面的 e 能区分开来,最简单的办法是改成 let,或者用函数来界定作用域范围
var e = 10
if(true) {
var e = 20
console.log(e)
}
console.log(e)
// 用函数来界定作用域范围
var e = 10
if(true) {
function b() {
var e = 20
console.log(e) // 打印20
}
b()
}
console.log(e) // 打印10
2) Array
语法
// 创建数组
let arr = [1,2,3]
// 获取数组元素
console.log(arr[0]) // 输出 1
// 修改数组元素
array[0] = 5 // 数组元素变成了 [5,2,3]
API
- 遍历
// 遍历数组元素,其中 length 是数组属性,代表数组长度
for (let i = 0; i < arr.length; i++) {
console.log(arr[i])
}
// 遍历数组 of
for (const item of arr) {
console.log(item)
}
- push(添加)、shift(删除)、splice(删除)
let arr = [1,2,3]
arr.push(4) // 向数组尾部(右侧)添加元素, 结果 [1,2,3,4]
arr.shift() // 从数组头部(左侧)移除元素, 结果 [2,3,4]
arr.splice(1,1) // 删除【参数1】索引位置的【参数2】个元素,结果 [2,4]
- join
let arr = ['a','b','c']
arr.join() // 默认使用【,】作为连接符,结果 'a,b,c'
arr.join('') // 结果 'abc'
arr.join('-') // 结果 'a-b-c'
- map(会产生新的对象)
let arr = [1,2,3,6]
// 需求:arr => [10, 20, 30, 60]
arr.map( i => i * 10 ) // [10, 20, 30, 60]
// 内部实现(伪代码)
function map(a) { // 参数是一个函数
let narr = []
for(let i = 0; i < arr.length; i++) {
let o = arr[i] // 旧元素
let n = a(o) // 新元素
narr.push(n)
}
return narr
}
- filter(会产生新的对象)
let arr = [1,2,3,6]
arr.filter( (i)=> i % 2 == 1 ) // 结果 [1,3]
- forEach
let arr = [1,2,3,6]
arr.forEach( (i) => console.log(i) )
arr 的合并及去重
const arrA = [{ id: 1, name: '张三' }, { id: 2, name: '李四' }]
const arrB = [{ id: 2, name: '李四' }, { id: 3, name: '王五' }]
// 合并
const combinedData = [...arrA, ...arrB]
// 去重
const arr = combinedData.reduce((acc, current) => {
// 如果 acc 中还没有当前项,则添加进去
if (!acc.some(item => item.name === current.name)) {
acc.push(current)
}
return acc
}, [])
将对象数组映射成 {key: item}: Object | map
以某个属性作为主键
const arr = [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' }
]
// 使用 reduce 方法创建 Object
const mapObject = arr.reduce((acc, item) => {
acc[item.id] = item
return acc
}, {})
console.log(mapObject)
// 输出:
// {
// "1": { "id": 1, "name": "张三" },
// "2": { "id": 2, "name": "李四" }
// }
// ※※※※※※※※※※※※※※※※※※※※
// 使用点符号访问
console.log(mapObject[1]) // 输出: { id: 1, name: '张三' }
// 或者使用方括号语法
console.log(mapObject['2']) // 输出: { id: 2, name: '李四' }
// ※※※※※※※※※※※※※※※※※※※※
// 检查键是否存在
if (mapObject.hasOwnProperty(1)) {
console.log(mapObject[1])
}
// 或者使用 in 运算符
if (1 in mapObject) {
console.log(mapObject[1])
}
// ※※※※※※※※※※※※※※※※※※※※
// 遍历映射对象的所有键值对
// 使用 for...in 循环
for (let id in mapObject) {
if (mapObject.hasOwnProperty(id)) {
console.log(`ID: ${id}, Name: ${mapObject[id].name}`)
}
}
// 使用 Object.keys() 和 forEach
Object.keys(mapObject).forEach(id => {
console.log(`ID: ${id}, Name: ${mapObject[id].name}`)
})
// 使用 Object.entries() 和解构赋值
Object.entries(mapObject).forEach(([id, item]) => {
console.log(`ID: ${id}, Name: ${item.name}`)
})
ES6 的 Map
对象而不是普通的对象字面量
const arr = [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' }
]
// 使用 Map 构造函数和 Array.prototype.map 创建 ES6 Map
const map = new Map(arr.map(item => [item.id, item]))
console.log(map)
// 输出: Map(2) { 1 => { id: 1, name: '张三' }, 2 => { id: 2, name: '李四' } }
// 访问 Map 中的数据
console.log(map.get(1)) // 输出: { id: 1, name: '张三' }
// ※※※※※※※※※※※※※※※※※※※※
// map 操作见下一节
3) map
const arr = [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' }
]
// 使用 Map 构造函数创建 ES6 Map
const map = new Map(arr.map(item => [item.id, item]))
console.log(map)
// 输出: Map(2) { 1 => { id: 1, name: '张三' }, 2 => { id: 2, name: '李四' } }
获取单个值
console.log(map.get(1)) // 输出: { id: 1, name: '张三' }
// 如果尝试获取不存在的键,返回 undefined
console.log(map.get(3)) // 输出: undefined
检查键是否存在
if (map.has(1)) {
console.log(map.get(1))
}
// 或者直接使用 has 方法
console.log(map.has(1)) // 输出: true
console.log(map.has(3)) // 输出: false
遍历 Map 中的所有键值对
// for...of
for (let [key, value] of map) {
console.log(`ID: ${key}, Name: ${value.name}`)
}
// forEach
map.forEach((value, key) => {
console.log(`ID: ${key}, Name: ${value.name}`)
})
// entries()迭代器
for (let entry of map.entries()) {
console.log(`ID: ${entry[0]}, Name: ${entry[1].name}`)
}
// keys()和values()迭代器
// 遍历所有的键
for (let key of map.keys()) {
console.log(`Key: ${key}`)
}
// 遍历所有的值
for (let value of map.values()) {
console.log(`Name: ${value.name}`)
}
其他常用操作
// 添加新的键值对
map.set(3, { id: 3, name: '王五' })
console.log(map.get(3)) // 输出: { id: 3, name: '王五' }
// 删除键值对
map.delete(2)
// 清空
map.clear()
// 获取大小
console.log(map.size)
4) Object
语法
let obj = {
属性名: 值,
方法名: 函数,
get 属性名() {},
set 属性名(新值) {}
}
举例
let stu3 = {
name: "小白",
age: 18,
study(){
console.log(this.name + "来学习");
}
}
get,set
let stu4 = {
_name: null, /*类似于java中私有成员变量*/
get name() {
console.log("进入了get");
return this._name;
},
set name(name) {
console.log("进入了set");
this._name = name;
}
}
调用get,set
stu4.name = "小白"
console.log(stu4.name)
属性的增删
let stu = {name:'张三'}
stu.age = 18 // 添加属性
delete stu.age // 删除属性
stu.study = function() { // 添加方法
console.log(this.name + '在学习')
}
增加get,set
let stu = {_name:null}
// 给stu后加 get,set方法
Object.defineProperty(stu, "name", {
get(){
return this._name
},
set(name){
this._name = name
}
})
this
- 【落单】的函数
let stu = {
name: "小花",
friends: ["小白","小黑","小明"],
play() {
this.friends.forEach(function(e){
console.log(this.name + "与" + e + "在玩耍");
});
}
}
stu.play()
this.name 所在的函数是【落单】的函数,因此 this 代表 window
- 用箭头函数
let stu = {
name: "小花",
friends: ["小白","小黑","小明"],
play() {
this.friends.forEach(e => {
console.log(this.name + "与" + e + "在玩耍");
})
}
}
stu.play()
this.name 所在的函数是箭头函数,因此 this 要看它外层的 play 函数,play 又是属于 stu 的方法,因此 this 代表 stu 对象
- 不适用箭头函数的做法
let stu = {
name: "小花",
friends: ["小白","小黑","小明"],
play() {
let me = this
this.friends.forEach(function(e){
console.log(me.name + "与" + e + "在玩耍")
})
}
}
原型继承
let father = {
f1: '父属性',
m1: function() {
console.log("父方法")
}
}
let son = Object.create(father)
console.log(son.f1) // 打印 父属性
son.m1() // 打印 父方法
- father 是父对象,son 去调用 .m1 或 .f1 时,自身对象没有,就到父对象找
- son 自己可以添加自己的属性和方法
- son 里有特殊属性
__proto__
代表它的父对象,js 术语: son 的原型对象 - 不同浏览器对打印 son 的
__proto__
属性时显示不同- Edge 打印 console.dir(son) 显示
[[Prototype]]
- Firefox 打印 console.dir(son) 显示
<prototype>
- Edge 打印 console.dir(son) 显示
基于函数的原型继承
出于方便的原因,js 又提供了一种基于函数的原型继承
函数职责
负责创建子对象,给子对象提供属性、方法,功能上相当于构造方法
函数有个特殊的属性 prototype,它就是函数创建的子对象的父对象
**注意!**名字有差异,这个属性的作用就是为新对象提供原型
function cons(f2) {
// 创建子对象(this), 给子对象提供属性和方法
this.f2 = f2;
this.m2 = function () {
console.log("子方法"
}
}
// cons.prototype 就是父对象
cons.prototype.f1 = "父属性";
cons.prototype.m1 = function() {
console.log("父方法")
}
配合 new 关键字,创建子对象
let son = new cons("子属性")
子对象的 __proto__
就是函数的 prototype
属性
4) boolean补充
undefined 和 null
- 执行表达式或函数,没有返回结果,出现 undefined
- 访问数组不存在的元素,访问对象不存在的属性,出现 undefined
- 定义变量,没有初始化,出现 undefined
console.log(1) // 函数没有返回值, 结果是 undefined
let a = 10 // 表达式没有返回值, 结果是 undefined
let b = [1,2,3]
console.log(b[10]) // 数组未定义元素是 undefined
let c = {"name":"张三"}
console.log(c.age) // 对象未定义属性是 undefined
let d
console.log(d) // 变量未初始化是 undefined
- 二者共同点
- 都没有属性、方法
- 二者合称 Nullish
- 二者区别
- undefined 由 js 产生
- null 由程序员提供
Truthy & Falsy
if (xxx), falsy如下:
false
Nullish
(null
,undefined
)0
,0n
,NaN
"" '' ``
长度为零的字符串
容易误当成 falsy 的例子:
"false", "0"
即字符串的 false 和 字符串的零[]
空数组{}
空对象
Promise
基础用法
- 构建一个 promise: 模拟一个异步函数, 随机会经过n时间拿到结果
const setDelay = (millisecond) => {
return new Promise((resolve, reject) => {
if(typeof millisecond != 'number') {
reject(new Error('参数必须时number类型'))
}
setTimeout(() => {
resolve(`延迟 ${millisecond} 毫秒输出`)
}, millisecond)
})
}
- 构建第二个 promise: 模拟一个异步函数, 随机会经过n时间拿到结果
const setDelaySecond = (seconds) => {
return new Promise((resolve, reject) => {
if(typeof seconds != 'number') {
reject(new Error('参数必须时number类型'))
}
setTimeout(() => {
resolve(`延迟 ${seconds} 秒输出`)
}, seconds * 1000)
})
}
链式调用
模拟第一个异步函数拿到结果后调用第二个函数
setDelay(2000)
.then(result => {
const data = result.split(' ')
console.log('一顿操作之后的结果 >>> ', data[1]) // 模拟处理结果
return setDelaySecond(parseInt(data[1] / 1000))
})
.then(result => {
console.log(result)
})
.catch(err => {
console.log(err)
})
reject 示例
then 式链式写法的本质其实是一直往下传递返回一个新的 Promise,也就是说 then 在下一步接收的是上一步返回的 Promise
setDelay(2000)
.then(result => {
console.log(result)
return setDelaySecond('2')
})
.then(result => {
console.log(result)
}, err => {
//console.log(err)
console.log('第二步报错了')
})
.catch(err => {
console.log(err) // 此时已经在前面的reject处理过了,因此不会走到这里
})
.then(result => {
console.log('继续执行') // 依然会走到这里
console.log(result) // 此时是undefined,因为上一个then没有返回一个Promise
})
返回自己定义的值
setDelay(2000)
.then(result => {
let msg = '第一步的值'
return Promise.resolve(msg)
})
.then(result => {
console.log('上一步的值 >>> ', result)
})
.catch(err => {
console.log(err)
})
跳出Promise链式
setDelay(2000)
.then(result => {
return setDelaySecond(1)
})
.then(result => {
console.log('主动跳出链式')
return Promise.reject('跳出链式msg')
})
.then(result => {
console.log('不执行了')
})
.catch(msg => {
console.log('跳出的失败结果 >>> ', msg)
})
串行
arr = [setDelay, setDelay, setDelay]
arr[0](1000)
.then(result => {
console.log(result)
return arr[1](1000)
})
.then(result => {
console.log(result)
return arr[2](1000)
})
.then(result => {
console.log(result)
})
for循环中的串行
for 循环中的串行, 并且逐一将结果推入数组
var res = []
// 初始化
var chain = setDelay(1000 + Math.ceil(Math.random() * 1000))
for (let i = 0; i < 8; i++) {
// 更新Promise链
chain = chain.then(result => {
// 封装结果
res.push(result + ' >>> ' + i)
// 查看结果
console.log(res)
// 执行下一次函数,并且将Promise的结果向下传递
return setDelay(1000 + Math.ceil(Math.random() * 1000))
})
}
通过setDelay的睡眠
const sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay))
// 方法调用
await sleep(1000)
TypeScript
入门
npm install -g typescript
编写 ts 代码
function hello(msg: string) {
console.log(msg)
}
hello('hello,world')
执行 tsc 编译命令
tsc xxx.ts
编译生成 js 代码,编译后进行了类型擦除
function hello(msg) {
console.log(msg);
}
hello('hello,world');
类型
类型 | 例 | 备注 |
---|---|---|
字符串类型 | string | |
数字类型 | number | |
布尔类型 | boolean | |
数组类型 | number[],string[], boolean[] 依此类推 | |
任意类型 | any | 相当于又回到了没有类型的时代 |
复杂类型 | type 与 interface | |
函数类型 | () => void | 对函数的参数和返回值进行说明 |
字面量类型 | “a”|“b”|“c” | 限制变量或参数的取值 |
nullish类型 | null 与 undefined | |
泛型 | <T> ,<T extends 父类型> |
复杂类型
type
type Cat = {
name: string,
age: number
}
const c1: Cat = { name: '小白', age: 1 }
const c2: Cat = { name: '小花' } // 错误: 缺少 age 属性
const c3: Cat = { name: '小黑', age: 1, sex: '公' } // 错误: 多出 sex 属性
interface
interface Cat {
name: string,
age: number
}
const c1: Cat = { name: '小白', age: 1 }
const c2: Cat = { name: '小花' } // 错误: 缺少 age 属性
const c3: Cat = { name: '小黑', age: 1, sex: '公' } // 错误: 多出 sex 属性
可选属性
如果需要某个属性可选,可以用下面的语法
interface Cat {
name: string,
age?: number
}
const c1: Cat = { name: '小白', age: 1 }
const c2: Cat = { name: '小花' } // 正确: age 属性可选
- 可选属性要注意处理 undefined 值
鸭子类型
interface Cat {
name: string
}
function test(cat: Cat) {
console.log(cat.name)
}
const c1 = { name: '小白', age: 1 }
test(c1)
- const c1 并没有声明类型为 Cat,但它与 Cat 类型有一样的属性,也可以被当作是 Cat 类型
方法类型
interface Api {
foo(): void,
bar(str: string): string
}
function test(api: Api) {
api.foo()
console.log(api.bar('hello'))
}
test({
foo() { console.log('ok') },
bar(str: string) { return str.toUpperCase() }
})
字面量类型
function printText(s: string, alignment: "left" | "right" | "center") {
console.log(s, alignment)
}
printText('hello', 'left')
printText('hello', 'aaa') // 错误: 取值只能是 left | right | center
nullish 类型
function test(x?: string | null) {
console.log(x?.toUpperCase())
}
test('aaa')
test(null)
test()
- x?: string | null 表示可能是 undefined 或者是 string 或者是 null
泛型
下面的几个类型声明显然有一定的相似性
interface RefString {
value: string
}
interface RefNumber {
value: number
}
interface RefBoolean {
value: boolean
}
const r1: RefString = { value: 'hello' }
const r2: RefNumber = { value: 123 }
const r3: RefBoolean = { value: true }
可以改进为
interface Ref<T> {
value: T
}
const r1: Ref<string> = { value: 'hello' }
const r2: Ref<number> = { value: 123 }
const r3: Ref<boolean> = { value: true }
- 泛型的要点就是
<类型参数>
,把【类型】也当作一个变化的要素,像参数一样传递过来,这样就可以派生出结构相似的新类型
函数定义也支持泛型
function ref<T>(n: T): Ref<T> {
return { value: n }
}
const v1 = ref("hello"); // Ref<string>
const v2 = ref(123.3333); // Ref<number>
v1.value.toLocaleLowerCase()
v2.value.toFixed(2)
类
关于 TypeScript 与 JavaScript 中的类语法不是重点,class 相关语法只是起到辅助作用,更重要的是前面讲的 interface
基本语法
class User {
name: string;
constructor(name: string) {
this.name = name
}
}
const u = new User('张三')
其实会被编译成这个样子(默认 --target=es3)
var User = /** @class */ (function () {
function User(name) {
this.name = name;
}
return User;
}());
var u = new User('张三');
所以 js 中的 class,并不等价于 java 中的 class,它还是基于原型实现的,原理参考第二章(036、037)
只读属性
class User {
readonly name: string;
constructor(name: string) {
this.name = name
}
}
const u = new User('张三')
u.name = '李四' // 编译错误
- readonly 是 typescript 特有的,表示该属性只读
方法
class User {
readonly name: string;
constructor(name: string) {
this.name = name
}
study() {
console.log(`[${this.name}]正在学习`)
}
}
const u = new User('张三')
u.study()
get,set
class User {
_name: string;
constructor(name: string) {
this._name = name
}
get name() {
return this._name
}
set name(name: string) {
this._name = name
}
}
const u = new User('张三')
console.log(u.name)
u.name = '李四'
console.log(u.name)
- 注意,需要在编译时加上
tsc --target es6 .\xxx.ts
选项 - es6 等价于 es2015,再此之上还有 es2016 … es2022
类与接口
interface User {
name: string
study(course: string): void
}
class UserImpl implements User {
name: string;
constructor(name: string) {
this.name = name
}
study(course: string) {
console.log(`[${this.name}]正在学习[${course}]`)
}
foo() { }
}
const user: User = new UserImpl('张三')
user.study('Typescript')
user.foo() // 错误,必须是接口中定义的方法
继承与接口
interface Flyable {
fly(): void
}
class Animal {
name: string;
constructor(name: string) {
this.name = name
}
}
class Bird extends Animal implements Flyable {
fly() {
console.log(`${this.name}在飞翔`)
}
}
const b: Flyable & Animal = new Bird("小花")
b.fly()
- Flyable & Animal 表示变量是 flyable 类型,同时也是 Animal 类型
方法重写
class Father {
study(): void {
console.log(`father study`)
}
}
class Son extends Father {
study(): void {
super.study()
console.log(`son study`)
}
}
const f: Father = new Son()
f.study()