js常见知识点
js 部分整理包括三部分。<!--more-->
分别是基础知识概念、看代码说输出和手写常见 js 题
基础知识概念
内置类型
JS 中分为七种内置类型,七种内置类型又分为两大类型:基本类型和对象(Object)。基本类型有六种: null
,undefined
,boolean
,number
,string
,symbol
。其中 JS 的数字类型是浮点类型的,没有整型。NaN
也属于 number
类型,并且 NaN
不等于自身。对于基本类型来说,如果使用字面量的方式,那么这个变量只是个字面量,只有在必要的时候才会转换为对应的类型。对象(Object)是引用类型。引用类型还包括 Array、Function
字面量:在计算机科学中,用于表达源代码中一个固定值的表示法(notation)。几乎所有计算机编程语言都具有对基本值的字面量表示,诸如:整数、浮点数以及字符串;而有很多也对布尔类型和字符类型的值也支持字面量表示;还有一些甚至对枚举类型的元素以及像数组、记录和对象等复合类型的值也支持字面量表示法。
const int b = 10
中b为常量,10为字面量。
symbol:通常被用作一个对象属性的键值(一般用来实现私有属性)。Symbol 值通过 Symbol
函数生成,它是独一无二的,可以保证不会与其他属性名产生冲突。不能与其他类型的值进行运算,可以显式转为字符串和布尔值。Symbol 作为属性名,该属性不会出现在 for...in
、for...of
循环中,也不会被 Object.keys()
、Object.getOwnPropertyNames()
、JSON.stringify()
返回。Object.getOwnPropertySymbols
方法可以获取指定对象的所有 Symbol 属性名。
var obj = {}
var a = '123', b = 123, c = Symbol('123'), d = Symbol('123')
obj[a] = 'a'
obj[b] = 'b'
obj[c] = 'c'
obj[d] = 'd'
// obj[a]
// obj[b]
// obj[c]
// obj[d]
Object 与 map 的区别:两者都是键值对的方式
-
但 Object 的键只能是基础数据类型(整数、字符串、symbol),如果将一个对象作为键值会进行类型转换,而 map 支持 js 所有数据类型作为键值。
-
Map 中同名属性可能不会覆盖,而object会。原因是 map 的键存的是内存的地址,只要地址不一样就是不同的键
let m = new Map() m.set({}, 1);m.set({}, 2);m.set({}, 3)
-
map 继承自 object 对象
-
Map 可以获取长度(.size)
-
map实现了迭代器,可以用 for...of 遍历
-
填入map的元素会保持原有的顺序
Typeof和instance
typeof
对于基本类型,除了 null
都可以显示正确的类型。typeof
对于对象,除了函数 function
都会显示 object
。对于 null
来说,虽然它是基本类型,但是会显示 object
。
因为在 JS 的最初版本中,使用的是 32 位系统,为了性能考虑使用变量机器码低位存储了其类型信息,
000
开头代表是对象,然而null
表示为全零,所以将它错误的判断为object
。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来小知识:undefined 不是保留字,可以在低版本浏览器中赋值,采用 undefined可能会出错,可以用 void 后面跟上一个组成表达式,例如
void 0
返回的就是undefined。
最好用:如果我们想获得一个变量的正确类型,可以通过 Object.prototype.toString.call(xx)
。这样我们就可以获得类似 [object Type]
的字符串。
instanceof主要作用就是判断一个实例是否属于某种类型、父类型或者祖先类型的实例。instanceof的主要实现原理就是只要右边变量的prototype在左边变量的原型链上即可,instanceof在查找的过程中会遍历左边变量的原型链,直到找到右边变量的prototype,如果查找失败返回false。
function new_instance_of(leftVaule, rightVaule) {
let rightProto = rightVaule.prototype; // 取右表达式的 prototype 值
leftVaule = leftVaule.__proto__; // 取左表达式的__proto__值
while (true) {
if (leftVaule === null) {
return false;
}
if (leftVaule === rightProto) {
return true;
}
leftVaule = leftVaule.__proto__
}
}
function foo() {}
typeof Function; // function
typeof foo; // function
typeof Object; // function
Object instanceof Object; // true
Function instanceof Function; // true
Function instanceof Object; // true
Foo instanceof Foo; // false
Foo instanceof Object; // true
Foo instanceof Function; // true
null instanceof Object // false
Object 本身是一个函数,由 Function 所创建,所以 Object.__proto__
的值是 Function.prototype
,而 Function.prototype
的 __proto__
属性是 Object.prototype
。
Foo 函数的 prototype
属性是 Foo.prototype
,而 Foo 的 __proto__
属性是 Function.prototype
。
instanceof 的缺点:基础类型没有被封装为对象,如 Number()、String(),在判断时会出错,即 instanof 检测的一定要是对象才行。同理 Number() 等返回的对象也在 Object 的原型链上
var num = 1
num.__proto__ === Number.prototype // true
num instanceof Number // false
var numA = Number(1)
numA instanceof Number // false
var numB = new Number(1)
numB instanceof Number // true
var s = new String('a')
s == 'a' // true
s === 'a' // false
NaN
typeof NaN
为 number,window.isNaN 会有一个强制类型转换的过程,将接受到的参数强制转换成数字类型,故 window.isNaN('AB'),window.isNaN('undefined'),window.isNaN({})
会显示为 true,而Number.isNaN 不会有自动的类型判断,实现就利用是否与自身相等或 typeof n ==='number' && window.isNaN(n)
??
?? 和 || 很像,但 ?? 不会屏蔽掉 false 和 0。
false || '1' // "1"
false ?? '1' // false
0 || '1' // "1"
0 ?? '1' // 0
类型转换
转Boolean:在条件判断时,除了 undefined
, null
, false
, NaN
, ''
, 0
, -0
,其他所有值都转为 true
,包括所有对象。
对象转基本类型:首先调用 valueOf
然后调用 toString
(这两个方法都可重写)。也可重写 Symbol.toPrimitive
(在转基本类型调用优先级最高),一般都不显示调用。
valueOf()
:同toString()
方法类似,为Object的原型方法,当需要返回对象的原始值而非字符串的时候使用valueOf。重写的valueof方法中不能传入参数。
Sysbol.toPrimitive
指向一个方法,该对象被转为原始类型的值时调用这个方法,返回该对象对应的原始类型值。可以接受一个字符串参数(表示当前运算模式):Number、String、Default。
四则运算:隐式类型转换,加法运算中如果其中一方是字符串类型就会把另一个也转为字符串类型,其他运算中只要其中一方是数字就将另一方转为数字。
==
操作符:
隐式类型转换:
- 如果比较的两个值是同一类型,除 null 和 undefined 相等外,其余都是正常
- 如果都是基本类型,先转数字后再比较
- 如果有一方是引用类型,将引用类型转基本类型后比较
toPrimitive
就是对象转基本类型 。例子:
[] == ![] //->true
// [] 转成 true,然后取反变成 false
[] == false
// 根据第 8 条得出
[] == ToNumber(false)
[] == 0
// 根据第 10 条得出、
ToPrimitive([]) == 0
// [].toString() -> ''
'' == 0
// 根据第 6 条得出
0 == 0 // -> true
![] == '' // true
3 + '2' === 5 // false
null == false // false
null == undefined // true
[] == [] // false
null < 1 // true
undefined < 1 false
1+null === 1
1+undefined // NaN
比较运算符:
- 对象:通过
toPrimitive
转换对象 - 字符串:通过
nicode
字符索引来比较
null和undefined:
- null表示没有对象,即该处不应该有值。表示“无”的对象,转为数值时为0
- undefined表示缺少值,此处应该有一个值,但还没有定义。表示“无”的原始值,转换成数值为NaN
new
调用new的过程中会依次发生:
- 新生成一个对象
- 链接到原型
- 绑定this
- 返回新对象
// 类似以上步骤实现new
function create() {
// 创建一个空的对象
let obj = new Object()
// 获得构造函数
let Con = [].shift.call(arguments)
// 链接到原型
obj.__proto__ = Con.prototype
// 绑定 this,执行构造函数
let result = Con.apply(obj, arguments)
// 确保 new 出来的是个对象
return typeof result === 'object' ? result : obj
}
async和await
var a = 0
var b = async () => {
a = a + await 10
console.log('2', a) // -> '2' 10
a = (await 10) + a
console.log('3', a) // -> '3' 20
}
b()
a++
console.log('1', a) // -> '1' 1
- 首先函数
b
先执行,在执行到await 10
之前变量a
还是 0,因为在await
内部实现了generators
,generators
会保留堆栈中东西,所以这时候a = 0
被保存了下来 - 因为
await
是异步操作,遇到await
就会立即返回一个pending
状态的Promise
对象,暂时返回执行代码的控制权,使得函数外的代码得以继续执行,所以会先执行console.log('1', a)
- 这时候同步代码执行完毕,开始执行异步代码,将保存下来的值拿出来使用,这时候
a = 10
- 然后后面就是常规执行代码了
async
函数返回一个 Promise 对象,可以使用then
方法添加回调函数。当函数执行的时候,一旦遇到await
就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
Proxy
es6新增功能,用于修改某些操作的默认行为,等同于在语言层面做出修改,属于一种“元编程”(对编程语言进行编程)。可以理解为在目标对象之前架设一层“拦截”,外界对该对象的访问都必须先通过这层拦截,可以对外界访问进行过滤和改写。
Proxy构造函数 var proxy = new Proxy(target,handler)
,Proxy对象的所有用法都是这种形式,不同的只是handler参数的用法。new Proxy()
表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数用来定制拦截行为,没有设置任何拦截等同于直接通向原对象。
实例:
let onWatch = (obj, setBind, getLogger) => {
let handler = {
get(target, property, receiver) {
getLogger(target, property)
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
setBind(value);
return Reflect.set(target, property, value);
}
};
return new Proxy(obj, handler);
};
let obj = { a: 1 }
let value
let p = onWatch(obj, (v) => {
value = v
}, (target, property) => {
console.log(`Get '${property}' = ${target[property]}`);
})
p.a = 2 // bind `value` to `2`
p.a // -> Get 'a' = 2
精度问题
类似于C++中判断两个double型的数值是否相等时,使用的方法是判断它们之差的绝对值是不是在一个很小的范围内,如果两个数相差很小就可以认为它们相等,深层的原因是计算机内表示小数时(C++是float和double型小数)存在误差。
JS采用IEEE 754双精度版本(64位),六十四位符号位占一位(S,63),阶码位(指数位)占十一位(E,6252,12046),其余五十二位都为小数位(有效数字)(M,51~0)。转化为十进制时:(-1)<sup>
S</sup>
* 2<sup>
E-1023</sup>
* (1+$\sum_{i=0}^{52}$(M<sub>
i</sub>
*2<sup>
-i</sup>
))
ar双精度浮点数:其数值范围为
-1.7E-308~1.7E+308
,二进制表示为:
- 规约最小值:
* 000 0000 0001 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
- 规约最大值:
* 111 1111 1110 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111
指数位为移码表示,没有用第一位来当作指示符号,使得第一位参与运算,简化运算规则,便于计算。如11位指数,正常表示范围是[-1023,1024],加上移码的表示范围是[0,2047]。
小数算二进制同整数不同。整数是辗转相除,小数则为辗转相乘。乘法计算时(每次乘2),只计算小数位,相乘的结果中整数位用作每一位的二进制,将其结果中的小数位继续进行辗转相乘。一般的小数最后的表示都是无限循环的二进制,需要在小数末尾处判断是否需要进位(同十进制的四舍五入类似)。
解决精度问题(0.1+0.2 不等于 0.3):扩大倍数,将小数都扩大相同倍数至整数,整数运算后再缩小。
参考:
数值问题
Infinity
:全局属性,无穷大(大于任何值),不是常量,但可覆盖在 ES5 中已经被改为只读。Number.POSITIVE_INFINITY
:初始值为Infinity
,常量不可重写。Number.NAGETIVE_INFINITY
:初始值为-Infinity
。Number.MAX_SAFE_INTEGER
:常量,2<sup>
53</sup>
-1。由于 JS 采用 IEEE 754 双精度版本,该格式下安全表示的最大整数即 -(2<sup>
53</sup>
- 1) 到 2<sup>
53</sup>
-1 之间的数值(包含边界),安全表示即能够准确区分两个不相同的值,2<sup>
53</sup>
就不是一个安全整数,即使它可以被 IEEE 754 直接准确表示,但 2<sup>
53</sup>
+1 不能被 IEEE 754 直接准确表示,会被近似为 2<sup>
53</sup>
(round-to-nearest、round-to-zero),即 2<sup>
53</sup>
+ 1 === 2<sup>
53</sup>
会被判定为 true(这在数学上是错误的)。在 [-2<sup>
53</sup>
,2<sup>
53</sup>
] 之间的值都能精确表示,介于 Infinity 和此最大值之间的数无法精确表示,不能被精确表示下来的大多数原因是超过 53 位有效数字的不能被保存下来(变成 0)。Number.MAX_VALUE
:指数部分为 11 位,最大为 2047,即能够表示的数值范围为 (2<sup>
-1023</sup>
,2<sup>
1024</sup>
),大于最大值得到的是 Infinity (正向溢出),负向溢出返回 0。
js 数组最大的长度为 2<sup>
32</sup>
-1。2<sup>
32</sup>
会报错,无符号 int 型的最大长度为 2<sup>
32</sup>
-1
参考:
- V8 中在 32 位系统下能表示的最大整形数字为
Math(2,30)-1
,原因是 V8 中对于 32 bits 长的值做了进一步分类,其中最低位作为区分,如果为 0 表示该值为 31 bits 长的整数,如果为 1 表示该值是 30 bits 长的指针。
参考:
遍历对象和数组(包括 es6)
对象
-
for in ,遍历对象自身和继承的可枚举属性(不含 Symbol),可以用 hasOwnProperty 过滤原型上的属性。键值遍历
-
Object.keys,返回一个包括对象自身(不包含继承)的所有可枚举属性(不含 Symbol)的键值数组
Object.values,同 Object.keys,返回的是 value 数组
-
Object.getOwnPropertyNames(obj),返回一个包含对象自身所有属性(不包含 Symbol,但包括不可枚举属性)的键值数组
-
Reflect.ownKeys(obj),返回一个对象自身的所有属性(包含 Symbol、不可枚举属性)的键值数组
可枚举性:对象 enumerable 属性,引入可枚举是为了让某些属性可规避掉 for in 操作,否则所有内部属性和方法都会被遍历到。目前,有四个操作会忽略
enumerable
为false
的属性。
for...in
循环:只遍历对象自身的和继承的可枚举的属性。Object.keys()
:返回对象自身的所有可枚举的属性的键名。JSON.stringify()
:只串行化对象自身的可枚举的属性。序列化的时候遇到 undefined 和函数的时候都会跳过。Object.assign()
: 忽略enumerable
为false
的属性,只拷贝对象自身的可枚举的属性。
数组
- forEach
- for in ,下标遍历
- for of,具有 iterator 接口(部署了 Symbol.iterator 属性)都可以遍历,如数组、set、map、类数组对象( arguments 对象、DOM NodeList 对象、Generator 对象)。扩展运算符
...
内部使用 for of 遍历
es5 继承和 es6继承的区别
es6 可以通过 extends
关键字实现继承,这比 ES5 的通过修改原型链实现继承要更加清晰、方便。
ES5 的继承,实质是先创造子类的实例对象 this
,然后再将父类的方法添加到 this
上面(Parent.apply(this)
)。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到 this
上面(所以必须先调用 super
方法),然后再用子类的构造函数修改 this
。
js 继承与 java 继承区别
-
java 里没有多继承,即一个子类同时继承多个父类,但通过实现多个接口可以间接实现多继承,js 可以通过组合继承的方式实现多继承,同名属性或共有方法会被覆盖。
-
如何才能像C#或者java里那样,在子类型中调用父类的方法呢?比如
Son.prototype.show=function(){super.show()}
这样:可以利用Object.getPrototypeOf
从子类上获取父类,故可通过Object.getPrototypeOf(Son.prototype)
父类的原型对象。
令对象不可新增属性,但可以修改当前属性用什么?
-
Object.preventExtensions
,阻止对象扩展,让一个对象变得不可扩展即不能再添加新的属性 -
Object.seal
,让一个对象密封,并返回被密封后的对象。密封指的是不能添加新的属性,不能删除已有属性,不能修改已有属性的可枚举性、可配置性、可写性,但可以修改已有属性的值。 -
Object.freeze
,冻结是指不能添加新的属性、不能修改已有属性的值,不能删除已有属性,以及不能修改已有属性的可枚举性、可配置性、可写性。即对象永远不可变。Object.freeze
不是彻底冻结对象,只冻结一层,如果属性是对象,则该属性下的对象内属性值可以更改。实现的原理是通过Object.seal
将对象密封,让其不可以新增,不能删除,然后遍历对象中的属性,通过 Object.defineProperty 修改 writeable 属性(false)
toString 与 valueOf
所有 js 数据类型都有这两个方法(null、undefined 除外),用于解决 JavaScript 值运算与显示的问题。alert 一个对象的时候调用的是 toString 方法,但如果存在 valueOf 则调用 valueOf(优先级更高)
var aaa = {
i:10,
valueOf:function () {
console.log('valueOf')
return this.i + 30;
},
toString:function () {
console.log('toString')
return this.valueOf() + 10;
}
};
alert(aaa > 20);
// valueOf
// alert: true
alert(+aaa);
// valueOf
// alert 40
alert(aaa);
// valueOf
// alert 50
var aaa = {
i:10,
toString:function () {
console.log('toString')
return this.i;
}
};
alert(aaa > 20);
// toString
// alert: false
alert(+aaa);
// toString
// alert 10
alert(aaa);
// toString
// alert 10
parseInt 、parseFloat
parseInt 与 parseFloat 的区别在于 parseFloat 可以转换小数,此外 parseInt 有第二个参数表示转换的进制,为0 时默认为10进制,只有字符串中的第一个数字会被返回,并且 parseInt('3','2')
返回 NaN
,parseInt('123',2)
返回 1
。两者转换数字的最大值相同,都是 Number.MAX_VALUE 1.7976931348623157e+308
,超出都会是 Infinity
需要注意:
- 开头和结尾的空格是允许的。
- ""空串,转换成NaN
拖拽事件
1、被拖对象:dragstart事件,被拖动的元素,开始拖放触发
2、被拖对象:drag事件,被拖放的元素,拖放过程中
3、经过对象:dragenter事件,拖放过程中鼠标经过的元素,被拖放的元素“开始”进入其它元素范围内(刚进入)
4、经过对象:dragover事件,拖放过程中鼠标经过的元素,被拖放的元素正在本元素范围内移动(一直)
5、经过对象:dragleave事件,拖放过程中鼠标经过的元素,被拖放的元素离开本元素范围
6、目标地点:drop事件,拖放的目标元素,其他元素被拖放到本元素中
7、被拖对象:dragend事件,拖放的对象元素,拖放操作结束
文件拖拽上传:可以监听目标元素(即拖拽区域)的 dragover 和 drop 事件,在 dragover 中设置 evt.dataTransfer.dropEffect
为 copy,在 drop 事件处理程序中拿到目标文件 e.dataTransfer.files
。需要注意都要在事件处理程序中增加 preventDefault 阻止默认操作,同时阻止事件冒泡 stopPropagation
onmousedown
onmouseup
onmousemove
roi 项目中图片移动的实现:给目标元素的 mousedown 事件,在 mousedown 事件处理程序中首先获取当前点击位置(距目标元素边界距离),并给 document 增加 onmouseup 和 onmousemove 事件处理程序,在 mousemove 中拿到当前移动的位置 pageX,pageY 与前述距离重新计算目标元素应该出现的位置。在 mouseup 中移除 mousemove 中的事件处理程序。
event.clientX 设置或获取鼠标指针位置相对于当前窗口的 x 坐标,其中客户区域不包括窗口自身的控件和滚动条。 (可见区域)
event.clientY 设置或获取鼠标指针位置相对于当前窗口的 y 坐标,其中客户区域不包括窗口自身的控件和滚动条。 (可见区域)
event.offsetX 设置或获取鼠标指针位置相对于触发事件的对象的 x 坐标。 (触发事件的元素,ie,chrome支持此属性,ff不支持)
event.offsetY 设置或获取鼠标指针位置相对于触发事件的对象的 y 坐标。 (触发事件的元素,ie,chrome支持此属性,ff不支持)
event.screenX 设置或获取获取鼠标指针位置相对于用户屏幕的 x 坐标。(用户屏幕左上角)
event.screenY 设置或获取鼠标指针位置相对于用户屏幕的 y 坐标。 (用户屏幕左上角)
event.x 设置或获取鼠标指针位置相对于父文档的 x 像素坐标(亦即相对于当前窗口)。(ff不支持)
event.y 设置或获取鼠标指针位置相对于父文档的 y 像素坐标(亦即相对于当前窗口)。(ff不支持) event.layerX 鼠标相比较于当前坐标系的位置,即如果触发元素没有设置绝对定位或相对定位,以页面为参考点,如果有,将改变参考坐标系,从触发元素盒子模型的border区域的左上角为参考点(未理解) event.laylerY 鼠标相比较于当前坐标系的位置,即如果触发元素没有设置绝对定位或相对定位,以页面为参考点,如果有,将改变参考坐标系,从触发元素盒子模型的border区域的左上角为参考点(未理解) event.pageX 设置或获取鼠标指针位置相对于当前窗口的 x 坐标,其中客户区域包括窗口自身的控件和滚动条。 event.pageY 设置或获取鼠标指针位置相对于当前窗口的 y 坐标,其中客户区域包括窗口自身的控件和滚动条。
事件相关
不是所有的事件都可以事件冒泡,DOM 中事件对象上的 bubbles 指示事件是否是冒泡事件类型,abort、blur、focus、load、mouseenter、mouseleave、resize、upload 等事件都不会冒泡。h5中定义的新事件:meida 相关事件都不冒泡、drag 相关事件(dragstart、drag、dragenter...)均冒泡、history 相关事件(popstate、hashchange 冒泡),pagetransition 事件(用户导航到网页和远离网页时发生的事件)不冒泡
事件冒泡的作用:
- 允许多个操作被集中处理(事件委托),可以在对象层的不同级别捕获事件
- 让不同对象同时捕获同一事件,并调用自己专属处理程序处理
事件委托的优缺点:
- 优点
- 减少事件注册,节省内存
- 简化 dom 节点更新时相应事件的更新(如不用在新添加的 dom 元素上绑定事件或者删除 dom 元素时解绑上面绑定的事件)
- 缺点
- 事件委托基于冒泡,对于不冒泡的事件不支持
- 事件冒泡可能在某层会被阻止掉
- 理论上委托会导致浏览器频繁调用处理函数,故需要就近委托(而不是 document 上)
- 可能会出现事件误判,如代码上不能明显看出,可能会进行不必要的处理(如 document 上代理,但对代码不熟悉可能会另行添加处理函数)
iOS 中的事件委托会失效:如果直接在 ios 中给 window 添加 click 的事件委托,并且想要点击某个 div、span 等默认不可点击元素是不会冒泡到 window 中。
事件穿透(移动端):
-
原因:移动端双击缩放会有 300 ms 延迟(延迟 300 ms 判断用户是否再次点击了屏幕),来判断是想要进行点击还是双击缩放
-
场景:
- 蒙层穿透:蒙层绑定 touch 事件,touch 事件触发后蒙层消失,而被蒙层覆盖元素绑定了 click 或 tap(只要这个事件有延迟都会发生),300 ms 后由于蒙层消失 click 事件就落到了被覆盖的元素上
- 跨页面点击穿透:即蒙层穿透中被蒙层覆盖的元素没有绑定 click ,而是一个 a 连接(或 input 输入框),也会发生穿透
-
解决方法:
-
遮挡
蒙层穿透主要原因是 click 事件是延迟了 300 ms 触发的,故可以给蒙层消失做一个动画淡出效果并设置动画时长大于 300 ms。
-
pointer-events
- auto:鼠标不会穿透当前层(和没有定义这个属性效果相同)
- none:元素不再是鼠标事件的目标,鼠标不再监听当前层而去监听当前层下层元素
故解决穿透就只需要在触发点击消除时将下层元素的 pointer-events 设置为 none,并在延迟一段时间后(大于 300ms)将其改回 auto
-
fastclick 库:实现思路是取消 click 事件,用 touchend 模拟快速点击行为
-
移动端 touch 事件:
pc 上的鼠标事件包括 mousedown、mouseup、mousemove、click,一次点击事件的触发过程是 mousedown -> mouseup -> click
移动端用触摸事件实现类似功能,touch 事件包括 touchstart、touchmove、touchend,手指触发触摸事件过程为 touchstart -> touchmove -> touchend
移动端的 tap 事件不是原生事件,是封装得来。tap 事件触发条件:没有移动、按住的时间没有超过特定时间(如 200 ms)
移动端的touch为什么可以触发 click 事件?
移动端响应 click 相关事件会有 300 ms 左右延迟,如 mousedown、mouseclick、mouseup,实际上是移动端浏览器在 touchend 后会等待 300 ms 来判断有没有进行双击行为,如果没有则触发 click 事件。
多异步网络请求串联
- promise then 链式(then 方法返回的是一个新的 Promise 实例)
- es6 async/await 方法(await 后跟一个 promise 对象)
eg:给一个存放 url 的数组,要求按序请求资源,用 fetch。关键词 map、promise、reduce
var urls = []
urls.reduce((acc, cur) => {
return acc.then(value => {
return fetch(cur)}, e => { throw e })
}, new Promise((resolve, reject)=>{resolve()}))
try catch
try catch 不能捕获异步方法中抛出的错误,即 try catch 能捕获的异常必须是线程执行已经进入 try catch 但 try catch 未执行完的时候抛出的,如在 try 中定义函数,但在 try catch 外执行函数、setTimeout 中定义的函数。另外 Promise 的异常在执行回调中都用 try catch 报过了,并未往上抛异常
正则表达式
[]
表示字符集,匹配括号中的任意字符,可以用-
来指定范围{n,m}
匹配前面的字符至少 n 次,最多 m 次。变体{n,}
至少 n 次,{n}
刚好 n 次x(?=y)
,先行断言,匹配 x 仅仅当 x 后是 y;(?<=y)x
,后行断言,匹配 x 仅仅当 x前面是 y;x(?!y)
,正向否定查找,仅仅当 x 后面不是y;(?<!y)x
,反向否定查找,匹配x仅当 x 前面不是y- replace 方法中引入正则,传入回调函数的参数(正则中的括号捕获)
js 中跨域问题
主要考察 cors 和 服务器代理,了解 jsonp 和 其他不同窗口之间的跨域通信
详情见跨域及解决方法
js 中节流防抖区别与应用场景
详情见 函数节流和防抖
es6 新特性
主要考察 map、weakmap、set、weakset,async/await,可以参考阮一峰 es6 入门教程
js 中的垃圾收集机制
详情见 垃圾收集
cookie 、session、token、storage 和 worker
详情见 js 中的数据存储
js 中的事件循环和宏/微任务
js 中的模块化
安全问题(常考 xss 和 csrf)
XSS
跨站脚本攻击(英语:Cross-site scripting,通常简称为:XSS)
XSS 攻击是指攻击者在网站上注入恶意的客户端代码,通过恶意脚本对客户端网页进行篡改,从而在用户浏览网页时,对用户浏览器进行控制或者获取用户隐私数据的一种攻击方式。
攻击者对客户端网页注入的恶意脚本一般包括 JavaScript,有时也会包含 HTML 和 Flash。有很多种方式进行 XSS 攻击,但它们的共同点为:将一些隐私数据像 cookie、session 发送给攻击者,将受害者重定向到一个由攻击者控制的网站,在受害者的机器上进行一些恶意操作。
XSS攻击可以分为3类:反射型(非持久型)、存储型(持久型)、基于DOM
- 反射型:把用户输入的数据 “反射” 给浏览器,这种攻击方式往往需要攻击者诱使用户点击一个恶意链接,或者提交一个表单,或者进入一个恶意网站时,注入脚本进入被攻击者的网站。攻击者可以注入任意的恶意脚本进行攻击,可能注入恶作剧脚本,或者注入能获取用户隐私数据(如cookie)的脚本,这取决于攻击者的目的。所谓反射型XSS就是将用户输入的数据(恶意用户输入的js脚本),“反射”到浏览器执行。
- 存储型:把用户输入的数据 "存储" 在服务器端,当浏览器请求数据时,脚本从服务器上传回并执行。这种 XSS 攻击具有很强的稳定性。比较常见的一个场景是攻击者在社区或论坛上写下一篇包含恶意 JavaScript 代码的文章或评论,文章或评论发表后,所有访问该文章或评论的用户,都会在他们的浏览器中执行这段恶意的 JavaScript 代码。
- 基于 DOM :指通过恶意脚本修改页面的 DOM 结构,是纯粹发生在客户端的攻击。
实例:
xss 中主要目的是想办法获取目标攻击网站的 cookie,有了 cookie 就可以以他人身份登录。主要的做法是诱使用户点击连接或者打开某个页面,这个页面中会带有恶意代码(如图片、链接、js 代码等),如发送 get 请求到指定连接中,其中这个连接上带上 document.cookie
作为参数,这样恶意站点就可以通过 get 参数拿到用户的 cookie。
具体实现:比如说动态处理的 script 中存在 setAttribute('src', 'http://xxx.js'),就会请求恶意代码,或者是 innerHtml
实例:
- 反射型: 用户输入查询,服务端返回中将查询字段插入 html 中,这样用户输入 html script 代码就会执行
- 持久型:攻击者在留言中增加一段代码,而留言输入框恰好将输入当做 innerHtml 插入。。
防范:
-
httpOnly 防止截取 cookie
httpOnly 的缺点?
-
输入检查:不要相信用户的任何输入。对用户的任何输入要进行检查、过滤和转义。建立可信任的字符和 HTML 标签白名单,对于不在白名单之列的字符或者标签进行过滤或编码。在 XSS 防御中,输入检查一般是检查用户输入的数据中是否包含
<
,>
等特殊字符,如果存在,则对特殊字符进行过滤或编码,这种方式也称为 XSS Filter。 -
输出检查:服务端的输出也会存在问题。一般来说,除富文本的输出外,在变量输出到 HTML 页面时,可以使用编码或转义的方式来防御 XSS 攻击
csrf
CSRF,即 Cross Site Request Forgery,中译是跨站请求伪造,是一种劫持受信任用户向服务器发送非预期请求的攻击方式。
通常情况下,CSRF 攻击是攻击者借助受害者的 Cookie 骗取服务器的信任,可以在受害者毫不知情的情况下以受害者名义伪造请求发送给受攻击服务器,从而在并未授权的情况下执行在权限保护之下的操作。比如说假设网站中有一个通过 Get 请求提交用户评论的接口,那么攻击者就可以在钓鱼网站中加入一个图片,图片的地址就是评论接口。
实例
csrf 主要目的是让用户在不知情的情况下攻击自己已登录的一个系统(类似钓鱼),如用户当前已经登录了邮箱或bbs,此时用户还在使用一个被控制的站点(钓鱼网站),这个网站上可能某个图片点击之后就会触发 js 点击事件:构造 bbs 发帖的请求,去 bbs 发帖,由于当前浏览器状态时已登录状态,这个伪造的请求会和正常的请求相同,利用当前的登录状态,让用户在不知情的情况帮你发帖或干其他事。
防范:
-
验证码:CSRF 攻击往往是在用户不知情的情况下构造了网络请求。而验证码会强制用户必须与应用进行交互,才能完成最终请求
-
禁止第三方网站携带本站的 cookie 信息,让服务器返回 Set-Cookie 字段中带上 SameSite 特定值,
SameSite可以是:
- Strict:完全禁止第三方 cookie,只有当前网页URL与请求目标一致才会带上 cookie。(过于严格,如 github 连接跳转后不会自动登录)
- Lax:只有导航到目标网址的 get 请求可以带上 cookie,包括三种情况:
- a 标签,
<a href="..."></a>
- 预加载请求,
<link re="prerender" href="..."/>
- get 表单,
<form method="GET" action="..."/>
- a 标签,
- None:显式关闭 SameSite(因为浏览器可能会默认 Lax),前提是 Set-Cookie 中需要带上 Secure(cookie 只能通过 https 发送)
-
Referer check:根据 HTTP 协议,在 HTTP 头中有一个字段叫 Referer,它记录了该 HTTP 请求的来源地址。通过 Referer Check,可以检查请求是否来自合法的"源"。比如,如果用户要删除自己的帖子,那么先要登录
www.c.com
,然后找到对应的页面,发起删除帖子的请求。此时,Referer 的值是http://www.c.com
;当请求是从www.a.com
发起时,Referer 的值是http://www.a.com
了。因此,要防御 CSRF 攻击,只需要对于每一个删帖请求验证其 Referer 值,如果是以www.c.com
开头的域名,则说明该请求是来自网站自己的请求,是合法的。如果 Referer 是其他网站的话,则有可能是 CSRF 攻击,可以拒绝该请求。referer、origin、host
host:客户端想访问的 web 服务器的域名/ip 地址和端口号
origin:说明请求从哪里发起,仅仅包括协议和域名
referer:包括协议、域名和查询参数(不包含锚点信息),可能会造成信息泄露(查询参数中可能包含id 或密码等)
-
添加 token 验证:CSRF 攻击之所以能够成功,是因为攻击者可以完全伪造用户的请求,该请求中所有的用户验证信息都是存在于 Cookie 中,因此攻击者可以在不知道这些验证信息的情况下直接利用用户自己的 Cookie 来通过安全验证。要抵御 CSRF,关键在于在请求中放入攻击者所不能伪造的信息,并且该信息不存在于 Cookie 之中。可以在 HTTP 请求中以参数的形式加入一个随机产生的 token,并在服务器端建立一个拦截器来验证这个 token,如果请求中没有 token 或者 token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求。token 也可以放在当前页面中(如表单中 input 的隐藏域、meta 标签或者任何 一个 dom 属性)和 cookie中,请求的时候参数中带上这个 token,后端将参数中的 token 和 cookie 中的 token 对比来判断是否合法
方法优缺点:验证码需要用户填写,增加了网站使用复杂度,token 验证可以在用户无感的情况下完成,不影响用户体验。故验证码一般用在需要提高用户认知的场景(如多次登录失败、修改个人信息),而一些常见接口(如获取商品列表、搜索)使用 token 较合理
sql 注入
SQL注入攻击指的是通过构建特殊的输入作为参数传入Web应用程序,而这些输入大都是SQL语法里的一些组合,通过执行SQL语句进而执行攻击者所要的操作,其主要原因是程序没有细致地过滤用户输入的数据,致使非法数据侵入系统。
防护
- 永远不要信任用户的输入。对用户的输入进行校验,可以通过正则表达式,或限制长度;对单引号和双"-"进行转换等。
- 永远不要使用动态拼装sql,可以使用参数化的sql或者直接使用存储过程进行数据查询存取。
- 永远不要使用管理员权限的数据库连接,为每个应用使用单独的权限有限的数据库连接。
- 不要把机密信息直接存放,加密或者hash掉密码和敏感的信息。
- 应用的异常信息应该给出尽可能少的提示,最好使用自定义的错误信息对原始错误信息进行包装。
主动攻击和被动攻击
- 主动攻击是具有破坏性的一种攻击行为,攻击者主动做一些不利于受害人的事情。按照攻击方法不同,主动攻击可以分为中断、篡改和伪造。
- 中断是指截获由原站发送的数据,将有效数据中断,使目的站无法接收到原站发送的数据;
- 篡改是指将原站发送的目的站的数据进行篡改,从而影响目的站所受的的信息;
- 伪造是指在原站未发送数据的情况下,伪造数据发送的目的站,从而影响目的站。
- 被动攻击是在不影响正常数据通信的情况下,获取由源站发送的目的站的有效数据,通过监听网络造成间接影响,不会对传输造成直接影响,难以监测。
参考资源:
防盗链
referer字段,不是同一个域名
禁用网站referer:
<meta name="referrer" content="no-referrer" />
<img referrer="no-referrer|origin|unsafe-url" src="{item.src}"/>
看代码说输出
涉及变量预解析、暂时性变量死区、闭包、promise、this、事件循环、宏任务微任务
setTimeout(_ => console.log(1))
new Promise(resolve => {
resolve()
console.log(2)
}).then(_ => {
setTimeout(_ => console.log(3))
console.log(4)
Promise.resolve().then(_ => {
console.log(5)
}).then(_ => {
Promise.resolve().then(_ => {
console.log(6)
})
})
})
console.log(7)
function Foo() {
this.a = 1;
return {
a: 4,
b: 5,
};
}
Foo.prototype.a = 6;
Foo.prototype.b = 7;
Foo.prototype.c = 8;
var o = new Foo();
console.log(o.a);
console.log(o.b);
console.log(o.c);
(async function (){
await console.log(1)
console.log(2)
})().then(() => console.log(3))
console.log(4)
setTimeout(()=> {
console.log(5)
},0)
// 2 3 的顺序?
var a = 1;
var foo = {
a: 2,
bar: function () {
return this.a;
}
};
console.log(foo.bar());
console.log((foo.bar)());
console.log((foo.bar=foo.bar)());
function fun(n,o) {
console.log(o)
return {
fun:function(m){
return fun(m,n);
}
};
}
var a = fun(0); a.fun(1); a.fun(2); a.fun(3);
var b = fun(0).fun(1).fun(2).fun(3);
var c = fun(0).fun(1); c.fun(2); c.fun(3);
//问:三行a,b,c的输出分别是什么?
var a = 10;
var obj = {
a:20,
say:()=>{
console.log(this.a)
}
}
var objTemp = {
a:20,
say:function () {
console.log(this.a)
}
}
obj.say();
var obj2 = {a:30}
obj.say.apply(obj2);
// obj 对象不能产生作用域,箭头函数中的 this 始终指向其父作用域中的 this
objTemp.say()
objTemp.say.apply(obj2)
function fun () {
return () => {
return () => {
return () => {
console.log(this.name)
}
}
}
}
var f = fun.call({name: 'foo'})
var t1 = f.call({name: 'bar'})()()
var t2 = f().call({name: 'baz'})()
var t3 = f()().call({name: 'qux'})
var name = 'global';
function Person(name) {
this.name = name;
this.sayName = () => {
console.log(this.name)
}
}
const personA = new Person('aaa');
const personB = new Person('bbb');
personA.sayName();
personB.sayName();
new Promise(resolve => {
resolve('value')
}).then(data => {
console.log('output1', data)
}).then(data => {
console.log('output2', data)
}).then(data => console.log('output3', data))
.then(data => console.log('output4', data))
手写常见 js 题
闭包
-
函数柯里化
function curryIt(fn) { var length = fn.length; // 函数的显式参数长度 var arr = []; var result = function(){ arr = arr.concat(...arguments) if(arr.length < length){ return result; }else{ return fn.apply(null,arr); } } return result; } function add(x,y) {return x+y;}
-
函数节流、防抖
// 防抖 function debounce(fn, context, wait = 1000) { let tId return function () { if (tId) { clearTimeout(tId) tId = null } tId = setTimeout(() => { fn.apply(context, arguments) }, wait) } } // 节流 function throttle(fn, context, interval = 300) { let canRun = true return function () { if (!canRun) { return } canRun = false setTimeout(() => { fn.apply(context, arguments) canRun = true }, interval) } } // 最后一次一定触发的节流函数 function throttleWithLastMust (fn, context, wait) { let prev = 0 let tId = null return function () { if (Date.now() - prev > wait) { if (tId) { clearTimeout(tId) tId = null } fn.apply(context, arguments) prev = Date.now() } else if (!tId) { tId = setTimeout(() => { fn.apply(context, arguments) tId = null }, wait) } } }
-
bind 方法
Function.prototype.mybind = function (context) { if (typeof this != 'function') { throw TypeError('error'); } var _this = this; var args = [...arguments].slice(1); // bind 调用有函数柯里化的形式 bind(this,...arg)(...arg) 即返回一个函数,函数也可接受参数 return function F() { if (this instanceof F) { return new _this(...args, ...arguments); } else { return _this.apply(context, args.concat(...arguments)); } } }
call、apply 方法也在考察范围之内(少见)
Function.prototype.mycall = function (context) { var context = context || window; context.fn = this; var args = [...arguments].slice(1); var result = context.fn(...args); delete context.fn; return result; } Function.prototype.myapply = function (context) { var context = context || window; var result; context.fn = this; if (arguments[1]) { result = context.fn(...arguments[1]); } else { result = context.fn(); } delete context.fn; return result }
promise 相关
-
promise 中 all 方法
MyPromise.prototype.all = function(proms){ return new MyPromise((resolve,reject) => { var result = proms.map(p => { var flag = { result: undefined, isResolved: false }; p.then(val => { flag.result = val; flag.isResolved = true; var isFinished = result.filter(r => !r.isResolved); if(isFinished.length == 0){ resolve(result.map(r => r.result)); } }, error => { reject(error); }); return flag; }) }) }
-
promise 中 race 方法
MyPromise.prototype.race = function(proms){ return new Promise((resolve,reject) => { proms.forEach(p => { p.then(val => { resolve(val); },error => { reject(error); }) }) }) }
-
借助 promise 实现链式先后请求(带并发控制)
function compositeWithPromise (urlArr, limit) { function _loadImg (url) { /*请求资源方法*/ } var copyArr = [].concat(urlArr) var pros = copyArr.splice(0, limit).map((v,i) => { return _loadImg(v).then(() => { return i }) }) var p = Promise.race(pros) for (var i=0; i<copyArr.length; i++) { p.then((index) => { pros[index] = _loadImg(copyArr[i]).then(() => { return index }) return Promise.race(pros) }) } }
继承、原型链相关
-
继承方法
function subType() {} function superType () {} // 原型链继承 subType.prototype = new superType() subType.prototype.constructor = subType // 组合继承 function subType() { superType.call(this) } subType.prototype = new superType() subType.prototype.constructor = subType // 寄生组合式继承 // Object.create // 对传入的对象进行了一次浅复制 function object(o) { function F() { } F.prototype = o; return new F(); } function inheritPrototype(subType, superType) { // 创建父类型原型副本 var prototype = object(superType.prototype); // 创建对象 // 重写原型失去了默认的 constructor prototype.constructor = subType; // 增强对象 subType.prototype = prototype; // 指定对象 } function superType() { } function subType() { superType.call(this); } inheritPrototype(subType, superType);
-
new 操作符
数组相关 api
-
去重
function dedupe(arr) { var arrSet = new Set(arr); return Array.from(arrSet); } function dedupeC(arr) { var res = []; arr && arr.forEach(item => { if (!res.includes(item)){ res.push(item); } }); return res; } // 数组中包含对象和数组,只能判断具有相同引用的对象 function dedupeCC(arr) { var res = []; var hash = {}; arr && arr.forEach(item => { if (!hash[item]){ res.push(item); hash[item] = true; } }); return res; }
-
扁平化
// 1 api ary = ary.flat(Infinity); // 参数表示展开的深度 // 2 字符串 // 会把数字变为了字符串 let str = JSON.stringify(ary); ary = str.replace(/[\[\]]/g,'').split(','); // 3 reduce + 递归 function myFlat(arr){ return arr.reduce((acc,cur) => { return acc.concat(Array.isArray(cur) ? myFlat(cur) : cur); },[]); } // 4 扩展运算符 while(ary.some(Array.isArray)) { ary = [].concat(...ary); } // 5 在原型上实现 Array.prototype.myFlat = function (count) { var arr = this.slice(), res = [] if (count < 1) return arr for (var i=0; i<arr.length; i++) { if (Array.isArray(arr[i])) { res.push(...arr[i].myFlat(count === Infinity || count === undefined ? Infinity : count-1)) } else { res.push(arr[i]) } } return res }
-
map
function myMap(fn,context) { var arr = [].slice.call(this); var arrMap = []; for(var i=0; i<arr.length; i++){ arrMap.push(fn.call(context,arr[i],i,this)); } return arrMap; }
-
reduce
function myReduce(fn,initialValue){ var arr = [].slice.call(this); if (arguments.length == 2) { // 避免过滤掉传入的非法字符(0,undefined,...) arr.unshift(initialValue); // unshift 从头部添加,reduce 从左向右执行,从 initialValue 开始 } var res = arr[0]; // 注意 i 的起始位置 for(var i=1; i<arr.length; i++) { // if(!arr.hasOwnProperty(i)) {continue;} res = fn.call(null,res,arr[i],i,this); } return res; }
其他
-
深拷贝(递归与非递归)
function depClone (target, map = new WeakMap()) { if (typeof target === 'object') { if (Object.prototype.toString.call(target) === '[object Null]') return null var res = new target.constructor if (map.get(target)) return target map.set(target, res) if (Object.prototype.toString.call(target) === '[object Set]') { for (var value of set.values()) { res.add((typeof value === 'object') ? depClone(value) : value) } return res } if (Object.prototype.toString.call(target) === '[object Map]') { for (var item of target.entries()) { // res.set((typeof item[0] === 'object') ? depClone(item[0]) : item[0], (typeof item[1] === 'object') ? depClone(item[1]) : item[1]) res.set(item[0], (typeof item[1] === 'object') ? depClone(item[1]) : item[1]) } return res } if (Object.prototype.toString.call(target) === '[object RegExp]') { var regFlag = /\w*$/ // 匹配结尾的 g i m(多行匹配) res = new target.constructor(target.source, regFlag.exec(target)) // 正则表达式原文本和属性 res.lastIndex = target.lastIndex // 正则表达式开始下一次查找的索引位置,只有在有全局标志正则表达式中才有作用 return res } if (Object.prototype.toString.call(target) === '[object Date]') { res = new target.constructor(+target) return res } for (var i=0; i<Object.keys(target).length; i++) { var key = Object.keys(target)[i] res[key] = (typeof target[key] === 'object') ? depClone(target[key]) : target[key] } return res } return target } function deepCloneNonRecursive (target) { if(!getType(target)) return target let res = new target.constructor var stack = [{ target, res }] var map = new WeakMap() while (stack.length) { const { target, res } = stack.pop() switch(getType(target)) { case 'Array': for (var i=0; i<target.length; i++) { var type = getType(target[i]) if (!type) res[i] = target[i] else if (type === 'RegExp') res[i] = copyRegExp(target[i]) else if (type === 'Date') res[i] = copyDate(target[i]) else { if (map.get(target[i])) res[i] = target[i] else { var tempRes = new target[i].constructor res[i] = tempRes map.set(target[i], tempRes) stack.push({ target: target[i], res: tempRes }) } } } break case 'Set': for (var item of target.values()) { var type = getType(item) if (!type) res.add(item) else if (type === 'RegExp') res.add(copyRegExp(item)) else if (type === 'Date') res.add(copyDate(item)) else { if (map.get(item)) res.add(item) else { var tempRes = new item.constructor res.add(tempRes) map.set(item, res) stack.push({ target: item, res: tempRes }) } } } break; case 'Map': for (var items of target.entries()) { var type = getType(item[1]) if (!type) res.set(item[0], item[1]) else if (type === 'RegExp') res.set(item[0], copyRegExp(item[1])) else if (type === 'Date') res.set(item[0], copyDate(item[1])) else { if (map.get(item[1])) res.set(item[0], item[1]) else { var tempRes = new item[1].constructor res.set(item[0], tempRes) map.set(item[1], tempRes) stack.push({ target: item[1], res: tempRes }) } } } break; case 'Object': var keys = Object.keys(target) for (var i=0; i<keys.length; i++) { var key = keys[i] var type = getType(target[key]) if (!type) res[key] = target[key] else if (type === 'RegExp') res[key] = copyRegExp(target[key]) else if (type === 'Date') res[key] = copyDate(target[key]) else { if (map.get(target[key])) res[key] = target[key] else { res[key] = new target[key].constructor map.set(target[key], res[key]) stack.push({ target: target[key], res: res[key] }) } } } break default: console.log('error') } } return res } function copyRegExp (target) { var reg = /\w*$/ var res = new RegExp(target.source, reg.exec(target)) res.lastIndex = target.lastIndex return res } function copyDate (target) { return new Date(+target) } function getType (v) { switch(Object.prototype.toString.call(v)) { case '[object Object]': return 'Object' case '[object Array]': return 'Array' case '[object Date]': return 'Date' case '[object Map]': return 'Map' case '[object Set]': return 'Set' case '[object RegExp]': return 'RegExp' default: return false } }
-
手写 ajax
function myAxios(options){ return new Promise ((resolve,reject) => { // 兼容 IE5(通过 ActiveX 实现) var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP'); var {method,url,data} = options; var handle = function(){ /** * XMLHttpRequest.readyState 代理当前所处的状态 * 0 | UNSENT | client 已经创建,但还未调用 open * 1 | OPENED | 已经调用了 open * 2 | HEADERS_RECEIVED | 已经调用了send,可以获得 headers 和 status * 3 | LOADING | 下载中,responseText 已经包含部分数据 * 4 | DONE | 下载操作已经完成 * * open 用于初始化一个请求,参数(method, url, async) 第三个参数表示是否异步,默认为 true. 和 send 方法有关 * send 如果 open 一个异步请求,会在请求发送之后立即返回,否则直到响应到达后才返回 */ if(xhr.readyState != 4){return;} if(xhr.state == 200) { resolve(xhr.responseText); }else { reject(new Error(xhr.reponseText)); } } // 第三个参数表示是否异步发送请求 xhr.open(method,url,true); xhr.onReadyStateChange = handle; /** * send 中 data 的类型: */ xhr.send(data); }) }
ES6新特性
-
默认参数:函数的参数可以设置默认值,并且这个默认值在传入的值是fasly也能变成参数本身的值。
var link = function (height) { var height = height || 50; console.log(height, color, url) } link(0) // 50 var link = function(height = 50) { console.log(height,color,url) } link(0) // 0
-
模版对象+多行字符串:`$
-
解构赋值:花括号、数组
npm、yarn、pnpm
Npm2:
- 依赖树(node_modules)是嵌套的,可能超过windows文件最长路径
- 重复安装
yarn/npm3:
- 扁平化:依赖平铺到一层,但还是会有嵌套(名称相同但版本不同的包)
- 问题:
- 扁平化算法复杂度,耗时长
- 幽灵依赖:可以非法访问没有声明过依赖的包
- 依赖结构不确定:相同名称不同版本的包,不确定依赖扁平化之后的最后结构,这也是.lock文件解决的问题
pnpm
- 文件安装都是从全局store硬链接到虚拟store(node_modules/.pnpm)
- node_modules中包和包之间是通过软连接的方式组织依赖关系,都是各个位置目录的link,不是同一个目录,没有路径限制
- 原理:内部使用基于内容寻址的文件系统来存储文件,不会安装同一个包,同一个包的不同版本也会极大程度复用
Script crossorigin
为什么要有(script本身就是跨域):
- script标签请求资源,header没有origin
- script标签请求跨域资源内部运行报错时,window.onerror捕获error.message只能看到Script error而看不到具体的错误内容
crossorigin作用:
- 设置后请求资源会带上origin,要求服务器进行cors校验,如果没有返回Access-Control-Allow-Origin会报错
- crossorigin:
- 默认anonymous,同域会带上cookie,跨域不带cookie
- use-credentials,跨域也会带上cookie,需要设置Access-Control-Allow-Credentials: true
requestAnimationFrame 与 requestIdleCallback
requestAnimationFrame
前端动画:
- css动画:transition、animation
- js动画:
- setInterval、setTimeout 定时器(不停更改dom位置)
- canvas动画,搭配js定时器动态修改(绘制)
- requestAnimationFrame动画
执行js动画:window.requestAnimationFrame ,调用时间间隔与硬件相关,和屏幕的刷新率保持一致。具体间隔大小:
- 如果当前回调操作能够在1帧的时间内完成,requestAnimationFrame下一次执行时间间隔为1帧的时间
- 如果当前回调操作不能在1帧的时间内完成,requestAnimationFrame下一次执行时间间隔为多帧的时间
缺点:没有对回调函数进行管理,如果多次调用带有同一回调函数的requestAnimationFrame,会导致在同一帧内执行多次回调。解决方法是管理requestAnimationFrame的回调函数及调用次数。
MDN:告诉浏览器希望在下次重绘之前调用指定的回调函数更新动画,传入一个回调函数作为参数,这个回调函数会在浏览器下次重绘之前执行。
传入这个回调函数的参数是什么含义,和时间戳有什么不同:
DOMHighResTimeStamp类型,以浮点数的形式表示时间,时间精度最高可达微秒级,而Date.now()是毫秒级
是相对时间(相对于performance.timing.navigationStart),以恒定速率慢慢增加,同performance.now类似,不会受到系统时间影响(系统时钟可能会被手动调整或NTP软件修改)
performance.timing.navigationStart + performance.now()
约等于Date.now()
如果需要在下下次重绘前继续更新,需要回调函数内部调用requestAnimationFrame。
// 获取帧率 function getFps () { let pts = 0 // 上一次运行时间 let pte = 0 let cb = function (t) { if (!pts) { pts = t } if (!pte) { pte = t } console.log((1/(t - pte) * 1000)); pte = t window.requestAnimationFrame(cb) } window.requestAnimationFrame(cb) }
与其他的js动画区别:为什么好?
-
requestAnimationFrame:
- 会将每一帧的所有DOM操作集中起来,在一次重绘/回流中完成,且时间间隔与浏览器刷新频率相关;
- 不可见或隐藏元素中,requestAnimationFrame不会重绘、回流
- 页面不是激活状态下,动画会自动暂停
-
setInterval:执行时间不确定(参数中的时间间隔是将代码添加到异步队列中等待的时间,只有主线程以及队列前的任务执行完毕才开始执行setInterval代码。
缺点是会无视代码错误、无视网络延迟、不保证执行
-
setTimeout:执行时间不确定(同setInterval),实现的动画在某些低端机上出现卡顿、抖动。setTimeout执行只会更改内存中的图像属性,变化需要在屏幕下次绘制才会被更新到屏幕上,如果两者步调不一致可能会导致中间某一帧的操作被跨越过去,直接更新下一帧的图像。
requestIdleCallback
相比于requestAnimationFrame在每一帧中确定执行不同(高优先级),浏览器会在空闲时执行requestIdleCallback,是低优先级任务。
一帧包含了用户的交互、js的执行、以及requestAnimationFrame的调用,布局计算以及页面的重绘等工作。 假如某一帧里面要执行的任务不多,在不到16ms(1000/60)的时间内就完成了上述任务的话,那么这一帧就会有一定的空闲时间,这段时间就恰好可以用来执行requestIdleCallback的回调
最大空闲时间:50ms。用户处于空闲状态(没有与网页进行任何交互),屏幕中也无动画执行。最大是50ms的原因是人类对100ms内的响应会认为是瞬时的,限制空闲时间为50ms是为了避免对用户操作响应的阻塞,从而感觉到明显的响应滞后。
不适合操作:
- 更新DOM:之前的布局计算会失效;如果下一帧中有获取布局操作,浏览器则必须执行重排,很容易超过当前帧空闲时间
- Promise操作:Promise回调会在idle回调执行完后立即执行,会拉长当前帧的耗时。
参考
MutationObserver 与 IntersectionObserver
MutationObserver:用来监听DOM的任何变化(如子元素、属性和文本内容的变化),和事件本质不同在于事件是同步触发,MutationObserver是异步触发(微任务),并且DOM发生变化之后不会立刻触发,而是要等到当前所有DOM操作都结束后才触发。兼容性良好,大部分设备能满足。
const observer = new MutationObserver((mutations, observer) => {
// mutations: 所有DOM变动记录数组
// observer:监听实例
mutations.forEach(mutation => {
//
})
})
const el = document.querySelector('listen')
const options = { childList: true }
// observe 接受两个参数:监听的对象和指定需要观察的特定变化
observer.observe(el, options)
// 停止观察
observer.disconnect()
IntersectionObserver:交叉观察器,可以观察目标元素与视口或指定根元素产生的交叉区的变化。事件异步触发,不随目标元素滚动同步触发,内部实现使用requestIdleCallback的方式,只有线程空闲下来,才会执行观察器,优先级较低。
大部分设备能使用,低版本android和ios设备需要注意兼容性。
// callback:可变性变化时的回调函数,参数是一个IntersectionObserverEntry对象数组,每个对象中包含目标元素的可见比例等属性。
// option:配置对象,threshold决定什么时候触发回调,触发回调门槛值数组。root指定元素所在容器。
const io = new IntersectionObserver(callback, option)
// 开始观察,参数为要观察的DOM节点
io.observe(document.getElementById('ex'))
// 停止观察
io.unobserver(element)
// 关闭观察器
io.disconnect()