JS
JS组成部分
- ECMAscript:基本语法
- DOM:操作HTML
- BOM:操作浏览器
JS代码引入方式
- 内联式:标签里写JS代码
- 嵌入式:script标签里写
- 外部式:script标签里src引入
访问JS对象
- 方式:点号和中括号
- 区别:一是中括号可以操作特殊的属性名。二是中括号里面的属性名可以是一个变量,点号不支持变量。
- 判断属性是否在对象里:in操作符。语法:’属性名’**(一定要带引号)**/变量 in 对象
ES6新特性
块作用域(let\const)
类
箭头函数
模板字符串
对象字面量属性赋值简写
1
2
3
4
5
6
7
8
9
10
11
12
13var obj = {
// __proto__
__proto__: theProtoObj,
// Shorthand for ‘handler: handler’
handler,
// Method definitions
toString() {
// Super calls
return "d " + super.toString();
},
// Computed (dynamic) property names
[ 'prop_' + (() => 42)() ]: 42
}对象解构
Promise
Symbol
代理(proxy)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
/*
参数:
target(要使用 Proxy 包装的目标对象)
handler(一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。)
*/
var target = {};
var handler = {
get: function (obj, prop) {
return `Hello, ${prop}!`;
}
};
var p = new Proxy(target, handler);
console(p.c); //Hello, c
/*
可监听的操作: get、set、has、deleteProperty、apply、construct、getOwnPropertyDescriptor、defineProperty、getPrototypeOf、setPrototypeOf、enumerate、ownKeys、preventExtensions、isExtensible。
*/函数默认参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18//Default
function findArtist(name='lu', age='26') {
...
}
//Rest
function f(x, ...y) {
// y is an Array
return x * y.length;
}
f(3, "hello", true) == 6
//Spread
function f(x, y, z) {
return x + y + z;
}
// Pass each elem of array as argument
f(...[1,2,3]) == 6rest
Map + Set + WeakMap + WeakSet
1
2
3
4
5
6
7
8
9
10
11
12
13WeakMap、WeakSet作为属性键的对象如果没有别的变量在引用它们,则会被回收释放掉。
Map原型中的方法有:get、has、keys、set、values
- get:返回key对应的value
- has:返回布尔值判断key是否在map中
- keys:返回迭代器,iter.next().value获取下一个key
- values:返回迭代器,iter.next().value获取下一个value
- set:添加键值对
Set原型中的方法有:add、has、keys、values
- has:返回布尔值判断对应的值是否在set中
- keys:values方法的别名
- values:返回迭代器,iter.next().value获取下一个valueiterators+for of
内置功能模块
1
2(1).具有CommonJS的精简语法、唯一导出出口(single exports)和循环依赖(cyclic dependencies)的特点。
(2).类似AMD,支持异步加载和可配置的模块加载。Math + Number + String + Array + Object APIs
TypedArray、DataView。ArrayBuffer对象作为内存区域可以存放多种类型的数据。同一段内存,不同数据有不同的解读方式,称为视图。ArrayBuffer中有两种类型的视图,一种是类型化数组视图(TypedArray),另一种是数据视图(DataView)。类型化数组视图的数组成员都是同一个数据类型,而数据视图的数组成员可以是不同的数据类型。
严格模式
- 使用:在script标签或函数开头加上’use strict’;
- 必须用var声明变量;禁止自定义的函数中的this指向window;创建eval作用域;对象中的属性不能重名;
数据类型(八种)
基础数据类型:Boolean、Null、Undefined、Number、BigInt、String、Symbol
引用数据类型:Object
Bigint和Number的区别
BigInt和Number两者必须转换为同一种类型才能进行运算,不能混合运算。
BigInt | Number | |
---|---|---|
表示范围 | 可以表示任意大的整数 | ±2^53 - 1 |
使用Math对象中的方法 | 不可以 | 可以 |
互相转化 | BigInt转Number时可能会丢失精度 | 无需担心精度 |
1 | const big1 = 9007199254740991n; |
基础数据类型与引用数据类型的区别
拷贝:
基本数据类型,设原值为a,拷贝后的值为b。则无论是单独修改a还是b,都不会改变另一个值。而引用数据类型,例如对象,设原对象为a,拷贝后的对象为b,则拷贝后修改a或b,另一个对象也会同时改变。比较:
基本数据类型在比较的时候是比较值,而引用数据类型在比较的时候是比较地址。
为什么引用值存放在堆中,原始值存放在栈中呢?
栈是特殊线性表,存取数据的原则是先进后出。而堆的数据结构通常是一个完全二叉树,每个节点存储一个值,整棵树都是经过排序的。
栈的每一块内层空间都是固定大小的,存储能力有限。而堆的内存空间可以动态增长。对象是复杂的结构,它可以自由扩展,比较适合存放在内存空间能够动态增长的地方;而原始值占用的空间相对固定,适合存放在栈中。
还存在一个查找效率的问题。不将原始值放在堆中是因为通过引用到堆中查找实际值是要花费时间的,不如直接通过栈查找原始值快。且将引用值放入堆中通过二叉树查找的搜索效率高于将引用值放在栈中。
typeof
- typeof null = ‘object’
- typeof NaN = ‘number’
- typeof Object = ‘function’
instanceof
概念:检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上
注意点:instanceof
不能直接用于判断基本数据类型,但是可以直接判断引用数据类型
1 | console.log(2 instanceof Number); // false |
日期和时间
1 | - Date():返回当日的日期和时间 |
arguments
特点
- 内置对象
- 有length,可以被遍历
- 是伪数组,无pop()、push()等数组方法
- 可以结合扩展运算符生成数组:[…arguments]
- 如果实参个数多于形参个数,则按照形参的个数进行匹配;如果实参个数小于形参个数,则多余的形参会是undefined,打印出来是NaN。
this绑定规则
- 默认绑定。非严格模式下,this指向window; 严格模式下,函数内部this指向undefined,函数外部不受影响。默认情况下,立即执行函数的this指向window。
- 隐式绑定。函数调用是在某个对象上触发的,即调用位置存在上下文对象。类似于xxx.func()这种调用模式。如果存在xxx.yyy.zzz.func,此时this永远指向最后调用它的对象。
- 隐式绑定丢失。常见的两种丢失:a.使用另一个变量作为函数别名,之后使用别名执行函数;b.将函数作为参数传递时会被隐式赋值。此时,this的指向会启用默认绑定。如下输出2,1.
显式绑定。使用call\apply\bind强行改变this指向。Call和apply函数会立即执行;bind函数会返回新函数,不会立即执行函数;call和apply在于call接收若干参数,而apply接收数组。或者说bind只是改变了this指向,但是不会调用函数。
new绑定。以下输出zc 18。
- 箭头函数。箭头函数没有自己的this,它的this指向外层作用域的this,且指向函数定义时的this而非执行时。
prototype
- 原型对象
每创建一个函数,解析器就会向函数中添加一个属性prototype。也就是说prototype只有函数对象上有。这个属性对应着一个对象,这个对象就是原型对象。如果函数以普通函数形式调用prototype则没有任何作用。当函数以构造函数的形式调用时,它所创建的对象中都会有一个隐含的属性,指向该构造函数的原型对象,可以通过__proto__
来访问该属性。
- 原型对象的特点
原型对象就相当于一个公共区域,所有同一个类的实例都可以访问到这个原型对象,可以将对象中共有的内容,统一设置到原型对象中。
当我们访问对象的一个属性或方法时,它会先在对象自身中寻找,如果有则直接使用,如果没有则会去原型对象中寻找,如果找到则直接使用。
使用in检查对象中是否存在某个属性时,如果对象中没有但是原型中有,也会返回true.可以使用对象的hasOwnProperty()来检查对象自身中是否含有该属性。Object对象的原型没有原型为null。
__proto__
、prototype、constructor
- 获得原型的方法
f1.__proto__
- Foo.prototype
- f1.constructor.prototype
- Object.getPrototypeOf(f1)
箭头函数
模块化
- 模块化的目的
- 解决命名冲突
- 提高复用性
- 提高代码可维护性
- 整体理解
模块是实现一个特定功能的一组方法
- 模块的几种形式
- 将函数作为模块。由于函数具有独立作用域,故可将函数作为模块。几个函数作为一个模块,但这种方式容易导致全局变量的污染,并且模块之间没有关系。
- 对象的写法。可以解决将函数作为模块的某些缺陷,但是这种方法会暴露所有的模块成员,外部代码可以修改内部属性的值。
- 立即执行的匿名函数。利用闭包来实现模块私有作用域的建立,同时不会对全局作用域造成污染。
- 模块规范
- commonJS。通过require来引入模块,通过module.exports定义模块的输出接口。这种模块加载方案是服务器端的解决方案,以同步的方式来引入模块的。在服务器端,文件都存储在本地磁盘,读取比较快,以同步的方式加载没有问题。在浏览器端,模块加载需要使用网络请求,故使用异步加载方式更合适。导出值是可以被修改的。
- AMD。异步加载模块。AMD在模块加载完成后就会执行该模块,等所有模块加载完成后就会进入require的回调函数。模块的加载不影响后面语句的执行,所有依赖这个模块的语句都定义在一个回调函数里,等加载完成后再执行回调函数。require.js实现了AMD规范。
- CMD。异步加载模块。sea.js实现了CMD规范。它和require.js的区别在于模块定义时对依赖的处理不同和对依赖模块的执行时机的处理不同。
- ES6方案。import 和export导入导出模块。不能动态加载模块,只能在该文件的最顶部声明要导入的文件。导出值都是可读的,不能修改。
1 | //在浏览器中使用es module的方法 |
- export
- 默认导出:export default Person
- 按需导出:export {age, name, sex}
- 混合导出:export default和export可以同时使用不影响
- 默认导入:import Person from ‘person’
- 按需导入:import {age, sex, name} from ‘person’
- 混合导入:必须先导入默认导出的,再导入单个导入值
- AMD和CMD的区别
- 模块定义时对依赖的处理不同。AMD依赖前置,在定义模块的时候就要声明其依赖的模块。而CMD就近依赖,只有在用到某个模块的时候再去require。
- 对依赖模块的执行时机不同。AMD和CMD都是异步加载。但是AMD在依赖模块加载完成后就直接执行依赖模块,依赖模块的执行顺序和书写顺序不一定一致。而CMD在依赖模块加载完成后并不执行,等到所有的依赖模块都加载好之后,进入回调函数逻辑,遇到require语句的时候才执行对应的模块。模块的执行顺序和书写顺序一致。
- CommonJS和ES6 模块的区别
- CommonJS输出的是值拷贝,ES6输出的值引用
- CommonJS模块是运行时加载,ES6是编译时加载
- CommonJS是动态语法可以写在判断里,ES6是静态语法只能写在顶层
WeakMap
概念:是一组键/值对的集合,其中的键是弱引用。其键必须是对象,而值可以是任意的。一个对象如果被弱引用所引用,则被认为是不可访问的,并在任何时刻都能被回收。
强引用与弱引用:默认创建的为强引用,只有手动置为null才会被垃圾回收机制进行回收。而弱引用则会被垃圾回收机制自动回收。
async / await
- async函数返回Promise对象,必须等到内部所有的await命令的Promise对象执行完,才会发生状态改变。
- await表达式:await在等待一个表达式的计算结果。如果await表达式的运算结果不是一个Promise对象,那么这个值就是它最终的运算结果。如果await表达式的运算结果是promise对象,那么它会阻塞后面的代码,等到这个promise对象resolve,得到resolve的值作为表达式的运算结果。await右侧的表达式一般为promise对象,但也可以是其他值。如果表达式是promise对象,await返回的是promise成功的值。如果表达式是其他值,直接将此值作为await的返回值。
- 当async函数中有一个await出现rejectd状态时,后面的await都不会执行。可以通过添加try/catch。
1 | async function doIt() { |
undefined、undeclared、null
- undefined表示已在作用域中声明但还没有赋值的变量
- undeclared表示未在作用域中声明的变量
- null主要赋值给一些可能会返回对象的变量,作为初始化
作用域和作用域链
- 作用域
- 概念:作用域是定义变量的区域,它有一套访问变量的规则,这套规则用来管理浏览器引擎如何在当前作用域以及嵌套的作用域中根据变量进行变量查找。
- 分类:ES6之前只有全局作用域和函数作用域,ES6之后新增块级作用域(let\const)。
- 作用域链
- 概念:在作用域的多层嵌套中查找自由变量的过程是作用域链的访问机制。层层嵌套的作用域,通过访问自由变量形成的关系叫做作用域链。查找一个变量的时候,JS会尝试在当前作用域下寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域。如果全局作用域仍然找不到,则报错。
- 本质:指向变量对象的指针列表。变量对象是一个包含执行环境中所有变量和函数的对象。作用域链的前端始终是当前执行上下文的变量对象,全局执行上下文的变量对象始终是作用域链的最后一个对象。
- 词法作用域
词法作用域又叫静态作用域,变量被创建时就确定好了,而非执行阶段确定的。也就是说写好代码时变量的作用域就确定了,JS遵循的就是词法作用域。
执行上下文
String的原生方法
正则表达式
- 边界
^ | 开头 |
---|---|
$ | 结尾 |
\b | 边界 |
\B | 与\b相反 |
(?=p) | 符合p子模式前面的那个位置 |
(?!p) | (?=p)匹配到的位置之外的位置都是属于(?!p) |
(?<=p) | 符合p后面的那个位置 |
(?<!p) | (?<=p)之外的所有位置 |
- 量词
{m,} | 至少出现m次 |
---|---|
{m} | 出现m次 |
? | 出现0次或者1次 |
+ | 至少出现1次 |
* | 出现0次或多次 |
- 常用
\w | 字符 |
---|---|
\d | 数字 |
- 修饰符
JS延迟加载的方式
- JS加载、解析和执行都会阻塞页面渲染
- 将JS脚本放在文档的底部,来使JS脚本尽可能的在最后来加载执行
- 给JS脚本添加defer属性,这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞了。多个设置了defer属性的脚本按规范来说最后是顺序执行的。
- 给JS脚本添加async属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行JS脚本,如果这个时候文档没有解析完成的话同样会阻塞。多个async属性的脚本的执行顺序不可预测。
- 动态创建DOM标签的方式。可以对文档的加载事件进行监听,当文档加载完成后再动态的创建script标签来引入JS脚本。
JS运行机制
- JS单线程
- 概念:同一时间只能做一件事。js单线程主要与它的用途有关。它主要用途是与用户交互,以及操作DOM。使用单线程能够消除同步问题。
- JS事件循环
- 任务分类:同步任务、异步任务
宏队列与微队列
- 概念:JS中用来存储执行回调函数的队列
- 宏队列:用来保存待执行的宏任务(回调),比如:定时器回调/DOM事件回调/AJAX回调
- 微队列:用来保存待执行的微任务(回调),比如:promise的回调/MutationObserver的回调
var \ let \ const区别
- var变量会挂载到window上,而let和const不会
- var声明变量存在变量提升,而let和const不会
- let和const形成块级作用域
- 同一作用域下let和const不能声明同名变量,而var可以
- let和const存在暂存死区,也就是在声明变量之前,该变量是不可用的。
为什么var可以重复声明
因为编辑器会判断是否已经声明过同名变量,如果声明过就忽略var,直接赋值。
Proxy
- 概念:在目标对象之前架设一层拦截,外界对该对象的访问,都必须先通过这层拦截。也就是提供了一种机制,可以对外界的访问进行过滤和改写。
1 | let validator = { |
Generator
概念:通过yield关键字,将函数的执行流挂起,通过next()方法可以切换到下一个状态,为改变执行流程提供了可能,从而为异步编程提供解决方案。
规则:yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
1 | //没明白 |
异步编程方式
- 回调函数
- 事件监听
- 发布 / 订阅
- Promise
setTimeout \ setInterval
定时器指定的时间间隔,表示的是何时将定时器的代码添加到消息队列,而不是何时执行代码。所以真正何时执行代码的时间是不能保证的,取决于何时被主线程的事件循环取到,并执行。每个setTimeout产生的任务会直接push到任务队列中;而setInterval在每次把任务push到任务队列前,都要进行一下判断(看上次的任务是否仍在队列中)。
闭包
- 概念
闭包就是能够读取其他函数内部变量的函数。例如在JS中,只有函数内部的子函数才能读取局部变量,所以闭包可以看成是一个函数内部的函数。本质上,闭包是将函数内部和函数外部连接起来的桥梁。闭包的特点:1.在函数外部操作读取了函数内部的值 2.闭包对应的函数中的变量是常驻内存的。闭包的条件:1.函数嵌套 2.子函数要使用函数内部的变量。
- 应用
- 作为缓存,第二次使用对象时候,可以不用新建对象。单例模式的实现等。
- 实现封装,对访问内容添加权限,实现类似private\protect\public的效果
- 缺点
闭包中的变量是常驻内存的,故其会造成内存泄漏。解决的方案:将外部调用闭包函数的变量最终赋值为null。
使用场景
创建私有变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17var Counter = (function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
})();延长变量的生命周期
计数器、延迟调用、回调等闭包的应用,其核心思想还是创建私有变量和延长变量的生命周期
柯里化函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// 假设我们有一个求长方形面积的函数
function getArea(width, height) {
return width * height
}
// 如果我们碰到的长方形的宽老是10
const area1 = getArea(10, 20)
const area2 = getArea(10, 30)
const area3 = getArea(10, 40)
// 我们可以使用闭包柯里化这个计算面积的函数
function getArea(width) {
return height => {
return width * height
}
}
const getTenWidthArea = getArea(10)
// 之后碰到宽度为10的长方形就可以这样计算面积
const area1 = getTenWidthArea(20)
// 而且如果遇到宽度偶尔变化也可以轻松复用
const getTwentyWidthArea = getArea(20)
call \ apply
- call和apply方法都能执行函数,用法:函数名.call()或函数名.apply();
- call和apply的区别在于,call给函数传递值时是依次传递的,而apply给函数传递值时需要通过一个数组。
防抖与节流
- 防抖
- 概念:一个会频繁触发的函数,在规定的时间内,只让最后一次生效,前面的不生效。在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
- 作用:防止用户重复操作
- 适用场景:用户点击事件
- 如果一直点击的话就不会被执行
- 节流
- 概念:一个函数执行一次后,只有大于设定的执行周期后,才会执行第二次。
- 作用:性能优化。比如有个需要频繁触发的函数,出于优化性能角度,在规定时间内,只让函数触发的第一次生效,后面不生效。通过节流函数,可以极大的减少函数执行的次数,从而节约性能。
- 常见的函数节流应用:oninput、onkeypress、onscroll、onresize等
Promise
- 作用
将异步操作以同步流程表达,避免回调地狱问题。Promise本质是状态机,通过设定不同的状态来执行不同的操作。
- 三个状态
初始化状态[pending]、成功状态[resolved]、失败状态[rejected]。状态只能改变一次,再次改变无效。
- promise.then()返回的新promise的结果状态由什么决定?
- 简单表达:由then()指定的回调函数执行的结果决定
- 详细表达:a.如果抛出异常,新promise变为rejected,reason为抛出的异常;b.如果返回的是非promise的任意值,新promise变为resolved,value为返回的值;c.如果返回的是另一个新promise,此promise的结果就会成为新promise的结果。
- promise串联多个操作任务
- 通过then的链式调用串联多个同步/异步任务
- 异步任务需要返回Promise对象,而同步任务可以直接返回
- promise异常穿透
- 当使用promise的then链式调用时,可以在最后指定失败的回调
- 前面任何操作出了异常,都会传到最后失败的回调处理
- 中断promise链
- 当使用promise的then链式调用时,在中间中断,不再调用后面的回调函数
- 方法:在回调函数返回一个pending状态的promise对象
- 回调地狱
回调函数获取异步操作的返回值的原理?回调函数作为实参传递给异步函数的形参,所以回调函数是在异步函数之内执行的,那么回调函数就可以获取异步操作的返回值。
什么是回调地狱?回调函数嵌套调用,外部回调函数异步执行的结果是嵌套的回调函数执行的条件。
回调地狱的缺点?不便于阅读/不便于异常处理。
- promise的缺点
- 一旦新建了就会立即执行,中途无法取消
- 如果不设置回调函数,promise内部抛出的错误不会反应到外部
- 当promise处于Pending状态时,无法得知目前进展到哪一个阶段
==与===
Symbol
- symbol 类型的值可以作为对象的属性标识符使用
- 创建一个 symbol 的值需要使用 Symbol() 函数,但不能使用 new 命令。
- Symbol() 方法每次都会创建一个新的值,且不会注册到全局。Symbol.for() 方法则会先查找命名参数是否在全局中注册过,如果注册过的就不会创建新的值,而是会直接返回,所以我们可以使用到相同的 symbol 值。
- Symbol.keyFor() 方法表示获取一个 symbol 的值在全局中注册的命名参数 key,只有使用 Symbol.for() 创建的值才会有注册的命名参数,使用 Symbol() 生成的值则没有。
- Symbol值作为对象属性名时,不能使用点运算符。因为点运算符后面总是字符串,所以不会读取Symbol作为标识名所指代的那个值,导致a的属性名实际上是一个字符串,而不是一个 Symbol 值。
JSON.stringfy()
原值 | 转换后的值 |
---|---|
NaN | null |
Infinity | null |
undefined | undefined |
任意函数 | undefined |
symbol | undefined |
对象 | 字符串 |
布尔值、数字、字符串 | 原始值对应的字符串 |
数组方法
- splice
用于删除、添加数组元素,原数组会被修改,并且返回修改后的数组。
1 | var a = [1,2,3,4,5]; //定义数组 |
- slice
能够截取数组中指定区段的元素,并返回这个子数组,不会修改原数组。该方法包含两个参数,分别指定截取子数组的起始和结束位置的下标。
1 | //如果仅指定一个参数,则表示从该参数值指定的下标位置开始,截取到数组的尾部所有元素。 |
- push
- pop
- shift
- unshift
- sort
- concat
- reduce
用于累加数组
1 | Arr.reduce(callback, [initialValue]) |
JS全局函数
类型转换机制
是什么?
变量的数据类型在编译阶段无法获取,只能等程序运行时才知道。各种运算符对数据类型是有要求的,如果参与运算的操作数的类型与预期不符,那么会触发类型转换机制。
常见的类型转换机制:
- 强制类型转换(显示转换)
- 自动类型转换(隐式转换)
显示转换
可以清楚的知道发生了类型转换,常见的方法有:
- Number()
- parseInt()
- String()
- Boolean()
Number():将任意类型的值转化为数值。只要有一个字符无法转成数值,整个字符串就会被转为NaN
。
parseInt():逐个解析字符,遇到不能转换的字符就停下来
String():将任意类型的值转化成字符串
Boolean():将任意类型的值转为布尔值
隐式转换
常见的两种发生隐式转换的场景:
比较运算(==、!=、>、<)、if、while需要布尔值的地方
算数运算(+、-、*、/、%)
运算符两边的操作数不是同一类型的
自动转换为布尔值
在需要布尔值的地方,就会将非布尔值的参数自动转为布尔值,系统内部会调用Boolean
函数。
可以得出个小结:undefined、null、false、+0、-0、NaN、”” 会被转化成false
,其他都换被转化成true
。
自动转换为字符串
遇到预期为字符串的地方,就会将非字符串的值自动转为字符串。具体规则是:先将复合类型的值转为原始类型的值,再将原始类型的值转为字符串。常发生在+
运算中,一旦存在字符串,则会进行字符串拼接操作。
1 | '5' + {} // "5[object Object]" |
自动转换成数值
除了+
有可能把操作数转为字符串,其他运算符都会把操作数自动转成数值
==与===
等于操作符==
等于操作符在比较中会先进行类型转换,再确定操作数是否相等。
遵循的规则:
- 两个都为简单类型,字符串和布尔值都会转换成数值,再比较
- 简单类型与引用类型比较,对象使用valueOf()转化成其原始类型的值,再比较
- 两个都为引用类型,则比较它们是否指向同一个对象
- null 和 undefined 相等
- 存在 NaN 则返回 false
全等操作符===
全等操作符只有两个操作数在不转换的前提下相等才返回true。需要操作数的类型和值都相同。
使用场景
在比较null
的情况的时候,我们一般使用相等操作符==
。
1 | const obj = {}; |
除了在比较对象属性为null
或者undefined
的情况下,我们可以使用相等操作符(==),其他情况建议一律使用全等操作符(===)。
浅拷贝与深拷贝
浅拷贝的方法
- Object.assign
- Array.prototype.slice(begin, end):返回一个新的数组对象,这一对象是一个由
begin
和end
决定的原数组的浅拷贝(包括begin
,不包括end
)。原始数组不会被改变。end如果不写则到数组末尾。 - Array.prototype.concat()
- 扩展运算符
执行上下文与执行栈
执行上下文
执行上下文是一种对Javascript
代码执行环境的抽象概念。
执行上下文的类型分为三种:
- 全局执行上下文:只有一个,浏览器中的全局对象就是
window
对象,this
指向这个全局对象 - 函数执行上下文:存在无数个,只有在函数被调用的时候才会被创建,每次调用函数都会创建一个新的执行上下文
- Eval 函数执行上下文: 指的是运行在
eval
函数中的代码,很少用而且不建议使用
执行栈
执行栈用于存储在代码执行期间创建的所有执行上下文。当Javascript
引擎开始执行你第一行脚本代码的时候,它就会创建一个全局执行上下文然后将它压到执行栈中。每当引擎碰到一个函数的时候,它就会创建一个函数执行上下文,然后将这个执行上下文压到执行栈中。引擎会执行位于执行栈栈顶的执行上下文(一般是函数执行上下文),当该函数执行结束后,对应的执行上下文就会被弹出,然后控制流程到达执行栈的下一个执行上下文。
ajax原理
是什么?
可以在不重新加载整个网页的情况下,与服务器交换数据,并且更新部分网页。Ajax
的原理简单来说通过XmlHttpRequest
对象来向服务器发异步请求,从服务器获得数据,然后用JavaScript
来操作DOM
而更新页面。
过程
实现 Ajax
异步交互需要服务器逻辑进行配合,需要完成以下步骤:
- 创建
Ajax
的核心对象XMLHttpRequest
对象 - 通过
XMLHttpRequest
对象的open()
方法与服务端建立连接 - 构建请求所需的数据内容,并通过
XMLHttpRequest
对象的send()
方法发送给服务器端 - 通过
XMLHttpRequest
对象提供的onreadystatechange
事件监听服务器端的通信状态 - 接受并处理服务端向客户端响应的数据结果
- 将处理结果更新到
HTML
页面中
正则表达式
是什么?
定义规则来匹配字符串。正则表达式是对象。
有两种创建方式:
- 字面量创建,其由包含在斜杠之间的模式组成
- 调用
RegExp
对象的构造函数
规则
贪婪模式:在匹配过程中,尝试可能的顺序是从多往少的方向去尝试。
懒惰模式:惰性量词就是在贪婪量词后面加个问号。表示尽可能少的匹配。
分组:分组主要是用过()
进行实现,比如beyond{3}
,是匹配d
字母3次。而(beyond){3}
是匹配beyond
三次
匹配方法
分为两类:
- 字符串(str)方法:
match
、matchAll
、search
、replace
、split
- 正则对象下(regexp)的方法:
test
、exec
方法 | 描述 |
---|---|
exec | 一个在字符串中执行查找匹配的RegExp方法,它返回一个数组(未匹配到则返回 null)。 |
test | 一个在字符串中测试是否匹配的RegExp方法,它返回 true 或 false。 |
match | 一个在字符串中执行查找匹配的String方法,它返回一个数组,在未匹配到时会返回 null。 |
matchAll | 一个在字符串中执行查找所有匹配的String方法,它返回一个迭代器(iterator)。 |
search | 一个在字符串中测试匹配的String方法,它返回匹配到的位置索引,或者在失败时返回-1。 |
replace | 一个在字符串中执行查找匹配的String方法,并且使用替换字符串替换掉匹配到的子字符串。 |
split | 一个使用正则表达式或者一个固定字符串分隔一个字符串,并将分隔后的子字符串存储到数组中的 String 方法。 |
应用场景
- 校验手机号
- 校验密码
- 解析url参数
事件循环
是什么?
JS是一门单线程的语言,意味着同一时间内只能做一件事,实现单线程非阻塞的方法就是事件循环。
在JavaScript
中,所有的任务都可以分为
- 同步任务:立即执行的任务,同步任务一般会直接进入到主线程中执行
- 异步任务:异步执行的任务,比如
ajax
网络请求,setTimeout
定时函数等
同步任务进入主线程,即主执行栈,异步任务进入任务队列,主线程内的任务执行完毕为空,会去任务队列读取对应的任务,推入主线程执行。上述过程的不断重复就是事件循环。
微任务(JS引擎发起)
一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。
常见的微任务有:
- Promise.then
- MutaionObserver
- Object.observe(已废弃;Proxy 对象替代)
- process.nextTick(Node.js)
宏任务(浏览器、node发起)
宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合。
常见的宏任务有:
- script (可以理解为外层同步代码)
- setTimeout/setInterval
- UI rendering/UI事件
- postMessage、MessageChannel
- setImmediate、I/O(Node.js)
加入宏任务和微任务后,事件循环变成如下:
- 执行一个宏任务,如果遇到微任务就将它放到微任务的事件队列中
- 当前宏任务执行完成后,会查看微任务的事件队列,然后将里面的所有微任务依次执行完
BOM
是什么?
提供与浏览器窗口进行交互的对象。
DOM与BOM的区别
DOM | BOM |
---|---|
文档对象模型 | 浏览器对象模型 |
把文档当作一个对象 | 把浏览器当作一个对象 |
顶级对象是document | 顶级对象是window |
操作页面元素 | 与浏览器进行交互 |
W3C规范 | 浏览器厂商定义 |
常见的BOM对象
window
Bom
的核心对象是window
,它表示浏览器的一个实例。在浏览器中,window
对象有双重角色,即是浏览器窗口的一个接口,又是全局对象。因此所有在全局作用域中声明的变量、函数都会变成window
对象的属性和方法。
常用的窗口控制方法有:window.moveBy(x, y)、window.moveTo(x, y)、window.resizeBy(w, h)、window.scrollTo(x, y)。
window.open()
既可以导航到一个特定的url
,也可以打开一个新的浏览器窗口。
location
location常用的属性有:hash、host、hostname、href、pathname、port、protocol、search。
location.reload()
,此方法可以重新刷新当前页面。这个方法会根据最有效的方式刷新页面,如果页面自上一次请求以来没有改变过,页面就会从浏览器缓存中重新加载。如果要强制从服务器中重新加载,传递一个参数true
即可。
navigator
navigator
对象主要用来获取浏览器的属性,区分浏览器类型。属性较多,且兼容性比较复杂。
screen
保存客户端显示器信息,如像素宽度和像素高度。
history
history
对象主要用来操作浏览器URL
的历史记录,可以通过参数向前,向后,或者向指定URL
跳转。
常用的属性有:history.go()、history.forward()、history.back()、history.length[获取历史记录数]
尾递归
递归函数:一个函数在内部调用自身本身,这个函数就是递归函数。其核心思想是把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。一般来说,递归需要有边界条件、递归前进阶段和递归返回阶段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。
尾递归,即在函数尾位置调用自身(或是一个尾调用本身的其他函数等等)。尾递归也是递归的一种特殊情形。尾递归是一种特殊的尾调用,即在尾部直接调用自身的递归函数。
在递归调用的过程当中系统为每一层的返回点、局部量等开辟了栈来存储,递归次数过多容易造成栈溢出。这时候,我们就可以使用尾递归,即一个函数中所有递归形式的调用都出现在函数的末尾,对于尾递归来说,由于只存在一个调用记录,所以永远不会发生”栈溢出”错误。
JS中内存管理
内存泄漏
由于疏忽或错误造成程序未能释放已经不再使用的内存。并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。
JS垃圾回收机制
自动垃圾回收机制。原理:垃圾收集器会定期(周期性)找出那些不再继续使用的变量,然后释放其内存。
通常情况下有两种实现方式:
- 标记清除
- 引用计数
标记清除(清除存在上下文中的变量以及被上下文变量所引用的变量的标记)
JavaScript
最常用的垃圾收回机制。
当变量进入执行环境时,就标记这个变量为“进入环境“。进入环境的变量所占用的内存就不能释放,当变量离开环境时,则将其标记为“离开环境“。
垃圾回收程序运行的时候,会标记内存中存储的所有变量。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。
在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存。
标记清除方案的缺点:在清除之后,剩余对象的内存位置是不变的,导致空闲内存空间是不连续的,也就是出现了内存碎片。内存碎片会给新建对象分配内存带来问题。解决方案:标记整理。标记整理与标记清除算法相同,只是标记结束后,标记整理算法会将活着的对象向内存的一端移动,最终清理掉边界的内存。
引用计数
语言引擎有一张”引用表”,保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0
,就表示这个值不再用到了,因此可以将这块内存释放。如果一个值不再需要了,引用数却不为0
,垃圾回收机制无法释放这块内存,从而导致内存泄漏。
虽然JS有自动垃圾回收机制,但不代表不用关注内存泄露。那些很占空间的值,一旦不再用到,需要检查是否还存在对它们的引用。如果是的话,就必须手动解除引用。
引用计数的缺点:需要计数器,会占用空间;无法解决循环引用问题;
常见的内存泄露情况
意外的全局变量
1 | function foo(arg) { |
定时器
1 | var someResource = getData(); |
闭包
1 | function bindEvent() { |
没有及时清理DOM元素引用
1 | const refA = document.getElementById('refA'); |
JS本地存储方式
javaScript
本地缓存的方法我们主要讲述以下四种:
- cookie
- sessionStorage
- localStorage
- indexedDB
cookie
HTTP协议具有无状态的特点,对于事件处理没有记忆,每次都是独立请求,无法保持客户端与服务端的会话状态。无法根据之前的请求状态进行本次的请求处理。
Cookie
是某些网站为了辨别用户身份而储存在用户本地终端上的数据,是为了解决 HTTP
无状态导致的问题。
一般不超过 4KB 的小型文本数据,它由一个名称(Name)、一个值(Value)和其它几个用于控制 cookie
有效期、安全性、使用范围的可选属性组成。
cookie
在每次请求中都会被发送,如果不使用 HTTPS
并对其加密,其保存的信息很容易被窃取,导致安全风险。举个例子,在一些使用 cookie
保持登录态的网站上,如果 cookie
被窃取,他人很容易利用你的 cookie
来假扮成你登录网站。
cookie常用属性
- Expires 用于设置 Cookie 的过期时间
- Max-Age 用于设置在 Cookie 失效之前需要经过的秒数(优先级比
Expires
高) Domain
指定了Cookie
可以送达的主机名。Domain
属性**指定了哪些主机可以接受Cookie
**,如果不指定,默认为Origin
,但不包含子域名。当多个子域名需要共享Cookie
信息的时候,就必须要指定Domain
属性值为一级域名,指定Path
属性值为根路径。Path
指定了一个URL
路径,这个路径必须出现在要请求的资源的路径中才可以发送Cookie
首部。**Path
属性指定了主机下的哪些路径可以接受Cookie
,子路径也可以被匹配。**- 标记为
Secure
的Cookie
只应通过被HTTPS
协议加密过的请求发送给服务端
localstorage
特点:
- 生命周期:持久化的本地存储,除非主动删除数据,否则数据是永远不会过期的
- 存储的信息在同一域中是共享的
- 当本页操作(新增、修改、删除)了
localStorage
的时候,本页面不会触发storage
事件,但是别的页面会触发storage
事件。 - 大小:5M(跟浏览器厂商有关系)
localStorage
本质上是对字符串的读取,如果存储内容多的话会消耗内存空间,会导致页面变卡- 受同源策略的限制
- 无法像
Cookie
一样设置过期时间 - 只能存入字符串,无法直接存对象
常用方法:setItem、getItem、removeItem
sessionStorage
sessionStorage
和 localStorage
使用方法基本一致,唯一不同的是生命周期,一旦页面(会话)关闭,sessionStorage
将会删除数据
IndexedDB
- 是存储大量结构化数据的数据库
- 储存量理论上没有上限
- 所有操作都是异步的,相比
LocalStorage
同步操作性能更高,尤其是数据量较大时 - 原生支持储存
JS
的对象 - 是个正经的数据库,意味着数据库能干的事它都能干
cookie、localStorage、sessionStorage的区别
- 存储大小:
cookie
数据大小不能超过4k
,sessionStorage
和localStorage
虽然也有存储大小的限制,但比cookie
大得多,可以达到5M或更大 - 有效时间:
localStorage
存储持久数据,浏览器关闭后数据不丢失除非主动删除数据;sessionStorage
数据在当前浏览器窗口关闭后自动删除;cookie
设置的cookie
过期时间之前一直有效,即使窗口或浏览器关闭 - 数据与服务器之间的交互方式,
cookie
的数据会自动的传递到服务器,服务器端也可以写cookie
到客户端;sessionStorage
和localStorage
不会自动把数据发给服务器,仅在本地保存
函数式编程
是什么?
函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而非设计一个复杂的执行过程。
函数式编程旨在尽可能的提高代码的无状态性和不变性。要做到这一点,就要学会使用无副作用的函数,也就是纯函数。
优缺点
优点
- 更好的管理状态:因为它的宗旨是无状态,或者说更少的状态,能最大化的减少这些未知、优化代码、减少出错情况
- 更简单的复用:固定输入->固定输出,没有其他外部变量影响,并且无副作用。这样代码复用时,完全不需要考虑它的内部实现和外部影响
- 更优雅的组合:往大的说,网页是由各个组件组成的。往小的说,一个函数也可能是由多个小函数组成的。更强的复用性,带来更强大的组合性
- 隐性好处。减少代码量,提高维护性
缺点
- 性能:函数式编程相对于指令式编程,性能绝对是一个短板,因为它往往会对一个方法进行过度包装,从而产生上下文切换的性能开销
- 资源占用:在 JS 中为了实现对象状态的不可变,往往会创建新的对象,因此,它对垃圾回收所产生的压力远远超过其他编程方式
- 递归陷阱:在函数式编程中,为了实现迭代,通常会采用递归操作
JS中实现函数缓存
是什么?
函数缓存,就是将函数运算过的结果进行缓存。本质上就是用空间(缓存存储)换时间(计算过程)。常用于缓存数据计算结果和缓存对象。
实现方式
闭包、柯里化、高阶函数
数字精度丢失
产生原因
计算机存储双精度浮点数需要先把十进制数转换为二进制的科学记数法的形式,然后计算机以自己的规则{符号位+(指数位+指数偏移量的二进制)+小数部分}存储二进制的科学记数法。
因为存储时有位数限制(64位),并且某些十进制的浮点数在转换为二进制数时会出现无限循环,会造成二进制的舍入操作(0舍1入),当再转换为十进制时就造成了计算误差。
解决方法
理论上无法解决,只能处理数据得到期望的结果。
1 | function strip(num, precision = 12) { |
防抖与节流的应用场景
防抖在连续的事件,只需触发一次回调的场景有:
- 搜索框搜索输入。只需用户最后一次输入完,再发送请求
- 手机号、邮箱验证输入检测
- 窗口大小
resize
。只需窗口调整完成后,计算窗口大小。防止重复渲染。
节流在间隔一段时间执行一次回调的场景有:
- 滚动加载,加载更多或滚到底部监听
- 搜索框,搜索联想功能
判断元素是否在可视区
是什么?
方法
- offsetTop、scrollTop
- getBoundingClientRect
- Intersection Observer
offsetTop、scrollTop
offsetTop:当前元素顶端距离父元素顶端距离,鼠标滚轮不会影响其数值.
scrollTop:当前元素顶端距离窗口顶端距离,鼠标滚轮会影响其数值.外层元素的高度值是200px,内层元素的高度值是300px。很明显,“外层元素中的内容”高过了“外层元素”本身.当向下拖动滚动条时,有部分内容会隐没在“外层元素的上边界”之外,scrollTop
就等于这部分“不可见的内容”的高度。
clientHeight与clientWidth如图所示
判断:
1 | // 精简代码 |
getBoundingClientRect
返回值是一个 DOMRect
对象,拥有left
, top
, right
, bottom
, x
, y
, width
, 和 height
属性
如果一个元素在视窗之内的话,那么它一定满足下面四个条件:
- top 大于等于 0
- left 大于等于 0
- bottom 小于等于视窗高度
- right 小于等于视窗宽度
1 | function isInViewPort(element) { |
Intersection Observer
Intersection Observer
即重叠观察者,用于判断两个元素是否重叠,因为不用进行事件的监听,性能方面相比getBoundingClientRect
会好很多
使用步骤主要分为两步:创建观察者和传入被观察者
1 | // 创建观察者 |
案例:
实现:创建了一个十万个节点的长列表,当节点滚入到视窗中时,背景就会从红色变为黄色
- 使用getBoundingClientRect
1 | <div class="container"></div> |
- Intersection Observer
1 | const observer = new IntersectionObserver(getYellow, { threshold: 1.0 }); |
大文件上传时断点续传
是什么?
分片上传
- 概念:将所要上传的文件,按照一定的大小,将整个文件分隔成多个数据块(Part)来进行分片上传。上传完之后再由服务端对所有上传的文件进行汇总整合成原始的文件。
- 大致流程
- 将需要上传的文件按照一定的分割规则,分割成相同大小的数据块;
- 初始化一个分片上传任务,返回本次分片上传唯一标识;
- 按照一定的策略(串行或并行)发送各个分片数据块;
- 发送完成后,服务端判断数据上传是否完整,如果完整,则进行数据块合成得到原始文件
断点续传
断点续传文件数据块采用线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载。用户可以节省时间,提高速度。
一般实现方式有两种:
- 服务器端返回,告知从哪开始
- 浏览器端自行处理
上传过程中将文件在服务器写为临时文件,等全部写完了(文件上传完),将此临时文件重命名为正式文件即可。
如果中途上传中断过,下次上传的时候根据当前临时文件大小,作为在客户端读取文件的偏移量,从此位置继续读取文件数据块,上传到服务器从此偏移量继续写入文件即可。
实现思路
整体思路:拿到文件,保存文件唯一性标识(最简单的是md5),切割文件,分段上传,每上传一段,就根据唯一性标识判断文件上传进度,直到文件的全部片段上传完毕。
伪代码:
1 | // 读取文件内容 |
后端主要做的内容为:根据前端传给后台的md5
值,到服务器磁盘查找是否有之前未完成的文件合并信息(也就是未完成的半成品文件切片),取到之后根据上传切片的数量,返回数据告诉前端开始从第几节上传
如果想要暂停切片的上传,可以使用XMLHttpRequest
的 abort
方法
使用场景
- 大文件加速上传:当文件大小超过预期大小时,使用分片上传可实现并行上传多个 Part, 以加快上传速度
- 网络环境较差:建议使用分片上传。当出现上传失败的时候,仅需重传失败的Part
- 流式上传:可以在需要上传的文件大小还不确定的情况下开始上传。这种场景在视频监控等行业应用中比较常见
实现上拉加载,下拉刷新
上拉加载
上拉加载的本质是页面触底,或者快要触底时的动作
判断页面触底需要先了解下面几个属性
scrollTop
:滚动视窗的高度距离window
顶部的距离,它会随着往上滚动而不断增加,初始值是0,它是一个变化的值clientHeight
:它是一个定值,表示屏幕可视区域的高度;scrollHeight
:页面不能滚动时也是存在的,此时scrollHeight等于clientHeight。scrollHeight表示body
所有元素的总长度(包括body元素自身的padding)
综上得出一个触底公式:
1 | scrollTop + clientHeight >= scrollHeight |
下拉刷新
关于下拉刷新的原生实现,主要分成三步:
- 监听原生
touchstart
事件,记录其初始位置的值,e.touches[0].pageY
; - 监听原生
touchmove
事件,记录并计算当前滑动的位置值与初始位置值的差值,大于0
表示向下拉动,并借助CSS3的translateY
属性使元素跟随手势向下滑动对应的差值,同时也应设置一个允许滑动的最大值; - 监听原生
touchend
事件,若此时元素滑动达到最大值,则触发callback
,同时将translateY
重设为0
,元素回到初始位置
单点登录
是什么?
系统A
和 系统B
都属于某公司下的两个不同的应用系统,当用户登录 系统A
后,再打开 系统B
,系统便会自动帮用户登录 系统B
,这种现象就属于单点登录。
单点登录SSO:在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
SSO 一般都需要一个独立的认证中心(passport),子系统的登录均得通过passport
,子系统本身将不参与登录操作。当一个系统成功登录以后,passport
将会颁发一个令牌给各个子系统,子系统可以拿着令牌会获取各自的受保护资源,为了减少频繁认证,各个子系统在被passport
授权以后,会建立一个局部会话,在一定时间内可以无需再次向passport
发起认证。
实现
- 同域名下的单点登录:cookie domain
- 不同域名下的单点登录:token、localstorage
同域名下:cookie domain
cookie
的domain
属性设置为当前域的父域,并且父域的cookie
会被子域所共享。path
属性默认为web
应用的上下文路径。
利用 Cookie
的这个特点,没错,我们只需要将Cookie
的domain
属性设置为父域的域名(主域名),同时将 Cookie
的path
属性设置为根路径,将 Session ID
(或 Token
)保存到父域中。这样所有的子域应用就都可以访问到这个Cookie
。
当多个子域名需要共享 Cookie
信息的时候,就必须要指定 Domain
属性值为一级域名,指定 Path
属性值为根路径。
不同域名下:token
如果是不同域的情况下,Cookie
是不共享的,这里我们可以部署一个认证中心,用于专门处理登录请求的独立的 Web
服务
用户统一在认证中心进行登录,登录成功后,认证中心记录用户的登录状态,并将 token
写入 Cookie
(注意这个 Cookie
是认证中心的,应用系统是访问不到的)
应用系统检查当前请求有没有 Token
,如果没有,说明用户在当前系统中尚未登录,那么就将页面跳转至认证中心
由于这个操作会将认证中心的 Cookie
自动带过去,因此,认证中心能够根据 Cookie
知道用户是否已经登录过了
如果认证中心发现用户尚未登录,则返回登录页面,等待用户登录
如果发现用户已经登录过了,就不会让用户再次登录了,而是会跳转回目标 URL
,并在跳转前生成一个 Token
,拼接在目标URL
的后面,回传给目标应用系统
应用系统拿到 Token
之后,还需要向认证中心确认下 Token
的合法性,防止用户伪造。确认无误后,应用系统记录用户的登录状态,并将 Token
写入Cookie
,然后给本次访问放行。(注意这个 Cookie
是当前应用系统的)当用户再次访问当前应用系统时,就会自动带上这个 Token
,应用系统验证 Token 发现用户已登录,于是就不会有认证中心什么事了
用户访问系统1的受保护资源,系统1发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数。sso认证中心发现用户未登录,将用户引导至登录页面。用户输入用户名密码提交登录申请。sso认证中心校验用户信息,创建用户与sso认证中心之间的会话,称为全局会话,同时创建授权令牌。sso认证中心带着令牌跳转回最初的请求地址(系统1)。系统1拿到令牌,去sso认证中心校验令牌是否有效。sso认证中心校验令牌,返回有效,注册系统1。系统1使用该令牌创建与用户的会话,称为局部会话,返回受保护资源。用户访问系统2的受保护资源。系统2发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数。sso认证中心发现用户已登录,跳转回系统2的地址,并附上令牌。系统2拿到令牌,去sso认证中心校验令牌是否有效。sso认证中心校验令牌,返回有效,注册系统2。系统2使用该令牌创建与用户的局部会话,返回受保护资源。
不同域名下:localstorage
可以选择将 Session ID
(或 Token
)保存到浏览器的 LocalStorage
中,让前端在每次向后端发送请求时,主动将LocalStorage
的数据传递给服务端。
这些都是由前端来控制的,后端需要做的仅仅是在用户登录成功后,将 Session ID
(或 Token
)放在响应体中传递给前端。
单点登录完全可以在前端实现。前端拿到 Session ID
(或 Token
)后,除了将它写入自己的 LocalStorage
中之外,还可以通过特殊手段将它写入多个其他域下的 LocalStorage
中。
前端通过 iframe
+postMessage()
方式,将同一份 Token
写入到了多个域下的 LocalStorage
中,前端每次在向后端发送请求之前,都会主动从 LocalStorage
中读取Token
并在请求中携带,这样就实现了同一份Token
被多个域所共享。
1 | // 获取 token |
CSS放头部,JS放尾部
css放头部,可以给用户带来更好的体验感,渲染引擎会尝试尽快在屏幕上显示内容。这样做不会等到所有HTML元素解析之后才开始构建和布局DOM树。浏览器能够渲染不完整的DOM树和CSSOM,尽快减少白屏时间。
JS放尾部,是因为JS的下载和运行会阻塞DOM的解析,从而影响DOM树的绘制。此外,JS可能会改变DOM树的结构,所以需要一个稳定的DOM树。
web常见的攻击方式及防御方法
常见的攻击方式
- XSS(跨站脚本攻击)
- CSRF(跨站请求伪造)
- SQL注入攻击
XSS
是什么?
跨站脚本攻击允许攻击者将恶意代码植入到页面中。在这个过程中,涉及三方:攻击者、客户端与web应用。攻击的目的是获取客户端的cookie或者其他服务器用于识别客户端身份的信息。一旦获取,攻击者就可以假冒用户与服务器进行交互。
分类
- 存储型
- 反射型
- DOM型
存储型
攻击步骤:
- 攻击者将恶意代码提交到目标网站的数据库中
- 用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器
- 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作
场景:这种攻击常见于带有用户保存数据的网站功能,如论坛发帖、商品评论、用户私信等。
反射型
攻击步骤:
- 攻击者构造出特殊的 URL,其中包含恶意代码
- 用户打开带有恶意代码的 URL 时,网站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器
- 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作
场景:反射型 XSS 漏洞常见于通过 URL 传递参数的功能,如网站搜索、跳转等。由于需要用户主动打开恶意的 URL 才能生效,攻击者往往会结合多种手段诱导用户点击。
反射型 XSS 跟存储型 XSS 的区别是:存储型 XSS 的恶意代码存在数据库里,反射型 XSS 的恶意代码存在 URL 里。
DOM型
攻击步骤:
- 攻击者构造出特殊的 URL,其中包含恶意代码
- 用户打开带有恶意代码的 URL
- 用户浏览器接收到响应后解析执行,前端 JavaScript 取出 URL 中的恶意代码并执行
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作
DOM 型 XSS 跟前两种 XSS 的区别:DOM 型 XSS 攻击中,取出和执行恶意代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞,而其他两种 XSS 都属于服务端的安全漏洞
预防
XSS攻击的两大要素
- 攻击者提交恶意代码
- 浏览器执行恶意代码
针对提交恶意代码的防御(过滤)
- 在用户输入的过程中,过滤用户输入的恶意代码
- 在后端写入数据库前,对输入进行过滤,然后将内容传给前端。但是这种方法可能会导致输入内容在不同的地方有不同的展示形式。
防止执行恶意代码
- 使用
.innerHTML
、.outerHTML
、document.write()
时要特别小心,不要把不可信的数据作为 HTML 插到页面上,而应尽量使用.textContent
、.setAttribute()
等 - DOM 中的内联事件监听器,如
location
、onclick
、onerror
、onload
、onmouseover
等,<a>
标签的href
属性,JavaScript 的eval()
、setTimeout()
、setInterval()
等,都能把字符串作为代码运行。如果不可信的数据拼接到字符串中传递给这些 API,很容易产生安全隐患,请务必避免。
CSRF(跨站请求伪造)
是什么?
攻击者诱导受害者进入第三方网站,在第三方网站中向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台用户验证,达到冒充用户对被攻击网站执行操作的目的。
典型流程
- 受害者登录a.com,并保留了登录凭证(Cookie)
- 攻击者引诱受害者访问了b.com
- b.com 向 a.com 发送了一个请求:a.com/act=xx。浏览器会默认携带a.com的Cookie
- a.com接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发送的请求
- a.com以受害者的名义执行了act=xx
- 攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让a.com执行了自己定义的操作
场景
可以通过get
请求、通过设置自动提交表单发送post
请求、a
标签
防御
- 验证HTTP Referer字段。HTTP请求头中Referer字段用于记录HTTP请求的来源地址。由于CSRF是通过第三方网站向目标网站进行请求的,故Referer将指向恶意的第三方网站。目标网站只需验证Referer值是否是来自网站自己的请求。当然Referer字段可能会被攻击者篡改。
- 在请求地址中添加token。CSRF无法直接窃取用户信息,而是直接让用户利用自己的cookie来通过服务器验证的。要抵御 CSRF,关键在于在请求中放攻击者所不能伪造的信息,并且该信息不存在于 cookie 之中。可以在 HTTP 请求中以参数的形式加入一个随机产生的 token,并在服务器端建立一个拦截器来验证这个 token,如果请求中没有 token 或者 token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求。
SQL注入
是什么?
通过将恶意的 Sql
查询或添加语句插入到应用的输入参数中,再在后台 Sql
服务器上解析执行进行的攻击。
典型流程
- 找出SQL漏洞的注入点
- 判断数据库的类型以及版本
- 猜解用户名和密码
- 利用工具查找Web后台管理入口
- 入侵和破坏
预防
- 严格检查输入变量的类型和格式
- 过滤和转义特殊字符
- 对访问数据库的Web应用程序采用Web应用防火墙
获取元素宽高
- 获取内联样式宽高:el.style.width/height
1 | const el = document.getElementById('documentLabel'); |
- el.getBoundingClientRect().width/height
1 | const el = document.getElementById('documentLabel'); |
与宽高相关的属性
元素属性
1 | el.clientWidth; //可见区域宽 |
浏览器属性
1 | window.screenTop; //从屏幕上边到由window对象表示的页面可见区域的距离 |
获取浏览器窗口的宽高
1 | var w = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; |
迭代器
是什么?
从一个数据集合中按照一定的顺序,不断取出数据的过程。迭代强调的是依次取数据,并不保证取多少,也不保证所有的数据都要取完。遍历则是把所有数据依次全部取出。
含义
如果一个对象具有next(),并且该方法能够返回一个对象{value: 值,done(是否迭代完成)},则认为该对象是一个迭代器。
迭代器就是执行next()方法来获取一个result对象,result对象内部有属性value:本次运行获得的值,done:是否可继续迭代 (done的值由代码可知,当i的值大于或等于数组的的长度,说明已经遍历到最后一个数字了,那么done就为true)。然后i自增,等到下次运行的next方法返回的对象中value就为数组的第二个值,done会继续判断是否为false。
ES6中规定, 如果对象具有知名符号 symbol.iterator
并且属性值是一个迭代器创建函数,则该对象是可以进行迭代的。
生成器
是什么?
ES6新增的一种函数控制、使用的方案,可以更加灵活的控制函数什么时候继续执行、暂停执行等。生成器实际上是一种特殊的迭代器。生成器函数是函数。
- 生成器函数需要在function的后面添加符号*
- 生成器函数可以通过yield关键字控制函数的执行流程
- 生成器函数的返回值是一个Generator
使用
调用next获取返回对象{value: 100, done: false}
调用next函数的时候,可以传入参数,这个参数会作为上一个yield的返回值
for…in / for…of
- for…of不能遍历对象,for…in可以遍历对象
- for…in遍历数组的时候会存在一些问题
1 | - 索引为字符串型数字,不能直接进行几何计算 |
1 | let a = {'a': 1, 'b': 2}; |
Object与Map
- 对于object,它的键只能是字符串、数字或者symbol;对于map而言,它的键可以是任何类型。
- map中的元素会保持其插入时的顺序,而object则不会完全保持插入时的顺序。
- 读取map的长度可以通过.size()方法;而读取object长度则需要通过:Object.keys(obj).length
- Map是可迭代对象,可以通过for…of或者forEach方法来迭代;而object默认是不可迭代的,只能通过for…in循环来访问
- 创建和使用方式不同。object可以通过字面量、构造函数的方式创建;而map只能通过构造函数的方式创建。map新增/读取/删除元素时只能通过内置方法,object可以通过中括号或者点号。