挑战坚持学习1024天——前端之JavaScript高级 补充一些遗漏的知识点
js基础部分可到我文章专栏去看 ---点击这里
Day43【2022年9月5日】
学习重点: 纯函数
1.纯函数的定义
函数式编程中有一个非常重要的概念叫纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念;在react开发中纯函数是被多次提及的;比如react中组件就被要求像是一个纯函数(为什么是像,因为还有class组件),redux中有一个reducer的概念,也是要求必须是一个纯函数;所以掌握纯函数对于理解很多框架的设计是非常有帮助的;在redux中的reducer函数规定必须是一个纯函数,reducer中的state对象要求不能直接修改,可以通过扩展运算符把修改路径的对象都复制一遍,然后产生一个新的对象返回。
纯函数的维基百科定义:
- 在程序设计中,若一个函数符合以下条件,那么这个函数被称为纯函数:
- 此函数在相同的输入值时,需产生相同的输出。
- 函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的外部输出无关。
- 该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等。
总结如下:
-
确定的输入,一定会产生确定的输出;
-
函数在执行过程中,不能产生副作用;
副作用的概念 表示在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响,比如修改了全局变量,修改参数或者改变外部的存储;纯函数在执行的过程中就是不能产生这样的副作用;副作用往往比较容易产生bug。
2.副作用的优点
纯函数在函数式编程中非常重要,你可以安心的编写和安心的使用你在写的时候保证了函数的纯度,只是单纯实现自己的业务逻辑即可,不需要关心传入的内容是如何获得的或者依赖其他的外部变量是否已经发生了修改; 你在用的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出; React中就要求我们无论是函数还是class声明一个组件,这个组件都必须像纯函数一样,保护它们的props不被修改;相对来说react会灵活一些
3.实例
slice:slice截取数组时不会对原数组进行任何操作,而是生成一个新的数组;
splice:splice截取数组, 会返回一个新的数组, 也会对原数组进行修改;
slice就是一个纯函数,不会修改数组本身,而splice函数不是一个纯函数;
var arr = [1, 2, 3, 4, 5];
// slice 操作不会改变原数组是纯函数
var slicetest = arr.slice(0, 3);
console.log(slicetest);
console.log(arr);
//splice 操作会改变原数组该方法不是纯函数
//splice(start, deleteCount, item1, item2, ...)
var splicetest = arr.splice(2, 1);
console.log(arr);
console.log(splicetest);
// [1, 2, 3]
// [1, 2, 3, 4, 5]
// [1, 2, 4, 5]
// [3]
4.幂等
纯函数是幂等的
4.1什么是幂等
所谓的幂等性,是分布式环境下的一个常见问题,一般是指我们在进行多次操作时,所得到的结果是一样的,即多次运算结果是一致的。也就是说,用户对于同一操作,无论是发起一次请求还是多次请求,最终的执行结果是一致的,不会因为多次点击而产生副作用,一个函数被调用多触发时,保证内部状态的一致性。
4.2常见幂等性操作
在我们进行代码实现时,常见的请求有如下几种,他们的幂等性如下:
- select查询天然幂等;
- delete删除也是幂等,删除同一个数据多次其效果一样;
- update直接更新某个值时,幂等;
- update更新累加操作的的结果,非幂等;
- insert操作会每次都新增一条,非幂等;
4.3什么情况下会产生重复提交(非幂等性)
以下几种情况会导致非幂等性的结果出现:
- 连续点击提交两次按钮;
- 点击刷新按钮;
- 使用浏览器后退按钮重复之前的操作,导致重复提交表单;
- 使用浏览器历史记录重复提交表单;
- 浏览器重复地HTTP请求(POST)等。
4.4 纯函数和幂等
和纯函数相比,幂等主要强调多次调用,对内部的状态的影响是一样的(但多次调用返回值可能不同)。而纯函数,主要强调相同的输入,多次调用,输出也相同且无副作用。纯函数一定是幂等的 在任何可能的情况下通过幂等的操作限制副作用要比不做限制的更新要好得多。确保操作是幂等的,可避免意外的发生
实例
var Student = function(name,age){
this.name = name;
this.age = age;
};
Student.prototype.delName = function(){
var response = this.name ? this.name + "已被删除":"name不存在";
this.name = null;
return response;
}
//对内部的影响是一样的,但是返回值可以不同 也就是说幂等性是对外的 对内部的影响是一样的 但是对外的影响是不一样的
var lilei = new Student("lilei",19);
console.log(lilei.delName());//lilei已被删除
console.log(lilei.delName());//name不存在
console.log(lilei.delName());//name不存在
4.5 GET和POST
HTTP方法的幂等性是指一次和多次请求某一个资源应该具有同样的副作用。幂等操作用GET请求不改变服务器的状态,非幂等操作用POST会改变服务器的状态。值得注意,幂等性指的是作用于结果而非资源本身。可能某个方法可能每次会得到不同的返回内容,但并不影响资源,这样的也满足幂等性,例如get服务器当前时间
HTTP GET方法
- 用于获取资源,不管调用多少次接口,结果都不会改变,所以是幂等的。
- 若接口用于获取当前时间,它获取的是服务器当前时间,本身不会对资源产生影响,因此满足幂等性
HTTP POST方法
- POST是一个非幂等方法,它会对资源本身产生影响,每次调用都会有新的资源产生,因此不满足幂等性
HTTP PUT方法
- PUT方法直接把实体部分数据替换到了服务器的资源,但我们多次调用它时,只会产生一次影响,即有相同结果的HTTP方法,所有满足幂等性
HTTP PATCH方法
- PATCH方法是非幂等的,因为它提供的实体需要根据程序或其他协议的定义,解析后在服务器上执行,以此来修改服务器上的资源。也就是若反复提交,程序可能执行多次,对服务器资源可能造成额外影响。
HTTP DELETE方法
- DELETE方法用于删除资源,会将资源删除,但调用一次和调用多次的影响是相同的,因此也满足幂等性
根据HTTP规范,GET用于信息获取,而且应该是安全的和幂等的。
- 这里的安全并不是网络安全上的信息安全,而是指该操作作用于获取信息,而不是修改信息。即GET请求一般不产生副作用,仅仅获取资源信息,就行数据库查询一样,不会修改增加数据,不会影响资源的状态
根据HTTP规范,POST表示可能修改服务器上的资源的请求
- 例如注册用户信息时post提交表单请求,将用户信息添加到服务器数据库上,即对资源进行了修改
Day44【2022年9月6日】
学习重点: 赋值、深拷贝和浅拷贝
基本数据类型的变量值分配在栈上,引用数据类型的变量值分配在堆上,栈中只是存储具体堆中对象的地址。
1.赋值
赋值分两个,一个是基本数据类型的赋值,一个是引用数据类型的赋值,基本数据类型赋的是 “值”,引用数据类型赋的是 “指针地址”。 基本类型:赋值之后新旧变量不会相互影响,即拷贝。
实例
//在栈内开辟一个空间,空间名称叫a,存放值1;
var a = 1;
//在栈内开辟一个空间,空间名字叫b。接着先把a的值1复制一份,然后存放进b
var b = a;
b = 2;
console.log(a); //1
console.log(b); // 2
2、引用类型:赋址,实际上是把一个变量的地址赋给另一个变量,两个变量具有相同的引用,指向堆内存中同一个对象,修改其中一个变量的值,新旧变量相互之间有影响即在新变量上改变对象值,旧变量对应值也会改变。通过严格相等运算符"==="来检测二者是指向同一个地址。
实例
//引用类型
var a = {name: 'a' , city :{ name: 'beijing' } };
//a赋值给b,此时b会在栈开辟一个空间b,用来放置指针地址,这个指针指向a所在堆的对象数据
var b = a;
//修改赋值后的值b,其实就是修改b的指针指针地址所指向的对象数据
b.name = 'b';
//修改b会影响原数据(所有层次的数据都会影响)
//这个原数据其实不是原数据,因为a和b其实都是同一个数据
//就像从中国去美国,可以从a地点(比如北京)或者b地点(比如上海)坐飞机去,但是到达的都是同一个地方(也就是对象数据)
b.city.name = 'shanghai';
console.log(a, b); // {name: "b", city: {name: "shanghai"}} {name: "b", city: {name: "shanghai"}}
console.log(a === b); //true
对b的修改会影响a原本的值。对a的修改同样会同步b的值。
直接赋值,修改赋值后的对象b的非对象属性,也会影响原对象a的非对象属性;修改赋值后的对象b的对象属性,也会影响原对象a的对象属性。
很多时候,我们想要的结果是两个初始值相同的变量互不影响,由上可知我们其实只需要处理引用类型的变量,所以就要使用到拷贝(分为深浅两种)来处理引用类型
2.浅拷贝
以下是来自mdn的解释
对象的浅拷贝是一种拷贝,其属性与创建拷贝的源对象共享相同的引用(指向相同的底层值)。结果,当您更改源或副本时,您也可能导致另一个对象也发生更改 - 因此,您最终可能会无意中导致对源或副本的更改是您不期望的。该行为与深层副本的行为形成对比,其中源和副本是完全独立的。对于浅拷贝,重要的是要理解有选择地更改对象中现有元素的共享属性的值不同于为现有元素分配全新的值。
浅拷贝改变基础数据类型不会影响到原数据,但是改变引用数据类型就会影响到原数据(因为拷贝的是内存地址,还是指向同一个堆)。
深拷贝是在堆中再开辟一片空间存储拷贝的值,而浅拷贝则是直接拷贝指向堆内存的指针。
实例
var a = {name: 'a' , city :{ name: 'beijing' } };
var b = {...a}
// var b = Object.assign({}, a);
b.name = 'b';
b.city.name = 'shanghai';
console.log(a, b); // {name: "a", city: {name: "shanghai"}} {name: "b", city: {name: "shanghai"}}
console.log(a === b); //false
for实现浅拷贝
function shallowCopy(obj) { //判断是否是对象
if (typeof obj !== 'object') return; //根据obj的类型判断是新建一个数组还是对象
var newObj = obj instanceof Array ? [] : {}; //遍历obj,并且判断是obj的属性才拷贝
for (var key in obj) { //排除原型链上的属性
if (obj.hasOwnProperty(key)) { //判断obj子元素的类型,如果是对象,递归复制
newObj[key] = obj[key]; //如果不是,就直接赋值
}
}
return newObj; //返回拷贝的对象
}
在 JavaScript 中,所有标准的内置对象复制操作(扩展语法(...)、Array.prototype.concat()、Array.prototype.slice()、Array.from()、Object.assign()和Object.create())、以及一层遍历都是浅拷贝。
修改赋值后的对象b的非对象属性,不会影响原对象a的非对象属性;修改赋值后的对象b的对象属性,却会影响原对象a的对象属性。
浅拷贝缺点
- 只能拷贝对象的第一层
- 如果对象的属性值是对象类型,拷贝的是内存地址,修改拷贝后的对象会影响原对象
- 不能拷贝不可枚举的属性
- 不能拷贝对象的继承属性和方法
- 不能拷贝对象的原型属性和方法
- 没有嵌套的情况下浅拷贝是安全的
- 有嵌套的情况下浅拷贝是不安全的
浅拷贝优点
- 拷贝速度快
3.深拷贝
对象的深层副本是一个副本,其属性不共享与创建副本的源对象相同的引用(指向相同的底层值)。因此,当您更改源或副本时,您可以确保不会导致其他对象也发生更改;也就是说,您不会无意中对源或副本造成您不期望的更改。这种行为与浅拷贝的行为形成对比,其中对源或副本的更改也可能导致另一个对象也发生更改(因为两个对象共享相同的引用)。
深拷贝是完完全全的拷贝,新旧变量之间不会相互影响。深拷贝会另外拷贝一份一个一模一样的对象,但是不同的是会从堆内存中开辟一个新的区域存放新对象,新对象跟原对象不再共享内存,修改赋值后的对象b不会改到原对象a。深拷贝的实现原理是递归,如果obj的子元素还是一个对象,就继续递归,直到子元素不是对象为止。
即深拷贝,修改赋值后的对象b的非对象属性,不会影响原对象a的非对象属性;修改赋值后的对象b的对象属性,也不会影响原对象a的对象属性。而且,二者不指向同一个对象。
对 JavaScript 对象进行深度复制的一种方法(如果可以序列化)是JSON.stringify()将对象转换为 JSON 字符串,然后JSON.parse()将字符串转换回(全新的)JavaScript 对象。
实例
var a = {name: 'a' , city :{ name: 'beijing' } };
var b = JSON.parse(JSON.stringify(a));
b.name = 'b';
b.city.name = 'shanghai';
console.log(a, b); // {name: "a", city: {name: "beijing"}} {name: "b", city: {name: "shanghai"}}
console.log(a === b); //false
但是由于用到了JSON.stringify(),这也会导致一系列的问题,因为要严格遵守JSON序列化规则:原对象中如果含有Date对象,JSON.stringify()会将其变为字符串,之后并不会将其还原为日期对象。或是含有RegExp对象,JSON.stringify()会将其变为空对象,属性中含有NaN、Infinity和-Infinity,则序列化的结果会变成null,如果属性中有函数,undefined,symbol则经过JSON.stringify()序列化后的JSON字符串中这个键值对会消失,因为不支持。
也可以手写实现深拷贝
var a = {name: 'a' , city :{ name: 'beijing' } };
function deepCopy(obj) { //判断是否是对象
if (typeof obj !== 'object') return; //根据obj的类型判断是新建一个数组还是对象
var newObj = obj instanceof Array ? [] : {}; //遍历obj,并且判断是obj的属性才拷贝
for (var key in obj) { //排除原型链上的属性
if (obj.hasOwnProperty(key)) { //判断obj子元素的类型,如果是对象,递归复制
newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key]; //如果不是,就直接赋值
}
}
return newObj; //返回拷贝的对象
}
var b = deepCopy(a);
console.log(b);
b.name = 'b';
b.city.name = 'shanghai';
console.log(a,b); // {name: "a", city: {name: "beijing"}} {name: "b", city: {name: "shanghai"}}
console.log(a === b); //false
深拷贝的缺点
- 无法拷贝函数;
- 无法拷贝正则;
- 无法拷贝日期;
- 无法拷贝undefined;
- 无法拷贝循环引用的对象;
- 会把对象的方法也拷贝过来,如果对象中有方法,可以用浅拷贝来实现。
深拷贝的优点
- 可以拷贝任意层级的对象
- 拷贝后的对象和原对象互不影响
- 可以拷贝不可枚举的属性
- 可以拷贝对象的继承属性和方法
- 可以拷贝对象的原型属性和方法
运用场景
当你想使用某个对象的值,在修改时不想修改原对象,那么可以用深拷贝弄一个新的内存对象。像es6的新增方法都是深拷贝,所以推荐使用es6语法。
4.今日精进
看待事物不能仅仅停留在表象,应透过表象究其本质,掌握其规律、摸索其起因、了解其过程。将其形成成习惯,洞察事物本质的习惯会让你在决策时少走弯路。
Day45【2022年9月7日】
学习重点: Set和Map(ES6/ES2015)及WeakSet和WeakMap
1. Set
ECMAScript 6 新增的 Set 是一种新集合类型,为javascript带来集合数据结构。Set 对象允许你存储任何类型的唯一值,无论是基本数据类型或者引用类型。Set对象是值的集合,可以按照插入的顺序迭代它的元素。Set 中的元素只会出现一次,即 Set 中的元素是唯一的。Set 在很多方面都 像是加强的 Map,这是因为它们的大多数 API 和行为都是共有的。
1.2. Set的运用
在ES6之前,我们存储数据的结构主要有两种:数组、对象。在ES6中新增了另外两种数据结构:Set、Map,以及它们的另外形式WeakSet、WeakMap。Set是一个新增的数据结构,可以用来保存数据,类似于数组,但是和数组的区别是元素不能重复。创建Set我们需要通过Set构造函数(暂时没有字面量创建的方式):我们可以发现Set中存放的元素是不会重复的,那么Set有一个非常常用的功能就是给数组去重。
去重实例
// Set 去重
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7, 8, 9];
let set = new Set(arr);
console.log(set); // Set { 1, 2, 3, 4, 5, 6, 7, 8, 9 }
//使用Array.from转成数组
let item = Array.from(new Set(arr));
console.log(item); // [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
// ... 扩展运算符 转成数组
let set1 = [...new Set(arr)];
console.log(set1); // [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
1.3. Set 常见的方法
Set常见的属性:
size:返回Set中元素的个数;
Set常用的方法:
-
add(value):添加某个元素,返回Set对象本身;
-
delete(value):从set中删除和这个值相等的元素,返回boolean类型;
-
has(value):判断set中是否存在某个元素,返回boolean类型;
-
clear():清空set中所有的元素,没有返回值;
-
forEach(callback, [, thisArg]):通过forEach遍历set;
另外Set是支持for of的遍历的
实例
// Set 属性方法
let myset = new Set(); // 创建一个空Set对象
console.log(myset); // Set(0) {}
myset.add(1); // 添加一个元素 1
myset.add(2); // 添加一个元素 2
myset.size; // 2
myset.has(1); // true
myset.delete(1); // 删除一个元素 1
myset.clear(); // 清空Set对象
console.log(myset); // Set(0) {}
// Set 遍历
myset.add([1, 2, 3]);
for (let item of myset) {
console.log(item); // [ 1, 2, 3 ]
}
2. WeakSet
ECMAScript 6 新增的“弱集合”(WeakSet)是一种新的集合类型,为这门语言带来了集合数据结构。WeakSet 是 Set 的“兄弟”类型,其 API 也是 Set 的子集。WeakSet 中的“weak”(弱),描述的是 JavaScript 垃圾回收程序对待“弱集合”中值的方式。
2.1 WeakSet使用
和Set类似的另外一个数据结构称之为WeakSet,也是内部元素不能重复的数据结构。
那么和Set有什么区别呢?
- 区别一:WeakSet中只能存放对象类型,不能存放基本数据类型;
let weakeset = new WeakSet();
weakeset.add(1); // TypeError: Invalid value used in weak set
- 区别二:WeakSet对元素的引用是弱引用,如果没有其他引用对某个对象进行引用,那么GC可以对该对象进行回收;
WeakSet常见的方法:
- add(value):添加某个元素,返回WeakSet对象本身;
- delete(value):从WeakSet中删除和这个值相等的元素,返回boolean类型;
- has(value):判断WeakSet中是否存在某个元素,返回boolean类型; 跟Set是一样的
2.2 WeakSet应用
注意:WeakSet不能遍历 因为WeakSet只是对对象的弱引用,如果我们遍历获取到其中的元素,那么有可能造成对象不能正常的销毁。所以存储到WeakSet中的对象是没办法获取的;
WeakSet运用
const pwset = new WeakSet(); // 创建一个空的WeakSet对象
class Person { // 通过构造函数创建对象
constructor() { // 将对象添加到WeakSet对象中
pwset.add(this); // this指向当前对象
}
running() { // 判断当前对象是否在WeakSet对象中
if (!pwset.has(this)) { // this指向当前对象
throw new TypeError('this is not a Person'); // 抛出错误
}
console.log('running',this); // this指向当前对象
}
}
3.Map
ECMAScript 6 以前,在 JavaScript 中实现“键/值”式存储可以使用 Object 来方便高效地完成,也就是使用对象属性作为键,再使用属性来引用值。但这种实现并非没有问题,为此 TC39 委员会专门为“键/值”存储定义了一个规范。作为 ECMAScript 6 的新增特性,Map 是一种新的集合类型,为这门语言带来了真正的键/值存储机制。Map 的大多数特性都可以通过 Object 类型实现,但二者之间还是存在一些细微的差异。具体实践中使用哪一个,还是值得细细甄别。
Map 对象保存键值对,并且能够记住键的原始插入顺序。任何值(对象或者基本类型)都可以作为一个键或一个值。一个 Map 对象在迭代时会根据对象中元素的插入顺序来进行——一个 for...of 循环在每次迭代后会返回一个形式为 [key,value] 的数组。
3.1 Map的运用
Map,用于存储映射关系。但是我们可能会想,在之前我们可以使用对象来存储映射关系,他们有什么区别呢?事实上我们对象存储映射关系只能用字符串(ES6新增了Symbol)作为属性名(key);某些情况下我们可能希望通过其他类型作为key,比如对象,这个时候会自动将对象转成字符串来作为key;
设置的时候要以键值对的形式进行存储,取的时候要去键。 实例
// Map 对象
let map = new Map();
map.set('name', '张三');
map.set('age', 18);
console.log(map); // Map(3) { 'name' => '张三', 'age' => 18, [Function: set] => [Function: set] }
console.log(map.get('name')); // 张三
console.log(map.get('张三')); // undefined
let map1 = new Map([
['name', '张三'],
['age', 18]
]);
console.log(map1); // Map(2) { 'name' => '张三', 'age' => 18 }
console.log(map1.get('name')); // 张三
let map2 = new Map();
map2.set('name');
console.log(map2); // Map(1) { 'name' => undefined }
3.2 Map常见方法
Map常见的属性:
- size:返回Map中元素的个数;
Map常见的方法:
-
set(key, value):在Map中添加key、value,并且返回整个Map对象;
-
get(key):根据key获取Map中的value;
-
has(key):判断是否包括某一个key,返回Boolean类型;
-
delete(key):根据key删除一个键值对,返回Boolean类型;
-
clear():清空所有的元素;
-
forEach(callback, [, thisArg]):通过forEach遍历Map;
Map也可以通过for of进行遍历。
实例
// Map 属性方法
let map = new Map();
map.set('name', '张三');
map.set('age', 18);
console.log(map); // Map(2) { 'name' => '张三', 'age' => 18 }
console.log(map.size); // 2
console.log(map.has('name')); // true
console.log(map.has('张三')); // false
map.delete('name');
console.log(map); // Map(1) { 'age' => 18 }
map.clear();
console.log(map); // Map(0) {}
// Map 遍历
// for of
let map1 = new Map([
['name', '张三'],
['age', 18]
]);
for (let item of map1) {
console.log(item); // [ 'name', '张三' ] [ 'age', 18 ]
}
// Map forEach
map1.forEach((value, key) => {
console.log(value, key); // 张三 name 18 age
});
// Map 转成数组
let map2 = new Map([
['name', '张三'],
['age', 18]
]);
let arr = [...map2];
console.log(arr); // [ [ 'name', '张三' ], [ 'age', 18 ] ]
// Map 转成对象
let map3 = new Map([
['name', '张三'],
['age', 18]
]);
let obj = Object.fromEntries(map3);
console.log(obj); // { name: '张三', age: 18 }
4.WeakMap
ECMAScript 6 新增的“弱映射”(WeakMap)是一种新的集合类型,为这门语言带来了增强的键/ 值对存储机制。WeakMap 是 Map 的“兄弟”类型,其 API 也是 Map 的子集。WeakMap 中的“weak”(弱),描述的是 JavaScript 垃圾回收程序对待“弱映射”中键的方式。
4.1WeakMap使用
和Map类型的另外一个数据结构称之为WeakMap,也是以键值对的形式存在的。那么和Map有什么区别呢?
- 区别一:WeakMap的key只能使用对象,不接受其他的类型作为key;
- 区别二:WeakMap的key对对象想的引用是弱引用,如果没有其他引用引用这个对象,那么GC可以回收该对象;
let weakmap = new WeakMap();
// weakmap.set(1, 2); // TypeError: Invalid value used as weak map key
// weakmap.set('name', '张三'); // TypeError: Invalid value used as weak map key
//weakmap.set({}, 2); // {Object => 2}
weakmap.set({ a: 1 }, 2); //{Object => 2}
WeakMap常见的方法有四个:
- set(key, value):在Map中添加key、value,并且返回整个Map对象;
- get(key):根据key获取Map中的value;
- has(key):判断是否包括某一个key,返回Boolean类型;
- delete(key):根据key删除一个键值对,返回Boolean类型;
4.2WeakMap应用
注意: WeakMap也是不能遍历的没有forEach方法,也不支持通过for of的方式进行遍历;
放在WeakMap中的对象都是弱引用,在实际应用的时候可以防止对象无法被回收比如以下代码
let map = new Map();
let button = document.querySelector('#login');
map.set(button, {disabled: true});
这样会导致按钮的Dom无法回收
用WeakMap可以解决该问题
let weakMap = new WeakMap();
let button = document.querySelector('#login');
weakMap.set(button, {disabled: true});
5.今日精进
跳出自己的舒适圈,给与自己可试错的机会。未知与挑战能真正磨练一个人的心智。
Day46【2022年9月8日】
学习重点: 防抖(debounce)和节流(throttle)
- 节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效
- 防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时
1.防抖
最后一个人说了算 用电梯的来举例 电梯第一个人进来后,等待15秒。如果过程中又有人进来,15秒等待重新计时,直到15秒后开始运送,这是防抖。只有在某个时间内,没有再次触发某个函数时,才真正的调用这个函数; 当事件触发时,相应的函数并不会立即触发,而是会等待一定的时间;当事件密集触发时,函数的触发会被频繁的推迟;只有等待了一段时间也没有事件触发,才会真正的执行响应函数;
1.1手写防抖
function debounce(fn, wait) {
let timer = null; // 创建一个标记用来存放定时器的返回值
return function () {
let context = this; // 保存当前this的指向
args = arguments; // 保存当前调用debounce函数时传入的参数
// 如果此时存在定时器的话,则取消之前的定时器重新记时
if (timer) {
clearTimeout(timer); // 取消定时器
timer = null; // 清空定时器
}
// 设置定时器,使事件间隔指定事件后执行
timer = setTimeout(() => {
fn.apply(context, args); // 执行事件
}, wait);
}
}
1.2运用场景
防抖的应用场景很多:
- 输入框中频繁的输入内容,搜索或者提交信息;
- 频繁的点击按钮,触发某个事件;
- 监听浏览器滚动事件,完成某些特定操作;
- 用户缩放浏览器的resize事件;
2.节流
第一个人说了算 电梯第一个人进来后,15秒后准时运送一次,这是节流。
2.1手写节流
// 节流
function throttle(fn, delay) { // fn是要执行的函数,delay是时间间隔
let flag = true; // 设置一个标志,初始值为true
return function () { // 返回一个函数
if (!flag) return; // 如果标志为false,直接返回
flag = false; // 如果标志为true,将标志设置为false
timer = setTimeout(() => { // 设置一个定时器
fn(); // 执行函数
flag = true; // 执行完函数后,将标志设置为true
}, delay);
}
}
// 调用节流函数
let a = throttle(() => {
console.log('throttle'); // throttle
}
, 1000);
//调用的是 return 里面的函数 不影响/执行let flag = true;
a();
a();
a();
//节流函数时间戳版
function throttle(fn, delay) { // fn是要执行的函数,delay是时间间隔
let prev = Date.now(); // 设置一个标志,初始值为true
return function () { // 返回一个函数
let context = this; // 保存this
args = arguments; // 保存参数
let now = Date.now(); // 获取当前时间
if (now - prev >= delay) { // 如果当前时间减去上一次执行的时间大于等于时间间隔,则执行函数
fn.apply(context, args); // 执行函数
prev = Date.now(); // 更新上一次执行的时间
}
}
}
节流的应用场景:
- 监听页面的滚动事件;
- 鼠标移动事件;
- 用户频繁点击按钮操作;
- 游戏中的一些设计;
3.今日精进
智者务其实,愚者争虚名。智者,追求事物的本质,不会计较别人说了什么,不会在意表面风光,更看中有没有给自己带来实际的利益。愚者追求事物的外表虚名,会为了一点表面上的东西争的面红耳赤。
Day47【2022年9月9日】
学习重点: js中的代理(Proxy)和反射(Reflect)
1.代理
ECMAScript 6 新增的代理和反射为开发者提供了拦截并向基本操作嵌入额外行为的能力。具体地说,可以给目标对象定义一个关联的代理对象,而这个代理对象可以作为抽象的目标对象来使用。在 ES6 之前,ECMAScript 中并没有类似代理的特性。由于代理是一种新的基础性语言能力,很多转译程序都不能把代理行为转换为之前的 ECMAScript 代码,因为代理的行为实际上是无可替代的。为此,代理和反射只在百分之百支持它们的平台上有用。可以检测代理是否存在,不存在则提供后备代码。不过这会导致代码冗余,因此并不推荐。
代理的概念分为正向代理及反向代理
正向代理代理的对象是客户端,隐藏了真实的客户端。 由多个客户端向一台明确已知的的服务器上请求数据,隐藏了真实请求的客户端,服务端不知道真实的客户端是谁,客户端请求的服务都被代理服务器代替去请求,客户端对于服务器来说是不透明可见的,服务端对于客户器来说是透明可见的。常见的运用场景有VPN网络代理
反向代理代理的对象是服务端,隐藏了真实的服务端。 反向代理隐藏了真实的服务端,服务端对客户端来说是服务端是不透明可见的,反向代理服务器会帮我们把请求转发到真实的服务器那里去,我们作为客户方,不知道真正提供服务的人是谁。对于客户端来说,代理服务器就好像目标服务器一样,客户端是直接访问代理服务器,代理服务器后台会进行转发到对应目标服务器,对客户端来说是看不到的,客户端不知道具体是哪个目标服务器,但是访问代理服务器是和请求目标服务器效果是一样的。
常见的运用场景有:反向代理服务器可以做负载均衡,根据所有真实服务器的负载情况,将客户端请求分发到不同的真实服务器上。提高访问速度, 反向代理服务器可以对于静态内容及短时间内有大量访问请求的动态内容提供缓存服务,提高访问速度。提供安全保障, 反向代理服务器可以作为应用层防火墙,为网站提供对基于Web的攻击行为(例如DoS/DDoS)的防护,更容易排查恶意软件等。还可以为后端服务器统一提供加密和SSL加速(如SSL终端代理),提供HTTP访问认证等。
跨域代理要看怎么配置可以配置反向代理也可以配置正向代理解决跨域问题,从前后端的角度分析既可以从前端解决跨域也可以从后端解决跨域。这里不展开详细叙述,后文会详细分析跨域。现在分析代理问题,vue可配置反向代理解决跨域问题。(Vue中用webpack启动了本地服务器,在这里相当于代理服务器)。或者nginx可配置正向/反向代理来解决跨域,解决跨域的核心在于处理同源策略,而配置代理服务器的方式相当于是骗过浏览器告诉浏览器这是同源的从而解决了跨域。一般来说正向代理必须要客户端做一些配置配置正确的域名,方向代理隐藏了服务器则不用,所以一般在开发环境用正常代理,开发环境用反向代理,但是这样还是繁琐了些,直接一个反向代理就不用切来切去。 正向代理方向代理好文推荐 ---点击这里
1.1 Proxy基本用法
const p = new Proxy(target, handler)
target要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。(操作代理的行为) 以上两者缺少其中任何一个参数都会抛出 TypeError。要创建空代理,可以传一个简单的对象字面量作为处理程 序对象,从而让所有操作畅通无阻地抵达目标对象。
// 在代理对象上执行的任何操作实际上都会应用到目标对象。唯一可感知的不同就是代码中操作的是代理对象。
const target = {
id: 'target'
};
const handler = {};
const proxy = new Proxy(target, handler);
// id 属性会访问同一个值
console.log(target.id); // target
console.log(proxy.id); // target
// 给目标属性赋值会反映在两个对象上
// 因为两个对象访问的是同一个值
target.id = 'foo';
console.log(target.id); // foo
console.log(proxy.id); // foo
// 给代理属性赋值会反映在两个对象上
// 因为这个赋值会转移到目标对象
proxy.id = 'bar';
console.log(target.id); // bar
console.log(proxy.id); // bar
// hasOwnProperty()方法在两个地方
// 都会应用到目标对象
console.log(target.hasOwnProperty('id')); // true
console.log(proxy.hasOwnProperty('id')); // true
// Proxy.prototype 是 undefined
// 因此不能使用 instanceof 操作符
console.log(target instanceof Proxy); // TypeError: Function has non-object prototype 'undefined' in instanceof check
console.log(proxy instanceof Proxy); // TypeError: Function has non-object prototype 'undefined' in instanceof check
// 严格相等可以用来区分代理和目标
console.log(target === proxy); // false
1.2 捕获器
使用代理的主要目的是可以定义捕获器(trap)。捕获器就是在处理程序对象中定义的“基本操作的 拦截器”。每个处理程序对象可以包含零个或多个捕获器,每个捕获器都对应一种基本操作,可以直接 或间接在代理对象上调用。每次在代理对象上调用这些基本操作时,代理可以在这些操作传播到目标对 象之前先调用捕获器函数,从而拦截并修改相应的行为。
注意 捕获器(trap)是从操作系统中借用的概念。在操作系统中,捕获器是程序流中的一个同步中断,可以暂停程序流,转而执行一段子例程,之后再返回原始程序流。
如果我们想要侦听某些具体的操作,那么就可以在handler中添加对应的捕捉器(Trap):
set和get分别对应的是函数类型;
set函数有四个参数:
target:目标对象(侦听的对象);
property:将被设置的属性key;
value:新属性值;
receiver:调用的代理对象;
get函数有三个参数:
target:目标对象(侦听的对象);
property:被获取的属性key;
receiver:调用的代理对象;
实例
const obj1 = {
name: 'obj',
age:18,
};
const objProxy = new Proxy(obj1,{
get: function(target,key){
console.log("get侦听");
return key in target
},
set: function(target,key,value){
console.log("set侦听");
target[key] = value;
},
has: function(target,key){
console.log("has侦听");
return key in target;
},
deleteProperty: function(target,key){
console.log("deleteProperty侦听");
delete target[key];
}
});
console.log(objProxy.name);
objProxy.name = 'objProxy';
console.log(objProxy.name);
console.log('name' in objProxy);
delete objProxy.name;
console.log(objProxy.name);
// get侦听
// true
// set侦听
// get侦听
// true
// has侦听
// true
// deleteProperty侦听
// get侦听
// false
//obj1
//{age: 18}
Proxy所有捕获器
handler.getPrototypeOf()
Object.getPrototypeOf //方法的捕捉器。
handler.setPrototypeOf()
Object.setPrototypeOf //方法的捕捉器。
handler.isExtensible()
Object.isExtensible //方法的捕捉器(判断是否可以新增属性)。
handler.preventExtensions()
Object.preventExtensions //方法的捕捉器。
handler.getOwnPropertyDescriptor()
Object.getOwnPropertyDescriptor //方法的捕捉器。
handler.defineProperty()
Object.defineProperty //方法的捕捉器。
handler.ownKeys()
Object.getOwnPropertyNames //方法和Object.getOwnPropertySymbols //方法的捕捉器。
handler.has()
//in 操作符的捕捉器。
handler.get()
//属性读取操作的捕捉器。
handler.set()
//属性设置操作的捕捉器。
handler.deleteProperty()
//delete 操作符的捕捉器。
handler.apply()
//函数调用操作的捕捉器。
handler.construct()
//new 操作符的捕捉器。
1.3 Proxy的construct和apply
function foo(){
console.log("foo1",this+'11',arguments+"2");
return 'foo';
}
const fooProxy = new Proxy(foo,{
apply: function(target,thisArg,args){
console.log("apply侦听");
return target.apply(thisArg,args);
},
construct: function(target,argArray,newTarget){
console.log("construct侦听");
return new target();
}
});
console.log(fooProxy(1,2,3));
//apply侦听
//foo1 [object Window]11 [object Arguments]2
//foo
实际运用
在 Vue3.0 中通过 Proxy 来替换原本的 Object.defineProperty 来实现数据响应式。
Proxy 是 ES6 中新增的功能,它可以用来自定义对象中的操作。
let p = new Proxy(target, handler)
target 代表需要添加代理的对象,handler 用来自定义对象中的操作,比如可以用来自定义 set 或者 get 函数。
下面来通过 Proxy 来实现一个数据响应式:
let onWatch = (obj, setBind, getLogger) => {
//下面的get添加的是对象 handler的方法 在浏览器中 输出 handler 会直接执行该方法浏览器会有返回值
//控制台返回undefined 的原因
//因为Console控制台的实质,即eval()函数,所以当输入的表达式或语句没有返回值时,会返回 undefined 。
let handler = {
get(target, property, receiver) {
getLogger(target, property)
return Reflect.get(target, property, receiver)
},
set(target, property, value, receiver) {
setBind(value, property)
return Reflect.set(target, property, value)
}
}
return new Proxy(obj, handler)
}
let obj = { a: 1 }
let p = onWatch(
obj,
(v, property) => {
console.log(`监听到属性${property}改变为${v}`)
},
(target, property) => {
console.log(`'${property}' = ${target[property]}`)
}
)
p.a = 2 // 监听到属性a改变
p.a // 'a' = 2
在上述代码中,通过自定义 set 和 get 函数的方式,在原本的逻辑中插入了我们的函数逻辑,实现了在对对象任何属性进行读写时发出通知。
当然这是简单版的响应式实现,如果需要实现一个 Vue 中的响应式,需要在 get 中收集依赖,在 set 派发更新,之所以 Vue3.0 要使用 Proxy 替换原本的 API 原因在于 Proxy 无需一层层递归为每个属性添加代理,一次即可完成以上操作,性能上更好,并且原本的实现有一些数据更新不能监听到,但是 Proxy 可以完美监听到任何方式的数据改变,唯一缺陷就是浏览器的兼容性不好。
补充对对象的理解
创建自定义对象的通常方式是创建 Object 的一个新实例,然后再给它添加属性和方法,如下例
所示:
let person = new Object();
person.name = "Nicholas";
person.age = 29;
person.job = "Software Engineer";
person.sayName = function() {
console.log(this.name);
};
这个例子创建了一个名为 person 的对象,而且有三个属性(name、age 和 job)和一个方法
(sayName())。sayName()方法会显示 this.name 的值,这个属性会解析为 person.name。早期
JavaScript 开发者频繁使用这种方式创建新对象。几年后,对象字面量变成了更流行的方式。前面的例
子如果使用对象字面量则可以这样写:
let person = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
这个例子中的 person 对象跟前面例子中的 person 对象是等价的,它们的属性和方法都一样。这
些属性都有自己的特征,而这些特征决定了它们在 JavaScript 中的行为。
2.反射
Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy handlers (en-US)的方法相同。Reflect不是一个函数对象,因此它是不可构造的。与大多数全局对象不同Reflect并非一个构造函数,所以不能通过new 运算符对其进行调用,或者将Reflect对象作为一个函数来调用。Reflect的所有属性和方法都是静态的(就像Math对象)。Reflect 对象提供了以下静态方法,这些方法与proxy handler methods (en-US)的命名相同。其中的一些方法与 Object 相同,尽管二者之间存在某些细微上的差别。可以点击这里查看差别
那么这个Reflect它主要提供了很多操作JavaScript对象的方法,有点像Object中操作对象的方法; 比如Reflect.getPrototypeOf(target)类似于 Object.getPrototypeOf(); 比如Reflect.defineProperty(target, propertyKey, attributes)类似于Object.defineProperty() ;
如果我们有Object可以做这些操作,那么为什么还需要有Reflect这样的新增对象呢 这是因为在早期的ECMA规范中没有考虑到这种对 对象本身 的操作如何设计会更加规范,所以将这些API放到了Object上面; 但是Object作为一个构造函数,这些操作实际上放到它身上并不合适; 另外还包含一些类似于 in、delete操作符,让JS看起来是会有一些奇怪的;所以在ES6中新增了Reflect,让我们这些操作都集中到了Reflect对象上;
2.1Reflect基本用法
Reflect.getPrototypeOf(target)
类似于 Object.getPrototypeOf()。 Reflect.setPrototypeOf(target, prototype)
设置对象原型的函数. 返回一个 Boolean, 如果更新成功,则返回true。 Reflect.isExtensible(target)
类似于 Object.isExtensible()
Reflect.preventExtensions(target)
类似于 Object.preventExtensions()。返回一个Boolean。 Reflect.getOwnPropertyDescriptor(target, propertyKey) 类似于 Object.getOwnPropertyDescriptor()。如果对象中存在
该属性,则返回对应的属性描述符, 否则返回 undefined.
Reflect.defineProperty(target, propertyKey, attributes)
和 Object.defineProperty() 类似。如果设置成功就会返回 true
Reflect的常见方法
Reflect.ownKeys(target)
返回一个包含所有自身属性(不包含继承属性)的数组。(类似于Object.keys(), 但不会受enumerable影响).
Reflect.has(target, propertyKey) 判断一个对象是否存在某个属性,和 in 运算符 的功能完全相同。
Reflect.get(target, propertyKey[, receiver])
获取对象身上某个属性的值,类似于 target[name]。 Reflect.set(target, propertyKey, value[, receiver])
将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true。 Reflect.deleteProperty(target, propertyKey) 作为函数的delete操作符,相当于执行 delete target[name]。 Reflect.apply(target, thisArgument, argumentsList) 对一个函数进行调用操作,同时可以传入一个数组作为调用参数。和Function.prototype.apply() 功能类似。
Reflect.construct(target, argumentsList[, newTarget])
对构造函数进行 new 操作,相当于执行 new target(...args)。
将之前Proxy案例中对原对象的操作,都修改为Reflect来操作:
const obj = {
name: 'obj',
age:18,
};
const objProxy = new Proxy(obj,{
get: function(target,prop){
console.log("get侦听");
return Reflect.get(target,prop);
},
set: function(target,prop,value){
console.log("set侦听");
return Reflect.set(target,prop,value);
},
has: function(target,prop){
console.log("has侦听");
return Reflect.has(target,prop);
},
deleteProperty: function(target,prop){
console.log("deleteProperty侦听");
return Reflect.deleteProperty(target,prop);
}
});
console.log(objProxy.name);
objProxy.name = 'objProxy';
console.log(objProxy.name);
console.log('name' in objProxy);
delete objProxy.name;
console.log(objProxy.name);
// get侦听
// obj
// set侦听
// get侦听
// objProxy
// has侦听
// true
// deleteProperty侦听
// get侦听
// undefined
在使用getter、setter的时候有一个receiver的参数,如果我们的源对象(obj)有setter、getter的访问器属性,那么可以通过receiver来改变里面的this;
const obj2 = {
name: 'obj',
age:18,
};
const objProxy = new Proxy(obj2,{
get: function(target,key,value,receiver){
console.log("get侦听");
return Reflect.get(target,key,value,receiver);
},
set: function(target,key,value,receiver){
console.log("set侦听");
return Reflect.set(target,key,value,receiver);
},
has: function(target,key){
console.log("has侦听");
return Reflect.has(target,key);
},
deleteProperty: function(target,key){
console.log("deleteProperty侦听");
return Reflect.deleteProperty(target,key);
}
});
console.log(objProxy.name);
objProxy.name = 'objProxy';
console.log(objProxy.name);
console.log('name' in objProxy);
delete objProxy.name;
console.log(objProxy.name);
// get侦听
// obj
// set侦听
// get侦听
// objProxy
// has侦听
// true
// deleteProperty侦听
// get侦听
// undefined
Reflect的construct用法
function Student(name,age){
this.name = name;
this.age = age;
}
function Animal(){
}
const stu = Reflect.construct(Student,['stu',18],Animal);
console.log(stu instanceof Student); //false
console.log(stu instanceof Animal); //true
console.log(stu.__proto__ === Student.prototype); //false
console.log(stu.__proto__ === Animal.prototype); //true
console.log(stu); //Animal { name: 'stu', age: 18 }
3.总结
代理是 ECMAScript 6 新增的令人兴奋和动态十足的新特性。尽管不支持向后兼容,但它开辟出了 一片前所未有的 JavaScript 元编程及抽象的新天地。 从宏观上看,代理是真实 JavaScript 对象的透明抽象层。代理可以定义包含捕获器的处理程序对象, 而这些捕获器可以拦截绝大部分 JavaScript 的基本操作和方法。在这个捕获器处理程序中,可以修改任 何基本操作的行为,当然前提是遵从捕获器不变式。 与代理如影随形的反射 API,则封装了一整套与捕获器拦截的操作相对应的方法。可以把反射 API 看作一套基本操作,这些操作是绝大部分 JavaScript 对象 API 的基础。 代理的应用场景是不可限量的。开发者使用它可以创建出各种编码模式,比如(但远远不限于)跟 踪属性访问、隐藏属性、阻止修改或删除属性、函数参数验证、构造函数参数验证、数据绑定,以及可 观察对象。
proxy的用处: 实现拦截和监视外部对对象的访问。 降低函数和类的复杂度,优雅的写出代理代码。 在复杂操作前对操作进行校验或对所需资源进行管理。
场景:
抽离校验模块。 私有属性。 访问日志。 预警和拦截。 过滤操作。 中断代理。
reflect有的方法object都有,有时候感觉这个reflect真是多余的。但是按照我的理解,es6希望数据和逻辑代码分离,那么object就是纯数据,所有的逻辑都放到reflect上。 reflect的用处:
当object的工具类来用。
proxy和reflect就是成了object的中间件。
Day48-Day49【2022年9月10日-9月11日】
休息
参考资料
- JavaScript高级程序设计(第4版)
- MDN
- 其他
结语
志同道合的小伙伴可以加我,一起交流进步,我们坚持每日精进(互相监督思考学习,如果坚持不下来我可以监督你)。我们一起努力鸭! ——>点击这里
备注
按照时间顺序倒叙排列,完结后按时间顺序正序排列方便查看知识点,工作日更新。