记录每日一题的学习整理,避免学了又忘记 = 白学
14.每日一题——Proxy的特性
Proxy用于修改某些操作的默认行为,相当于给目标对象设置一层拦截,外部的所有访问都必须先通过这层拦截,可以对外部的访问进行过滤和拦截,proxy是代理的意思,可以表示为用来“代理”某些操作和行为,被称为代理器。
var proxy = new Proxy(target,handler);
Proxy对象的用法如上所示,
- target表示需要代理的对象
- handler表示一个配置对象,对于每一个被代理的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作 用法如下所示:
var target = {
name:'C'
}
var handler = {
get: function(target, property) {
console.log('读取操作的代理 执行'+ property+' value: '+ target[property])
return target[property]
},
set: function(target, property, value) {
console.log('重写操作的代理 执行'+ property +' value: ' + value)
target[property] = value
return true
}
}
var proxy = new Proxy(target, handler)
proxy.name //读取操作的代理 执行name value: C
proxy.name = 'chen' //重写操作的代理 执行name value: chen
若handler为一个空对象,则相当于没有任何拦截处理,访问proxy就等于直接访问target对象。
Proxy支持的拦截操作方法有13种
-
get(target, propKey, receiver),get()可以接受三个参数,分别是目标对象,属性名,proxy实例本身(操作行为本身的对象【可选】),主要用于拦截读取操作,get方法可以继承,对应原型链上的属性也能够拦截到
-
set(target, propKey, value, receiver),set()可以接受四个参数,分别是目标对象,属性名,属性值,proxy实例本身(操作行为本身的对象【可选】),主要用来拦截某个属性的赋值操作
(PS:如果目标对象自身的某个属性不可写,那么set方法将不起作用) -
has(target, propKey),has()可以接受两个参数,分别是目标对象和需查询的属性名,主要用来拦截HasProperty操作,判断对象是否具有某个属性,这个方法会被调用,需要注意的是has方法拦截的是HasProperty操作而不是HasOwnProperty操作,即不能判断一个属性是对象自身的属性还是继承属性
-
deleteProperty(target, propKey),deleteProperty()用于拦截delet操作,如果这个方法抛出错误或者访问false,则当前属性无法被delete命令删除
-
ownKeys(target),ownKeys()方法用来拦截对象自身属性的读取操作。例如Object.getOwnPropertyNames(),Object.getOwnPropertySymbols(),Object.keys(),for...in循环
-
getOwnPropertyDescriptor(target, propKey),getOwnPropertyDescriptor()方法拦截Object.getOwnPropertyDescriptor(),返回一个属性描述对象或者undefined
-
defineProperty(target, propKey, propDesc),defineProperty()方法拦截了object.defineProperty()操作
-
preventExtensions(target),preventExtensions()方法拦截Object.preventExtensions()。
-
getPrototypeOf(target),getPrototypeOf主要用来拦截获取对象原型,包括Object.prototype._proto_,Object.prototyoe.isPropertyOf(),Object.getPropertyOf(),Object.getPrototypeOf(),Reflect.getPrototypeOf(),instanceof
-
isExtensible(target),isExtensible()方法拦截Object.isExtensible()操作。
-
setPrototypeOf(target, proto),setPrototypeOf()方法主要用来拦截Object.setPrototypeOf()方法。
-
apply(target, object, args),apply()可以接受三个参数,分别是目标对象,上下文对象(this),和目标对象的参数数组,直接调用或call和apply调用时会被apply拦截,主要是为了改变this的指向操作
-
construct(target, args, newTarget),construct()可以接受三个参数,分别是目标对象,构造函数的参数数组,创造实例对象时,new命令作用的构造函数;construct()返回的必须是一个对象,否则会报错,由于construct拦截的是构造函数,所以它的目标对象必须是函数,否则就会报错
(PS:construct方法中的this指向的是handler,而不是实例对象)
应用场景
- 验证属性赋值
例如对象上的某个属性设置值时需要进行参数限制,只能设置为number类型,那么可以通过proxy的get和set方法拦截,若设置的类型非number,则抛出错误
Proxy和Object.defineProperty有哪些优缺点
Proxy的优势如下:
- Proxy可以直接监听对象而非属性
- Proxy可以直接监听数组的变化
- Proxy有多达13种拦截方法
- Proxy返回的是一个新对象,我们可以直接操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改
Object.defineProperty的优势如下:
-
兼容性好,支持IE9,而Proxy存在浏览器兼容性问题
13.每日一题——垃圾回收机制
字符串,对象,数组在定义时会动态的分配内存空间,来存储它们的变量,在某些变量不参与运行时,就需要系统回收被占用的内存空间,从而能够时这些空间再利用。
IE6的垃圾回收是根据内存分配量运行的,当环境中存在256个变量,4096个对象,64k的字符串任意一种情况的时候就会触发垃圾回收器工作,看起来很科学,不用按一段时间就调用一次,有时候会没必要,如果环境中就是有这么多变量一直存在,现在脚本如此复杂,那么结果就是垃圾回收器一直在工作,这种设计也就变得不合理。
- 现在各大浏览器通常采用的垃圾回收有两种方法:标记清除和引用计数
- 引用计数
引用计数的含义是跟踪每个值被引用的次数,当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1.当这个引用次数变成0时,则说明没有办法再访问这个值了,因此可以将其占据的空间回收,垃圾收集器下次再运行的时候会释放掉引用次数为0的值的内存。
引用计数的最大问题就是:循环引用
function func() {
let obj1 = {};
let obj2 = {};
obj1.a = obj2;
obj2.a = obj1;
}
出现循环引用后,该对象无法被回收,这样的互相引用如果说大量的存在就会导致大量的内存泄漏
解决方法:手动解除引用
obj1 = null;
obj2 = null;
- 标记清除
这是JavaScript最常用的垃圾回收方式。当变量进入执行环境中,就会标记这个变量进入环境,这时候变量所占用的内存是不能被释放的,当变量离开执行环境,才可以被释放。
垃圾收集器在运行的时候会给存储在内存中的变量都加上标记,然后会去掉环境中的变量以及被环境中的变量引用的标记。而此后再被加上标记的变量将会被视为准备删除的变量,环境中已经无法访问到这些变量,最后垃圾收集器完成内存清除工作,销魂那些带标记的值,回收他们所占的内存空间。
PS:标记清除也会遇到循环引用的问题,IE中有一部分对象并不是原生的JavaScript对象,因此需要手动断开js对象和DOM之间的联系,赋值为null,IE9把DOM和BOM转换成真正的JS对象,所以避免了这个问题
避免垃圾回收
- 数组array的优化
在清空array的时候,如果通过把array赋值为[],实则又创建了一个新的空对象,可以将array.length = 0来达到清空数组的目的,从而减少内存垃圾的产生
- 对象尽量复用
对象尽量复用,在循环等地方出现创建新对象,能复用就复用,不能用的对象尽量设置为null
- 优化循环
在循环中的函数表达式,能复用最好放到循环外面
避免内存泄露
- 意外的全局变量
没有被声明的变量,在页面关闭之前不会被释放
- 被遗忘的计时器或回调函数
如果id为Node的元素从DOM中移除,但是定时器还是存在,则不会被释放
- 闭包
闭包可以维持函数内部的变量,使其不被释放
- 没有清理的DOM元素引用
12.每日一题——协商缓存和强缓存的区别
Web缓存是可以自动保存常见文档副本的HTTP设备,当Web请求抵达缓存时,如果本地有”已缓存的“副本,就可以从本地存储设备而不是服务器中提取这个文档,通俗理解的话就是浏览器对之前请求过的文件进行缓存,以便下一次访问时重复使用,节省带宽,提高访问速度,降低服务器压力
缓存的优点:
- 缓存减少了冗余的数据传输,节省了网络费用
- 减少服务器负担,提高网站性能
- 加快了客户端加载数据的速度
- 用户体验友好
缺点:
- 资源如果有更改会造成缓存数据与服务器数据不一致
- 消耗内存
http缓存机制主要是在http响应头中设定,响应头相关字段为Expires、Cache-Control、Last-Modified、Etag。
http 1.0协议中,就是告诉浏览器在约定的这个时间前,可以直接从缓存中获取资源(representations),而无需从服务器去获取。
- Expires因为是对时间设定的,且时间是Greenwich Mean Time(GMT),而不是本地时间,所以对时间要求比较高
第一次请求:(强缓存)
第二次请求:(协商缓存)
强缓存
浏览器不会像服务器发送任何请求,直接从本地缓存中读取文件并返回Status Code:200 OK
使用强缓存策略时,如果缓存资源有效,则直接使用缓存资源,不必再像服务器发起请求,强缓存可以通过两种方式来设置,分别是http头信息中的Expries属性和Cache-Control属性。
-
Expries:过期时间,如果设置了时间,则浏览器会在设置的时间内直接读取缓存,不再请求
-
Cache-Control:当值设为max-age=300时,则代表在这个请求正确返回时间的5分钟内再次加载资源,就会命中强缓存
Cache-Control还有以下几个设置值:
(1) max-age:用来设置资源可以被缓存多长时间,单位为秒;
(2)s-maxage:和max-age是一样的,不过它只针对代理服务器缓存而言;
(3)public:指示响应可被任何缓存区缓存;
(4)private:只能针对个人用户,而不能被代理服务器缓存;
(5)no-cache:强制客户端直接向服务器发送请求,也就是说每次请求都必须向服务器发送,然后判断资源是否变更,是则返回新内容,否则返回304,未变更
(6)no-store:禁止一切缓存
cache-control是http1.1的头字段,expires是http1.0的头字段,如果expires和cache-control同时存在,cache-control会覆盖expires,Cache-Control的优先级要高于Expires。
如果命中强缓存,则无需发起新的请求,直接使用缓存的内容,如果没有命中强缓存,同时如果设置了协商缓存,则协商缓存会发生作用
协商缓存
命中协商缓存的条件有两个:
- max-age = XXX过期了
- 值为no-store
使用协商缓存策略时,会先向服务器发送一个请求,如果资源没有发生改变,则返回一个304状态码,让浏览器使用本地的缓存副本,如果资源发生了修改,则返回修改后的资源。
协商缓存可以通过两种方式来设置,分别是http头信息中的Etag和Last-Modified属性
-
服务器通过在响应头中添加Last-Modified属性来指出资源最后一次修改的时间,当浏览器下一次发起请求时,会在请求头中添加一个if-Modified-Since的属性,属性值为上一次资源返回时的Last-Modified的值。当请求发送到服务器后会通过这个属性来和资源的最后一次的修改时间来进行比较,以此来判断资源是否做了修改。如果资源没有修改,那么返回304状态码,让客户端使用本地缓存,如果资源已经修改了,则返回修改后的资。使用这种方法有一个缺点,就是Last-Modified标注的最后修改时间只能精确到秒级,如果某些文件在1秒以内被修改多次的话,那么文件已经变了但是Last-Modified却没有改变,这样会造成缓存命中不准确的问题。
-
因为Last-Modified的这种可能发生的不准确性,http提供了另外一种方式,就是Etag属性。服务器在返回资源的时候,在头信息中添加了Etag属性,这个属性是资源生成的唯一标识符,当资源发生改变时,这个值也会发生改变,在下一次资源请求时,浏览器会在请求头中添加一个if-None-Match属性,这个属性的值就是上次返回资源的Etag的值。服务器接收到请求后会根据这个值来和资源当前的Etag的值进行比较,以此来判断资源是否发生了变更,是否需要返回资源,通过这种方式,比Last-Modified的方式更加精准。
总结:
强缓存和协商缓存的策略在缓存命中时都会使用本地的缓存副本,区别只在于协商缓存会向服务器发送一次请求。他们缓存不命中时,都会向服务器请求来获取资源,在实际的缓存机制中,强缓存和协商缓存是一起合作使用的,浏览器首先会根据请求的信息来判断,强缓存是否命中,如果命中则直接使用资源,如果不命中则根据头信息向服务器发送请求,使用协商缓存,如果协商缓存命中,则服务器不返回资源,浏览器直接使用本地缓存副本,并返回304状态码,如果协商缓存没命中,则服务器返回最新的资源给浏览器。
11.每日一题——JavaScript的数据类型以及检测类型方法
八种数据类型
JavaScript共有八种数据类型:Null,Undefined,String,Number,Boolean,Symbol,BigInt,Object
- Symbol代表创建后不可变的数据类型,它主要是为了解决全局变量冲突的问题。
- BigInt是一种数字类型的数据,它可以表示任何精度的格式的整数,使用BigInt可以安全地存储和操作较大的整数
数据类型可以分为两类:
- 栈:原始数据类型:Null,Undefined,String,Number,Boolean,Symbol,BigInt
- 堆:引用数据类型:Object(包括对象,数组,函数等)
两种类型的数据存储位置不同:
-
原始数据是直接存放在栈(stack)中的简单数据段,占据空间小,大小固定,属于被频繁使用的数据,所以放在栈中存储
-
引用数据存放在堆(heap)中的对象,占据空间大,大小不固定,引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址,当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获取实体
堆和栈的概念存在于数据结构中和操作系统中,在数据结构中:
- 在数据结构中,栈的数据存取方式是先进后出
- 堆是一个优先队列,是按照优先级来进行排序的,优先级可以按照大小来规定
检测数据类型的方式
- typeof
console.log(typeof 1) //number
console.log(typeof '1') //string
console.log(typeof true) //boolean
console.log(typeof []) //object
console.log(typeof {}) //object
console.log(typeof undefined) //undefined
console.log(typeof null) //object
console.log(typeof function(){}) //function
typeof检测数据类型,会将null,数组,对象都判断为object,其余都是正确的
- instanceof
instanceof的内部运行机制是判断在其原型链中能否找到该类型的原型
console.log(1 instanceof Number) //false
console.log('1' instanceof String) //false
console.log(true instanceof Boolean) //false
console.log([] instanceof Array) //true
console.log(function(){} instanceof Function) //true
console.log({} instanceof Object) //true
instanceof只能正确判断引用数据类型,而不能判断基本数据类型。instanceof运算符可以用来判断一个对象在其原型链中是否存在一个构造函数的prototype属性
- constructor
console.log((1).constructor === Number) //true
console.log(('1').constructor === String) //true
console.log((true).constructor === Boolean) //true
console.log(([]).constructor === Array) //true
console.log((function(){}).constructor === Function) //true
console.log(({}).constructor === Object) //true
constructor有两个作用,一个是用来判断数据的类型,二是对象通过constructor对象访问它的构造函数,如果创建一个对象来改变它的原型,constructor就不能用来判断数据类型了
- Object.prototype.toString.call()
Object.prototype.toString.call()通过Object对象的原型方法toString来判断数据类型:
Object.prototype.toString.call(1) // [object Number]
Object.prototype.toString.call('1') //[object String]
Object.prototype.toString.call(true) //[object Boolean]
Object.prototype.toString.call(null) //[object Null]
Object.prototype.toString.call(undefined) //[object Undefined]
Object.prototype.toString.call([]) //[object Array]
Object.prototype.toString.call(function() {}) //[object Function]
Object.prototype.toString.call({}) //[object Object]
toString是Object的原型方法,而Array,Function等类作为Object的实例,都重写了toString方法,不同的对象类型调用toString方法时,根据原型链的知识,调用的都是重写之后的toString方法