DOM
DOM:文本对象模型,是HTML和XML文档的编程接口。提供了对文档的结构化的表述,并定义可一种方式可以使从程序中对该结构进行访问,从而改变文档的结构、样式和内容。
DOM操作
- 创建节点:
document.createElement()
、document.createTextNode()
、document.createDocumentFragment()
(创建文档碎片,表示一种轻量级的文档,主要用来存储临时节点,然后把文档碎片的内容一次性添加到DOM中)、document.createAttribute()
- 查询节点:
querySelector()
、querySelectorAll()
、 - 更新节点:
innerHTML
、innerText
、textContent
- 添加节点:
innerHTML
、appendChild()
、insertBefore()
- 删除节点:
removeChild()
如何判断一个元素是否在可视区域内
- offsetTop、scrollTop
- getBoundingClientRect
- Intersection Observe
- offsetTop、scrollTop实现:
function isInViewPortOne(el) {const viewPortHeight = window.innerHeight || document.documentElement.clienHeight || document.body.clientHeightconst offsetTop = el.offsetTopconst scrollTop = document.documentElement.scrollTopconst top = offsetTop - scrollTopreturn top <= viewPortHeight
}
getBoundingClientRect()
实现
function isInViewPortOne(el) {const viewHeight = window.innerHeight || document.documentElement.clienHeight || document.body.clientHeightconst viewWidth = window.innerWidth || document.documentElement.clienWidth || document.body.clientWidthconst {top,right,bottom,left} = el.getBoundingClientRect()return (top > 0 && left > 0 && bottom > 0 && left > 0)
}
BOM
BOM:浏览器对象模型,顶级对象是
window
,表示浏览器的一个实例。
window对象
window.open(url, target)
window.close()
:仅用于关闭window.open()
打开的窗口;- 窗口操作方法:
- moveBy(x, y)
- moveTo(x, y):移动窗体左上角到相对于屏幕左上角的(x,y)点;
- resizeTo(x, y)
- resizeBy(x, y)
- scrollTo(x, y)
- scrollBy(x, y)
location对象
- location.hash:#后面的字符
- location.host:服务器名称+端口号
- location.hostname:域名
- location.href:完整url
- location.port
- location.pathname:服务器下文件路径
- location.protocol:使用协议
- location.search:url查询字符串,
?
后的内容
navigator对象
- navigator.appName
- navigator.geolocation
- navigator.getUserMedia():可用媒体设备硬件关联的流
- navigator.mediaDevices:可用的媒体设备
- …
screen对象
- screen.height
- screen.width
- screen.pixelDepth
- …
history对象
- history.go()
- history.back()
- history.forward()
- history.length
JS的数据类型
类型分类
- 基本类型:包含
Number
、String
、Boolean
、Null
(空对象指针,typeof
判断时候会返回object
)、Undefined
、Symbol
; - 引用类型:包含
Object
、Array
、Function
(还有Date
、RegExp
、Map
、Set
等)。
存储方式
基本数据类型存储在栈中(读取栈中数据),引用数据类型存储在堆中(索引栈地址,读取堆中数据)。
常见问题
- 声明变量时不同的地址分配
- 基本数据类型的值存放在栈中,在栈中存放的是对应的值;
- 引用数据类型的值存放在堆中,在栈中存放的是指向堆内存的地址;
- 不同的类型数据导致赋值变量时的不同
- 基本数据类型赋值,是生成相同的值,两个数据对应不同的地址;
- 引用数据类型赋值,是将保存对象的内存地址复制给另一个变量,两个数据对应指向同一个堆内存中的地址;
var、let、const的区别
- 变量提升:var声明的变量存在变量提升,声明之前调用,值为
undefined
- 暂时性死区:let和const
- 块级作用域:let和const
- 重复声明:let和const不允许重复声明
- 修改声明的变量:const是制度常量。
- 使用
类型转换机制
类型转换分类
- 显式转换:Number()、String()、parseInt()、Boolean()
- 隐式转换
隐式转换注意点
- 对象与基本类型数据比较:对象会先调用
valueOf
方法(如果valueOf
方法继续返回对象,则调用toString
方法),尝试得到一个原始值来进行比较; null
与undifined
比较:==
或者!=
时是相等的,其它情况下不等;NaN
的比较:与任何值都不等。(要检查一个值是否是NaN
,应使用Number.isNaN()
函数)- 布尔值与数字或字符串比较:布尔值会先转换为数字,然后按照数字与数字或者字符串比较规则比较。
- 两个值都为引用类型,则比较他们是否指向同一个对象;
常见问题
Number(undefined)
// NaNparseInt('234sdf2')
// 234null == undefined
// true[] == ![]
// truetypeof null
//object
undefined
和null
与自身严格相等
数据类型检测:typeof
和instanceof
typeof
操作符返回一个字符串,表示未经计算的操作数的类型。使用方法:typeof val
typeof
判断引用类型数据,只能识别出function
,其它均为object
。
instanceof
用于检测构造函数的prototype
属性是否出现在某个实例对象的原型链上。使用方法:object instanceof constructor
。构造函数通过
new
可以实例对象,instanceof
能判断这个对象是否是之前那个构造函数生成的对象。
typeof
和instanceof
用于判断数据类型,均存在弊端。因此,通用的数据类型检测方法为:Object.prototype.toString()
,该方法统一返回[object Xxx]
字符串。
数据类型检测方法封装
const getValType = val => {let type = typeof val;if(type !== 'object') return type;return Object.prototype.toString.call(val).replace(/^\[object (\S+)\]$/, '$1');
}
常用的字符串操作方法
- 增:
+
、${}
拼接以及concat()
等; - 删(创建新的副本):
slice(start, end)
、substr(start, end)
、substring(start, num)
- 改(创建新的副本):
trim()
、toLowerCase()
、repeat()
等; - 查:
chatAt()
、indexOf()
、includes()
、strartWith()
等; - 转换方法:
split()
- 模板匹配:
match()
、search()
、replace()
数组的常用操作方法
- 增:
push()
、unshift()
、splice()
、concat()
等 - 删:
pop()
、shift()
、splice()
、slice()
等 - 改:
splice()
- 查:
indexOf()
、includes()
、find()
等 - 排序:
sort()
、reverse()
等 - 转换:
join()
- 迭代:
some()
、map()
、filter()
、forEach
、every()
等
浅拷贝和深拷贝
浅拷贝和深拷贝的主要区别是在复制对象或数据结构时,拷贝的深度以及对原始数据内部结构的影响。
基本类型传递的是值,数据存储在栈中,引用类型传递的是地址!数据存储在堆中
- 浅拷贝:基本类型拷贝的是基本类型的值;引用类型拷贝:创建一个新对象,只复制原始对象的基本数据类型的字段或引用地址,不复制引用指向的对象,即新对象和原始对象数据均指向同一个引用对象,数据修改会相互影响;
Object.assigin()
slice()
/concat
- 拓展运算符
- 深拷贝:创建一个新对象,递归复制原始对象的所有字段和引用对象,即新对象和原始对象之间的数据相互独立,复制后无直接影响关系。
JSON.stringfly()
:存在弊端,会忽略undefined、symbol和函数- 手写循环递归
常见问题
- 手写一个浅拷贝
const shallowCopyFn = obj => {let result = null;const type = Object.prototype.toSting.call(obj)// 创建一个新对象if(type === '[Object Object]') {result = {}} else if(Object === '[Object Array]'){result = []} else {result = obj}// 对象数据 基本类型和字段赋值for(let prop in obj) {if(obj.hasOwnProperty(prop)) {result[prop] = obj[prop]}}return result
}
浅拷贝或者可以使用Object.assign()
、ES6的展开运算符、concat()
等实现。
- 手写一个深拷贝
function deepClone(obj, hash = new WeakMap()) { if (obj === null) return null; // null 的情况 if (obj instanceof Date) return new Date(obj); // 日期对象直接返回一个新的日期对象 if (obj instanceof RegExp) return new RegExp(obj); // 正则对象直接返回一个新的正则对象 if(typeof obj !== "object") return obj;// 如果循环引用了就用 weakMap 解决 if (hash.has(obj)) return hash.get(obj); let cloneObj = new obj.constructor; // 找到所属类型原型上的constructorhash.set(obj, cloneObj); for (let key of obj) { if(obj.hasOwnProperty(key))cloneObj[key] = deepClone(obj[key], hash)} return cloneObj;
} // 示例
const original = { a: 1, b: { c: 2 }, d: [3, 4], e: new Date(), f: /abc/g, g: function() {} };
const cloned = deepClone(original);
console.log(cloned);
console.log(cloned === original); // false
console.log(cloned.b === original.b); // false
JS的数据结构
数据结构:计算机存储、组织数据的方式。
分类
- 数组:连续的内存空间保存数据,保存的数据个数在内存分配的时候是确认的;
- 栈(Stack):先进后出(LIFO)的有序集合;
- 队列(Queue):先进先出(FIFO)的有序集合;
- 堆(Heap)
- 链表:以键-值对存储的数据结构;
- 字典
- 树
- 图
- 散列表:也称为哈希表,特点是操作很快;
原型和原型链
原型
由于JS中只有对象没有类(ES6之前),因此为了解决数据共享,引入了原型的概念。
原型其实就是一个普通对象,prototype
, 也称为显式原型,主要作用是为其它对象提供共享属性。
- 只有构造函数才有原型;
- 公有属性,可操作;
- 几乎所有对象在创建的时候都会被赋予一个非空的值作为原型对象的引用;
隐式原型:__proto__
- 只有对象(普通对象、函数)具备;
- 私有的对象属性,不可操作;
显示原型
prototype
是构造函数才具备的,普通对象要调用构造函数的方法,就只能通过__proto__
。隐式原型全等于显示原型,即__proto__ === prototype
。
常见问题
- Google中,隐式原型的写法:
[[prototype]]
- 函数的原型是放在
prototype
上; - 对象、数组的原型是放在
__proto__
上; Object.getPrototypeof(obj)
:获取val的原型对象
constructor、原型对象和函数实例三者间的关系
默认情况下,所有的函数的原型对象都会自动获得一个名为
constructor
的属性,指向与之关联的构造函数。
function Person() {}let per = new Person()console.log(Person.prototype === per.__proto__) // true
console.log(Person.prototype.constructor === Person) // true
构造函数的原型和函数实例对象的原型是同一个对象。
常见问题
constructor
用于判断类型:arr.constructor === Array
// true
原型链
原型链其实就是一条访问链路,通过对象特有的原型构成的一种链式结构,用来继承多个引用类型的属性和方法。当试图访问一个对象的属性时,就会在原型链上进行查找,默认情况下,终点就是最初原型对象的原型:
null
。
字符串。数组、构造函数的原型最终都会指向Object
,而Object
的原型指向是null
。
常见问题
__proto__ === prototype
prototype == {}
{}.__proto__ == Object.prototype[].__proto__ === Array.prototype
{}.__proto__ === [].__proto__.__proto__Person.prototype.__proto__ === Object.prototype
Object.prototype.__proto__ === null
总结
- 一切对象都是继承自
Object
,Object
对象直接继承根源对象null
; - 一切的函数对象,都是继承自
Function
对象; Object
对象直接继承自Function
对象;Function
对象的__proto__
会指向自己的原型对象,最终还是继承自Object
。
继承
在JS中,所有引用类型都继承了Object
,而继承也是通过原型链实现的,常见继承方法有8种。
- 原型链继承:把子类的原型指向父类构造函数实例来覆盖子类原型对象。一般用于一个子类继承的情况,避免属性篡改影响。
function Parent(name) {this.name = namethis.hobby= ['吃饭','睡觉']
}
Parent.prototype.getInfo = _ => {console.log(this.name)console.log(this.hobby)
}
function Child() {}
Child.prototype = new Parent()const children1 = new Child()
children1.name = '222'
children1.hobby.push('唱歌')
children1.getInfo() // 输出 222 ['吃饭','睡觉', '唱歌']const children2 = new Child()
children2.name = '333'
children2.getInfo() // 输出 333['吃饭','睡觉', '唱歌']
- 构造函数继承:在子类构造函数中调用父类构造函数,把
this
指向改变为子类对象(借助call
)。
注:父类的引用属性不会被共享,即构造函数继承,只能继承父类的实例属性和方法,不能继承原型属性和方法。
function Parent(name) {this.name = name;this.hobby = ['吃饭', '睡觉']
}
Parent.prototype.getName = function() {return this.name
}
function Child(name) {Parent.call(this, name)
}
const children1 = new Child('小明')
children1.hobby.push('唱歌')
console.log(children1.name + ', ' + children1.hobby) // 输出:小明 ['吃饭','睡觉', '唱歌']const children2 = new Child()
children2.name = '小红'
console.log(children2.name + ', ' + children2.hobby) // 输出:小红 ['吃饭','睡觉']
children2.getName() // 报错
- 组合继承:将原型链继承和构造函数继承结合。主要是使用原型链实现对原型属性和方法的继承,又通过借用构造函数实现对实例属性的继承。因此,组合式继承一般情况下回调用两次父类的构造函数。
function Parent(name) {this.name = name;this.hobby = ['吃饭', '睡觉']
}
Parent.prototype.getInfo = _ =>{console.log(this.name)console.log(this.hobby)
}
function Child(name, age) {Parent.call(this. name) // 第二次调用 this.age = age
}
// 继承Parent的原型 第一次调用 继承父类的属性和方法
Child.prototype = new Parent()
// 修复构造函数的指向
Child.prototype.constructor = Child
// 添加自定义方法
Child.prototype.getAge = _=>{console.log(this.age)
}
注:在上述示例中,Child.prototype = new Parent()
(即第一次调用)实际上是不必要的,因为它会导致父类的构造函数被不必要的调用,从而继承父类实例的所有属性的方法,数据共享。更推荐的做法是使用Object.create(Parent.prototype)
来创建子类的原型对象,即寄生组合式继承。
- 原型式继承
ES5中新增了Object.create()
方法规范了原型式继承。即实现一个对象的继承,不比创建构造函数。
let Parent = {name: '132',hobby: ['吃饭', '睡觉'],getInfo() {console.log(this.name)console.log(this.hobby)}
}
const children1 = Object.create(Parent)
children1.name = '232'
children1.hobby.push('打游戏')
children1.getInfo() // 232 ['吃饭', '睡觉', '打游戏']const children2 = Object.create(Parent)
children2.getInfo() // 132 ['吃饭', '睡觉', '打游戏']
- ES6 extend class 关键字继承
ES6继承是一种语法糖,先将父类实例对象的属性和方法,驾到this
上(super
使用),然后再用子类的构造函数修改this
。
class Parent() {constructor(name) {this.name = name}sayHello() {console.log(`hello, ${this.name}`)}
}
class Child extend Parent {constructor(name) {super(name);}
}
作用域和作用域链
作用域
作用域:即变量和函数有效的区域集合,变量作用域又称为上下文。换句话说,就是代码中国变量和其它资源的可见性。
分类
- 全局作用域
- 局部作用域(函数作用域)
- 块级作用域:ES6中国引入了
let
和const
关键字,局部访问。
作用域链
当使用一个变量时,js引擎会在当前作用域下寻找该变量,如果没有找到,则向上查找,知道找到全局作用域下结束。
执行上下文与执行栈
this
对象
this
关键字是函数运行时自动生成的一个内部对象,只能在函数内部使用,总指向调用它的对象。
this
在函数执行过程中,一旦确定,就不可再更改。
绑定规则
- 默认绑定:严格模式下,不能讲全局对象用于默认绑定,this会绑定到
undefined
; - 隐式绑定:
this
永远指向最后调用它的对象; - new绑定:通过构建函数
new
关键字生成一个实例对象,此时this
指向这个实例对象(new
过程中如果返回一个对象,this
则指向返回的对象;如果返回一个简单类型数据时,this
依旧指向实例对象); - 显式修改:
apply(obj)
、call(obj)
、bind(obj)
是改变函数调用对象的方法。
绑定规则优先级
new
绑定 > 显式绑定 > 隐式绑定 > 默认绑定
apply
、call
、bind
的区别
apply
、call
、bind
的作用都是改变函数执行时的上下文(即this
指向)。
const name = 'Li'
const obj = {name: 'Bo',sayHello() {console.log(this.name)}
}
obj.sayHello() // 输出Bo
setTimeout(obj.sayHello, 0) // 输出Li
上述示例中,由于使用了setTimeout
,在回调中执行obj.sayHello()
,因此在指向环境回到主栈执行时,实在全局执行上下文中的环境执行的,此时this
指向window
,因此输出 Li,因此,需要调整绑定this
指向:
setTimeout(obj.sayHello.call(obj), 0)
三者区别
apply(obj, arrayArgs)
:参数以数组形式传入,改变this
指向后原函数会立即执行,且此方法只是临时改变一次。call(obj, listArgs)
:参数以列表形式传入,改变this
指向后原函数会立即执行,且此方法只是临时改变一次。bind(obj, listArgs)
:参数以列表形式传入(可多次传入)。改变this
指向后不会立即执行,而是返回一个永久改变this指向的函数。
三者相同点
- 三者第一个参数都是
this
要指向的对象,如果是null
、undefined
时,默认指向window
;
手动实现bind
Function.prototype.myBind = function (context) {// 判断是否是函数if(typeof this !== "function") {throw new TypeError('error')}// 获取参数const args = [...arguments].slice(1), fn = this;return function Fn () {// 根据调用方式 传入不同的绑定值return fn.apply(this instanceof Fn ? new fn(...arguments) : context, args.concat(...arguments))}
}
箭头函数
ES6中提供了箭头函数语法,在书写时候就能确定this
指向。
箭头函数不能作为构建函数。
闭包
在JS中,闭包是指一个函数能够访问并操作它外部的变量。闭包的创建主要是在嵌套函数中发生的。及时外部函数执行完毕并返回之后,内部函数依旧可访问和修改这些变量,只是因为内部函数保持了对外部作用域的引用。
一般函数的词法环境在函数返回后就被销毁。但由于闭包保留了对所在词法环境的数据引用,因此创建时所在执行上下文被销毁,但创建所在词法环境依然存在,延长了变量的生命周期。
示例:
const sumFn = _ => {let count = 0return function () {return count += 1}
}
// 创建两个独立的计数器
const sum1 = sumFun()
const sum2 = sumFn()
console.log(sum1()) // 1
console.log(sum1()) // 2
console.log(sum2()) // 1
-
闭包的特性
- 保持变量私有;
- 模拟素有方法;
- 实现封装和抽象,使得代码模块化;
- 实现回调和异步操作;
-
闭包的用途
- 创建私有变量
- 数据封装和隐私;
- 模拟类的私有方法和属性;
- 实现函数工程;
-
闭包的缺点
- 内存消耗:由于闭包保持了对外部变量的引用,因此可能会造成内存消耗增加,甚至内存泄漏;
- 性能考虑:闭包可能会比普通函数的调用要稍慢一些。
-
其它
在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义在对象的构造器中,原因:每个对象的创建,方法都会被重新赋值。
function Obj(name, age) {this.name = namethis.age = age
}
Obj.prototype.getName = function() {return this.name
}
Obj.prototype.getAge = function() {return this.age
}
执行上下文
执行上下文就是代码的执行环境。分为:
- 全局执行上下文:
window
- 函数执行上下文:只有在函数被调用的时候才会被创建
- Eval函数执行上下文:Eval函数中的代码
生命周期
函数执行上下文的生命周期包括3个阶段:
- 创建节点:确定
this
指向 - 执行阶段:执行变量赋值、代码执行(找不到值,则分配
undefined
) - 回收阶段:执行上下文出栈,等待虚拟机回收执行上下文
变量提升
变量提升:由执行上下文和作用域的工作原理决定的。在JS代码执行前,解析器会首先解析代码,找出所有的变量声明(
var
关键字声明),然后在执行之前,讲这些变量提升在其所在作用域的最顶端,这个过程就是变量提升。
变量提升的实际原因
- 编译阶段与执行阶段分离:编译阶段,代码解析,变量和函数声明被提升到作用域顶部;执行阶段,代码按照编写顺序执行;
- 作用域决定:在JS中,作用域由函数决定;
注意
- 只有声明本身会被提升,赋值或者其它逻辑操作依旧在原处执行;
- 使用
let
和const
关键字声明的变量不会被提升(ES6处理);
执行栈(调用栈 - 先进后出结构)
执行栈:用于存储代码执行阶段创建的所有执行上下文。先进后出,从上到下,创建对应的函数执行上下文冰牙人员栈,执行完成被推出,直至结束。
new
操作符具体实现
在JS中,new
操作符用于创建一个给定构造函数的实例对象。
流程实现
- 创建一个新的
object
- 将对象与构造函数通过原型链连接起来
- 将构造函数中的
this
绑定道新建的对象object
上 - 根据构建函数返回类型做判断:原始值责备忽略,对象则需要正常处理使用。
function Person(name, age) {this.name = namethis.age = age
}
const per = new Person('haa', 33)
// 构造函数没有return语句 则将新创建的对象返回
console.log(per) //Person {name: 'haa', age: 33}
手写new操作符
function NNew(Func, ...args) {const obj = {}// 将新对象原型指向构造函数原型对象obj.__proto__ = Func.prototypelet res = Func.apply(obj, args)returm res instanceof Object ? res : obj
}
ES6新增扩展
- 扩展运算符的应用:
...
- 构造函数新增的方法:
Array.from()
(对象转数组)、Array.of()
(一组值转换为数组)
const arr = Array.form({'0':'a','1':'b',length: 2
}) // ['a', 'b']Array.from([1, 2, 4], x=>x+1) // [1, 3, 5]Array.of(3) // [,,,]
Array.of(1,2,4,5) [1, 2, 4, 5]
- 实例对象新增的方法:
copyWithin()
、find()
、keys()
、flatMap()
等 - 空值处理
- sort()排序算法的稳定性
JS的事件模型
事件:HTML文档或浏览器中发生的一种交互操作;
事件流:父子节点事件绑定,触发顺序
事件流
事件流的三个阶段
- 事件捕获阶段:从上到下依次触发;
- 处于目标阶段:
- 事件冒泡阶段:**从下(触发节点)往上(DOM的最高层父节点)**的传播方式。
事件模型
分类
- 原始事件模型(DOM0级)
- 绑定速度快
- 只支持冒泡,不支持捕获
- 同一个事件类型只能绑定一次,后绑定会覆盖之前的;
- 删除事件处理,赋值为
null
即可
- 标准事件模型(DOM2级)
标准事件模型中,一次事件共有三个过程:
- 事件捕获:从
document
一直向下传播到目标元素- 事件处理:触发目标元素的监听元素
- 事件冒泡:从目标元素冒泡到
document
,依次检查和执行相关的监听函数
添加事件监听:
addEventListener(evt, handler, userCapture)
移除事件监听:removeEventListener(evt, handler, userCapture)
useCapture
默认为false
,表示在冒泡过程中执行,设置为true
,表示在捕获过程中执行。
- IE事件模型
IE事件模型共有两个过程:
- 事件处理阶段:事件到达目标元素,触发监听函数;
- 事件冒泡阶段:冒泡到
document
,过程中检查和执行相关监听函数;
添加事件监听:
attachEvent(evt, handler)
移除事件监听:detachEvent(evt, handler)
事件代理(事件委托)
事件代理,就是把一个或一组元素的响应事件的函数委托给外层的元素完成,其在冒泡阶段完成。
应用场景
通常用于动态绑定,减少重复工作。列表项操作(增、删、改、查)示例:
const ulDom = document.getElementById('ulDom')
ulDom.onclick = function (evt) {evt = evt || window.eventconst target = evt.target || evt.srcElemnentif(target.nodeName.toLowerCase() === 'li') {console.log(target.innerText)}
}
事件委托的局限性
focus
、bour
等没有冒泡机制的事件,无法使用;mousemove
、mouseout
这些只能不断通过位置计算定位的,对性能消耗高,不适合事件委托;
事件循环
JS是一门单线程语言,实现单线程非阻塞的方法就是事件循环。主要实现:同步任务进入主线程(主执行栈),异步任务进入任务队列(先进先出)。主线程内的任务执行完毕为空,则会去任务队列读取对应的任务,推入主线程执行,不断重复。
在JS中,所有的任务都可以分为:
- 同步任务:一般会直接进入主线程执行;
- 异步任务:比如
ajax
请求、定时器函数等。
宏任务与微任务
微任务
微任务:一个需要异步执行的函数,执行时机:主函数执行结束之后、当前宏任务执行结束之前。
常见微任务
Promise.then()
MutationOberserve
Proxy
nextTick()
宏任务
宏任务的时间粒度比较大,执行事件间隔是不能精确控制的。
常见宏任务
- script
setTimeout
、setInterval
- UI事件
postMessage
、MessageChanel
setImmediate
、I/O等
关系图
按照关系图,执行机制:
- 执行一个宏任务,如果遇到微任务则先将它推入微任务的事件队列中;
- 当前宏任务执行完,查看微任务的事件队列,将里面的任务依次执行,再继续执行下一个宏任务。
示例:
console.log(1)setTimeout(_=>{console,log(2)
}, 0)new Promise((res, rej) => {console.log('Promise')resolve()
}).then(_=>{console.log('then')
})
console.log(3)
// 执行后 输出
1
Promise
3
then
2 // setTimeout术语新的宏任务,所以要等上一个微任务列表执行完后再执行
async
和 await
async
:异步的意思,await
可以理解为async await
,即等待异步方法执行,阻塞后面的代码。
async
async
函数返回一个Promise
对象。以下示例,两种实现是等效的:
function fnc() {return Promise.resolve('123')
}async function() {return '123'
}
await
正常情况下,await
后是一个Promise
对象,如果不是,则直接返回对应的值。函数运行,遇到await
,则会阻塞下面的代码(加入微任务列表),跳出去执行同步代码。
async function func() {return await 123
}
func().then(val => console.log(val)) // 123
函数缓存
函数缓存,就是将函数运算过的结果进行缓存。
实现函数缓存主要依靠闭包、科利华、高阶函数等。
柯里化:把接受多个参数的函数转换为接受一个单一参数的函数
const add = (x) => {return function (y) {return x+ y}
}
// 使用
add(2)(3) // 5
JS本地存储
JS的本地存储方式主要有:
- cookie
- sessionStorage
- localStorage
- indexedDB
Cookie
cookie,类型为小型文本文件,指某些网站为了辨别用户身份而存储在用户本地终端上的数据。是为了解决HTTP无状态导致的问题。
cookie一般大小不超过4KB,由key-value形式存储,还包含一些有效期、安全性、适用范围的可选属性。
Cookie每次请求都会被发送。修改Cookie,必须保证Domain和Path的值相同。删除Cookie,一般通过设置一个过期时间,使其从浏览器上删除。
Expires
:过期时间Domain
:主机名Path
:要求请求的资源路径必须带上这个URL路径,才可以发送Cookie受不- 标记为
Secure
的cookie只通过HTTPS协议加密的请求发送给服务端。
localStorage
特点
- 持久化存储,除非主动删除,否则不会过期;
- 存储信息在同一域下是共享的;
- 当前页进行 localStorage的增删改的时候,本页不会触发
storage
事件,只会在其它页面触发; - 大小:5M;
- 本质上就是字符串的读取,内容过多会消耗内存空间,导致页面卡顿;
- 受同源策略的限制
使用
localStorage.setItem(key, value)
localStorage.getItem(key)
localStorage.key(num)
:获取第num个键名localStorage.removeItem(key)
localStorage.clear()
sessionStorage
特点
- 会话级别存储,关闭页面则自动清除数据;
- 存储信息在同一域下是共享的;
- 当前页进行 sessionStorage的增删改的时候,本页不会触发
storage
事件,只会在其它页面触发; - 大小:5M;
- 本质上就是字符串的读取,内容过多会消耗内存空间,导致页面卡顿;
- 受同源策略的限制
使用
sessionStorage.setItem(key, value)
sessionStorage.getItem(key)
sessionStorage.key(num)
:获取第num个键名sessionStorage.removeItem(key)
sessionStorage.clear()
应用场景
- 标记用户与跟踪用户行为,使用
cookie
- 适合长期保存在本地的数据(令牌),使用
localStorage
- 敏感账号一次性登录,使用
sessionStorage
- 存储大量数据、在线文档保存编辑历史的情况,使用
indexDB
大文件的断点续传
分片上传
分片上传,就是将上传的文件,按照一定的大小等分割规则,将整个文件分割成多个数据块(chunk),来进行分片上传,上传完成后,再由服务端对所有分片进行汇总整合拼接,生成原始的文件。
断点续传
断点续传,就是在上传或下载时,将上传或下载任务人为的划分为几个部分。每一个部分采用一个线程来进行。如果遇到网络故障,可以从已经完成的部分处开始继续上传或下载未完成的部分。节省时间,提高速度。
实现的两种方式:
- 服务端返回,告诉从哪开始;
- 浏览器端自行处理。
上传或下载过程中在服务器谢伟临时文件,处理完成后,再将此文件重命名为正式文件即可。
实现思路
拿到文件,保存文件唯一标识,切割,分段上传。每次上传一段,根据唯一标识判断此次上传进度,直到全部文件上传完毕。对于上传失败、上传过程中刷星页面等情况,处理的一种常见方法是使用浏览器的存储机制(如localStorage、sessionStorage、IndexedDB或Cookies)来保存上传进度和已上传的文件块信息。
function uploadFile(file, chunkSize = 1024 * 1024) { const totalChunks = Math.ceil(file.size / chunkSize); for (let index = 0; index < totalChunks; index++) { const chunk = file.slice(index * chunkSize, (index + 1) * chunkSize); const formData = new FormData(); formData.append('file', chunk); formData.append('index', index); formData.append('totalChunks', totalChunks); fetch('/upload', { method: 'POST', body: formData }) .then(response => response.json()) .then(data => { console.log('Chunk uploaded', data); }) .catch(error => { console.error('Error uploading chunk:', error); // 保存已上传的块信息 }); }
}
使用场景
- 大文件加速上传
- 流式文件上传
- 网络环境不太行
使用Web worker处理大文件上传
- 新建 fileUploader.js文件(web worker)
self.onmessage = function(e) { const { file, chunkSize } = e.data; const totalChunks = Math.ceil(file.size / chunkSize); for (let index = 0; index < totalChunks; index++) { const chunk = file.slice(index * chunkSize, (index + 1) * chunkSize); const formData = new FormData(); formData.append('file', chunk); formData.append('index', index); formData.append('totalChunks', totalChunks); // 假设你有一个上传函数 uploadChunk(formData, index).then(() => { // 通知主线程该块已上传 self.postMessage({ index: index, status: 'success' }); }).catch(error => { // 通知主线程上传失败 self.postMessage({ index: index, status: 'error', error: error.message }); }); } // 假设的上传函数(需要替换为实际的API调用) function uploadChunk(formData, index) { return new Promise((resolve, reject) => { // 使用fetch或其他HTTP客户端发送formData // 这里只是模拟 setTimeout(() => { if (Math.random() > 0.5) { resolve(); } else { reject(new Error('Upload failed for chunk ' + index)); } }, 1000); }); }
};
主文件(index.js)中,创建Worker,并将文件数据和其它必要数据发送给Worker:
if (window.Worker) { const worker = new Worker('fileUploader.js'); // 假设你有一个文件输入元素 const fileInput = document.querySelector('input[type="file"]'); fileInput.addEventListener('change', function(e) { const file = e.target.files[0]; const chunkSize = 1024 * 1024; // 1MB // 发送文件和块大小到Worker worker.postMessage({ file: file, chunkSize: chunkSize }); // 监听来自Worker的消息 worker.onmessage = function(e) { console.log('Chunk ' + e.data.index + ' uploaded with status: ' + e.data.status); if (e.data.status === 'error') { console.error('Error:', e.data.error); } }; // 监听Worker的错误 worker.onerror = function(error) { console.error('Worker error:', error); }; });
} else { console.log('Your browser doesn\'t support web workers.');
}
ajax
Ajax:即异步的JS和XML,可以在不重新加载整个网页的情况下,与服务器交换数据,并且更新部分网页。
Ajax的原理:通过
XMLHttpRequest
对象向服务器发送异步请求,从服务器获取数据,然后用JS来操作DOM而更新页面。
创建Ajax异步交互需要服务器逻辑进行配合,完成如下步骤:
- 创建 Ajax 的核心对象:
XMLHttpRequest
; - 通过
XMLHttpRequest
对象的open()
方法与服务器建立连接; - 构建请求所需的数据内容,并通过
XMLHttpRequest
对象的send
方法发送给服务器端 - 通过
XMLHttpRequest
对象提供的onreadystatechange
事件监听服务器端的通信状态(XMLHttpRequest.readyState
) - 接受并处理服务器向客户端响应的数据结果
- 将处理结果更新到HTML中
封装:
function ajaxReq (options) {options = options || {}options.type = (options.type || 'GET').toUpperCase()options.dataType = options.dataType || 'json'const datas = options.dataconst xhr = new XMLHttpRequest()if(options.type === 'GET') {xhr.open('GET', `${options.url}?${params}`, true)xhr.send()} else if(options.type === 'POST'){xhr.open('POST', options.url, true)xhr.send(params)}// 监听服务器的通信状态xhr.onreadychange = function(e) {if(xhr.readyState === 4) { // 请求完成if(xhr.status >=200 && xhr.status < 300) {options.success && options.success(xhr.responseText, xhr.responseXML)} else {options.fail && options.fail(xhr.status)}}}
}
// 使用
ajaxReq({type: 'get',datas: {id: 1},url:'https:xxx',success: (text, xml) => {console.log(text)},fail: status => {console.log(status)}
})
防抖和节流
防抖和节流本质上就是优化高频率执行代码的一种手段,减少调用频率,优化体验。
- 防抖(debounce):n秒后再执行该事件,如在n秒内被重复触发,则重新计时(示例:电梯关门,计时关门)。确保事件处理函数在最后一次事件触发后的一段时间内才执行,通常用于用户输入、验证码输入验证等场景;
- 节流(throttle):n秒内只运行一次,如在n秒内重复触发,只有一次生效(示例:火车发车,要准点)。确保事件处理函数在固定时间间隔内只执行一次,通常应用于滚动、搜索联想等;
代码实现
- 防抖:n秒后执行
function debounce(func, wait, immediate) {let timeout;return function (...args) {let context = thisif(timeout) clearTimeout(timeout)if(immediate) {let callNow = !timeout // 第一次会立即执行,后续触发才执行timeout = setTimeout(function() {timeout = null}, wait)if(callNow) {func.apply(context, args)}} else {timeout = setTimeout(function () {func.apply(context, args)}, wait)}}
}
- 节流:n秒内仅执行一次
function throttled(fn, delay = 500) {let timer = nullreturn function(...args) {if(!timer) {timer = setTimeout(_ => {fn.apply(this, args)timer = null}, delay)}}
}
单点登录
单点登录,是目前比较流行的企业业务整合的解决方案之一。
SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
SSO一般需要一个独立的认证中心,子系统的登录均需要通过认证中心,本身不参与登录操作。
当一个系统成功登录以后,认证中心会颁发一个令牌给各个子系统,子系统拿着令牌去获取各自受保护的资源。为了减少频繁验证,一般在授权以后,一定时间内无需再发起认证。
实现方式
- 同域名下的单点登录:使用Cookie,即将Cookie的
domain
设置为当前域的父域,并且服务的Cookie会被子域共享。path
默认为web应用的上下文路径。 - 不同域名下的单点登录(两种方法均支持跨域):
- 方法一:认证中心进行登录,登录成功,将token写入Cookie。应用系统检查当前请求是否有token,没有则跳转登录,有则将当前token写入当前应用系统的Cookie,访问放行。
- 方法二:将认证中心的token保存在localStorage中,前端每次请求,都主动将localStorage的数据传给服务端。前端拿到token后,除了写入自己的localStorage中,还可以通过特殊手段写入其他域的localStorage中(iframe + postMessage)。
上拉加载、下拉刷新
上拉加载的本质就是页面触底时机,自动触发加载请求。
页面触底公式:
const scrollTop = document.documentElement.scrollTop
const scrollHeight = document.body.scrollHeight
const clientHeight = document.documentElement.clientHeight // 浏览器高度// 距离底部还有50的时候就可以开始触发
const isPut = (scrollTop + clientHeight) >= (scrollHeight - 50)
下拉刷新的本质就是页面本身置于顶部时,用户下拉触发操作。
正则表达式
遇到特殊字符,需要使用\
转义哦~
构建正则表达式的方式:
- 字面量创建,其包含在斜杠之间:
/\d+/g
- 调用
RegExp
对象的构造函数:new RegExp("\\d+", g)
匹配规则
- ^:匹配输入开始
- $:匹配输入结束
- *:匹配前一个表达式0次或毒刺
- +:匹配前一个表达式1次或多次,等价于
{1,}
- ?:匹配前一个表达式0次或1次,等价于
${0, 1}
- .:匹配除换行符意外的任何单个字符
- …
标记
- g:全局搜索
- i:不区分大小写搜索
- m:多行搜索
- …
匹配方法
- 字符串方法:
match()
、matchAll()
、search()
、replace()
、split()
- 正则对象方法:
test()
、exec()
常用正则
- 5-20个字符,以字母开头,可带数字,以及_、.的字符串:
/^[a-zA-Z]{1}([a-zA-Z0-9]|[._]){4, 19}$/
函数式编程
纯函数
纯函数就是对给定的输入返回相同输出的函数,并要求所有的数据都是不可变的,即:纯函数=无状态+数据不可变
高阶函数
高阶函数,就是以函数作为输入或者输出的函数。
高阶函数存在缓存的特性,主要是利用了闭包作用。
const doOnce = fn => {let done = falsereturn function() {if(!done) fn.apply(this. fn)else console.log('已处理')done = true}
}
柯里化
柯里化就是把一个多参数函数转换为一个嵌套的一元函数的过程(惰性执行)。
web攻击方式
常见攻击方式:
-
SQL注入:在表单的输入框中输入恶意SQL代码,并通过提交这些字段来执行恶意的SQL语句,从而影响网站的数据库安全。
SQL注入,主要是通过将恶意的Sql查询或添加语句插入到应用的输入参数中,再在后台Sql服务器上解析执行进行的攻击。
SQL注入预防:
- 严格检查输入变量的类型和格式;
- 过滤和转义特殊字符;
- 对访问数据库的Web应用采用防火墙等。
-
跨站脚本攻击(XSS):在Web应用中输入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。
分类:
- 存储型:恶意数据提交,读取返回,解析执行
- 反射型:包含恶意代码的URL,读取、拼接返回给HTML,解析执行
- DOM型:包含恶意代码的URL,前端读取打开(前端自身漏洞)
解决办法
- 在使用innerHTML、outerHTML、document.write()等小心,不要把不可信的HTML插入到页面上。如果使用Vue/react等技术栈,应在
render
阶段避免 innerHTML 的xss攻击隐患; - DOM中的内联事件监听器,如onclick、onload、onmousemove等,a标签的href属性,setTimeout()等,都能把字符串作为代码运行。如果吧不可信的数据拼接到字符串中传递给这些API,很容易产生隐患;
-
跨站请求伪造(CSRF):攻击者优导受害者进入第三方网站,在第三方网站中,向被攻击网站发起跨站请求。
跨站请求伪造,可以通过
get
请求,即通过访问img的页面后,浏览器自动访问目标地址,发送请求。也可以设置一个自动提交的表单发送POST
请求。CSRF的特点:
- 攻击一般发起在第三方网站;
- 攻击利用受害者在被攻击网站的登录凭证,冒充受害者提交操作;
- 跨站请求可以用各种方式:图片URL、超链接、Form表单提交等;部分请求方式可以直接嵌入第三方论坛、文章中,难以追踪。
CSRF的预防:
- 阻止不明外域的访问;
- 提交时要求附加本域才能获取的信息(token等);
-
文件上传漏洞:攻击者可能会利用文件上传功能将恶意软件或脚本上传到目标服务器上,进而执行恶意操作或获取敏感信息。
-
远程命令执行漏洞:攻击者可利用该漏洞在受害机上执行任意命令或程序,进一步控制受害机器。
-
目录遍历攻击:攻击者试图访问服务器根目录之外的目录,并利用特定的符号(如“…/”)来尝试访问受限制的目录或文件。
-
信息泄露攻击:攻击者可能试图通过各种方法获取或猜测敏感信息,如用户密码、密钥等,以便进一步入侵系统。
-
会话劫持:攻击者利用各种技术手段捕获会话信息,然后冒充合法用户进行非法操作。
-
零日攻击:黑客利用尚未被公众发现的漏洞信息进行攻击,使得目标系统无法防范。
JS内存泄漏
JS的内存泄漏,是指计算处理中,由于疏忽或者错误造成程序未能释放已经不在使用的内存,从而造成内存的浪费。
对于持续运行的服务进程,必须及时释放不再用到的内存,否则,内存占用越来越高,影响系统性能,甚至导致程序崩溃。
垃圾回收机制
JS具有自动垃圾回收机制,执行环境会负责管理代码执行过程中使用的内存。
原理:垃圾收集器会定期找出不再继续使用的变量,然后释放内存。
实现方式:
- 标记清除:变量标记进入和离开执行环境,垃圾回收程序会将离开状态,且无被引用的变量做清理销毁‘’
- 引用计数:如果一个值的引用次数为0,就表示这个值不再用了,可以将此销毁释放。
常见内存泄漏情况
- 意外的全局变量(使用严格模式可解决)
- 定时器
- 闭包
- 监听器:
addEventListener
JS数字精度丢失
0.1 + 0.3 === 0.4 // false
存储二进制小数点的偏移量最大为52位,最多可以表达的位数是2^53=90071992547740992,对应科学计数位数是9.0071992547740992,这也是JS最多能表示的精度。他的长度是16,所以可以使用toPrecision(16)
来做运算,超过的精度会自动做凑整处理。
要想解决大数的问题,使用第三方库:bignumber.js
,原理是把所有数字当做字符串,重新实现了计算逻辑,缺点就是性能差。
因此,0.1 + 0.3 === 0.4
为false
,主要是因为计算机存储双精度浮点数需要先把十进制数转换为二进制的科学计数法的形式,然后计算机以自己的规则存储二进制的科学计数法。
由于存储时有位数限制(64位),并且某些十进制浮点数在转换为二进制数时会出现无线循环,造成二进制的舍入操作,当再转换为十进制,就造成了计算误差。
解决方案
使用toPrecision()
凑整,并parseFloat()
转换为数字后显示
function strip(num, precision = 12) {return +parseFloat(num.toPrecision(precision))
}
或者直接使用第三方库:Math.js
、BigDecimal.js
等
尾递归
数组求和
const sum = (arr, total) => {if(arr.lengh === 1) return totalreturn sum(arr, total + arr.pop())
}
数组扁平化
const flatArr = (arr=[], result=[]) =>{arr.forEach(val => {if(Arrar.isArray(val)) {result = result.concat(flat(val, []))} else result.push(val)})
}