深浅拷贝

Object.assign

从一个或多个源对象分配到目标对象,返回目标对象

1
2
3
4
5
6
7
8
9
10
11
12
let obj1 = { a: { b: 1 }, sym: Symbol(1) };
Object.defineProperty(obj1, 'innumerable', {
value: '不可枚举属性',
enumerable: false,
});
let obj2 = {};
Object.assign(obj2, obj1);

obj1.a.b = 2;

console.log({ obj1 }); // { a: { b: 2 }, sym: Symbol(1), innumerable: "不可枚举属性" }
console.log({ obj2 }); // { a: { b: 2 }, sym: Symbol(1) }
  • 不会拷贝不可枚举属性
  • 可以拷贝 symbol 类型的属性
  • 他不会拷贝对象的继承属性

… 扩展运算符

Object.assign拥有同样的缺陷,但是拷贝基本类型的值的时候会更加方便

concat

用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组

slice

浅拷贝数组,仅仅针对数组类型。
类数组可以使用 Object.prototype.slice.call(arr),表示从 Array 中获取 slice 方法,绑定到 arr 中,并执行 slice。

测试:
array1cloneArray1第一项的 name 属性都变为’wangwu’

1
2
3
4
5
6
7
8
9
10
11
const array1 = [
{ id: 1, name: 'zhangsan' },
{ id: 2, name: 'lisi' },
];
// const cloneArray1 = [...array1]
// const cloneArray1 = Object.assign(array1)
// const cloneArray1 = array1.concat([])
const cloneArray1 = array1.slice();
cloneArray1[0].name = 'wangwu';
console.log('array1', array1);
console.log('cloneArray1', cloneArray1);

浅拷贝的实现

1
2
3
4
5
6
7
8
9
10
11
function shallowClone(obj) {
if (typeof obj !== 'object' || target === null) return;
const newObj = Array.isArray(obj) ? [] : {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
// 忽略原型上继承过来的属性
newObj[key] = obj[key];
}
}
return newObj;
}

深拷贝的实现

乞丐版:JSON.stringify

将对象序列化为 JSON 字符串,对对象里面的内容转为字符串,再用 JSON.parse 生成新对象。
注意点:

  • 如果对象”值”中有函数、undefined、symbol 这几种类型,经过序列化之后这个键值对会消失
  • 拷贝 Date 引用各类型会变成字符串
  • 拷贝 RegExp 引用类型会变成空对象
  • 无法拷贝不可枚举属性
  • 无法拷贝对象的原型链
  • 无法拷贝对象的循环应用,即对象成环(obj[key] = obj)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function Obj() {
this.func = function () {
alert(1);
};
this.obj = { a: 1 };
this.arr = [1, 2, 3];
this.und = undefined;
this.reg = /123/;
this.date = new Date(0);
this.NaN = NaN;
this.infinity = Infinity;
this.sym = Symbol(1);
this[Symbol(2)] = 'sym2';
}

let obj1 = new Obj();

Object.defineProperty(obj1, 'innumerable', {
enumerable: false,
value: 'innumerable',
});
console.log('obj1', obj1);
let obj22 = JSON.parse(JSON.stringify(obj1));
console.log('obj22', obj22);

pic.1708326973863

基础版

注意点:

  • 不能复制不可枚举和 Symbol 类型的属性
  • 对于 Date、RegExp 这样的引用类型不能正确的拷贝
  • 没解决循环引用问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function deepClone(target) {
if (typeof target === 'object' && target !== null) {
const newObj = Array.isArray(target) ? [] : {};
for (const key in target) {
if (typeof target[key] === 'object') {
newObj[key] = deepClone(target[key]);
} else {
newObj[key] = target[key];
}
}
return newObj;
}
}

const obj3 = deepClone(obj1);
console.log('obj3', obj3);

pic.1708326963610

改进版

  • 对于不可枚举和 Symbol 类型,可以 Reflect.ownKeys(),Reflect.ownKeys === Object.getOwnPropertyNames(obj).concat(Object.getOwnPropertySymbols(Obj))
  • 对于 Date 和 RegExp 类型,直接返回一个实例对象
  • Object.getOwnPropertyDescriptors 可以获取所有自身属性的描述符,以及对应的特性,顺便结合 Object 的 create 方法创建一个新对象,并集成原对象的原型链
  • 利用 WeakMap 类型作为 hash 表,因为 WeakMap 是弱引用类型,可以有效防止内存泄漏,如果存在循环引用,就直接返回 WeakMap 存储的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const isComplexDataType = (obj) =>
(typeof obj === 'object' || typeof obj === 'function') && obj !== null;

function deepCloneImprove(obj, hash = new WeakMap()) {
if (obj.constructor === Date) return new Date(obj); // 日期对象直接返回一个新的日期对象
if (obj.constructor === RegExp) return new RegExp(obj); //正则对象直接返回一个新的正则对象
//如果循环引用了就用 weakMap 来解决
if (hash.has(obj)) return hash.get(obj);
let allDesc = Object.getOwnPropertyDescriptors(obj);
//遍历传入参数所有键的特性
let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc);
//继承原型链
hash.set(obj, cloneObj);

for (let key of Reflect.ownKeys(obj)) {
cloneObj[key] =
isComplexDataType(obj[key]) && typeof obj[key] !== 'function'
? deepCloneImprove(obj[key], hash)
: obj[key];
}
return cloneObj;
}

验证代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
let obj4 = {
num: 0,
str: '',
boolean: true,
unf: undefined,
nul: null,
obj: { name: '我是一个对象', id: 1 },
arr: [0, 1, 2],
func: function () {
console.log('我是一个函数');
},
date: new Date(0),
reg: new RegExp('/我是一个正则/ig'),
[Symbol('1')]: 1,
};
Object.defineProperty(obj4, 'innumerable', {
enumerable: false,
value: '不可枚举属性',
});
obj4 = Object.create(obj4, Object.getOwnPropertyDescriptors(obj4));
obj4.loop = obj4; // 设置loop成循环引用的属性
let cloneObj = deepCloneImprove(obj4);
cloneObj.arr.push(4);
console.log('obj4', obj4);
console.log('cloneObj', cloneObj);

pic.1708326948006

闭包

本质: 局部数据共享

没有任何一个技术方案可以做到局部数据共享。那么如何局部共享呢?
可以创建一个函数作用域,在这个作用域内共享数据:

1
2
3
4
5
6
7
8
9
10
11
function p () {
var a = 10

funtion foo () {
console.log(a)
}

funtion bar () {
console.log(a)
}
}

p 函数的局部变量 a,被 foo 和 bar 这两个函数共享

定义: 闭包是一个特殊的对象。它由两部分组成,执行上下文 A 以及在 A 中创建的函数 B。当 B 执行时,如果访问了 A 中的变量,那么闭包就产生了。

在 chrome 中,执行上下文 A 的函数名代指闭包

词法作用域:在我们编写代码时,语法规范就已经确定了词法作用域的作用范围。

词法作用域是为了明确告诉我们,当前的上下文环境中,能够访问哪些变量参与程序的运行。除了自身上下文,还可以从函数体的[[Scopes]]属性访问其他作用域的声明。

闭包形成的原因:

闭包是基于词法作用域的规则产生,让函数内部可以访问函数外部的声明。闭包在代码解析时就能确定。

闭包对象什么时候被回收

闭包对象[Closure(A)]的引用存在于函数体 B 的内存中。如果 B 函数体被回收,闭包对象同样也被回收。

看个例子:

1
2
3
4
5
6
7
8
var fn = function () {
var a = 1;
(function f() {
console.log(++a); // 这时候闭包产生了,
})();
console.log(a); //运行到这里是,f的执行上下文被销毁,闭包也消失了。此时fn中的a变成了2
};
fn(); // 等fn执行完,fn的执行上下文也被销毁

pic.1708325490489
内部函数f执行完毕,其执行上下文被回收,闭包对象也随之被回收。
pic.1708325496558

保存闭包对象不被回收

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo() {
var a = 20;

function bar() {
a = a + 1;
var b = 10;
console.log(a + b);
}

return bar;
}

foo()(); // 31
foo()(); // 31

foo 调用完毕,其执行上下文被回收,bar 作为 foo 的一部分,自然也被回收,那么保存在 foo.[[Scipes]] 上的闭包对象自然也被回收。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function foo() {
var a = 20;

function bar() {
a = a + 1;
var b = 10;
console.log(a + b);
}

return bar;
}

// foo()()
// foo()()

var bar = foo();
bar(); // 31
bar(); // 32

那么如何保存这个闭包对象不被回收呢?我们知道垃圾回收机制会通过根搜索算法回收非活跃的对象,那只需要在当前执行上下文中,保存内部函数的引用,闭包对象就不会被回收。

当 foo 执行完毕时,foo 依然会被回收,但是由于执行了var bar = foo(),内部函数 bar 有了新的方式保存引用,所以即使 foo 执行完毕,bar 也不会被回收,而是在内存中持续存在,那么闭包对象 Closure[foo]也就不会被回收。

闭包的应用

常用面试题

表现为 setTimeout 第二个参数为 1-5,定时器在循环结束后隔秒输出 6

1
2
3
4
5
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}

用自执行函数改造-隔秒一次输出 1-5
自执行函数与 timer 形成闭包,所以 i 被保存了下来

1
2
3
4
5
6
7
for (var i = 1; i <= 5; i++) {
(function (i) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
})(i);
}

pic.1708325509698
可以看到,循环执行完毕后,还能访问到 i,此时 i 的值为保存在闭包对象的值。

进一步改造,缩小约束范围,只约束 timer,瞬间输出 1-5

1
2
3
4
5
6
7
8
for (var i = 1; i <= 5; i++) {
setTimeout(
(function timer(i) {
console.log(i);
})(i),
i * 1000,
);
}

单例模式与闭包

单例模式,就是只有一个实例的对象
对象字面量的方法,就是一个单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
var per = {
name: 'Jake',
age: 20,
getName: function () {
return this.name;
},
getAge: function () {
return this.age;
},
};

per.name = 'Tom';
console.log(per.getName());

这样的单例存在一个问题,属性可以被外部修改。
我们希望拥有自己的私有方法/属性,所以可以利用自执行函数的作用域进行隔离。

1
2
3
4
5
6
7
8
9
10
11
12
13
var per = (function () {
var name = 'Jake';
var age = 20;
return {
getName: function () {
return name;
},

getAge: function () {
return age;
},
};
})();

这样内部修改就不会被修改,我们可以控制对外提供的属性和方法
但是还有一个小问题,这种方式一开始就被初始化了。

改造一下,仅在调用时初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var per = (function () {
var instance = null; // 保存实例
var name = 'Jake';
var age = 20;

function initial() {
return {
getName: function () {
return name;
},
getAge: function () {
return age;
},
};
}

return {
getInstance: function () {
if (!instance) {
instance = initial();
}
return instance;
},
};
})();

var per1 = per.getInstance();
var per2 = per.getInstance();

console.log(per1 === per2); // true

模块化与闭包

为了减少全局变量的使用,可用模块化的思维解决需要使用全局变量的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// 使用单例模式,创建一个用于管理全局状态的模块
var module_status = (function module_status() {
var status = {
number: 0,
color: null,
};

var get = function (prop) {
return status[prop]; // 访问闭包对象 Closure(module_status) 的属性
};

var set = function (prop, value) {
status[prop] = value; // 设置闭包对象 Closure(module_status) 的属性
};

return {
get: get,
set: set,
};
})();

// 负责改变颜色的模块
var module_color = (function module_color() {
// 假装用这种方式引入模块 类似于 import state from 'module_status';
var state = module_status;
var colors = ['orange', '#ccc', 'pink'];

function render() {
var color = colors[state.get('number') % 3]; // 访问闭包对象 Closure(module_status) 的 number 属性
document.body.style.backgroundColor = color;
}

return {
render: render,
};
})();

// 负责显示当前number的模块
var module_context = (function module_context() {
var state = module_status;

function render() {
console.log('this Number is '); // 小提示(与主逻辑无关): render[[scopes]]]作用域信息在解析这个函数时就已经确定了。执行到这里时 Closure(module_context) 就已经产生了
document.body.innerHTML = 'this Number is ' + state.get('number'); // 执行闭包对象 Closure(module_status) 的 get 方法,
}

return {
render: render,
};
})();

// 主模块
var module_main = (function module_main() {
var state = module_status; // 保存 module_status 模块返回的字面量对象的引用
var color = module_color;
var context = module_context;

var newNumber = state.get('number') + 1; // 访问闭包对象 Closure(module_status) 的 number 属性
// state.get() 执行完毕,正常情况下闭包对象 Closure(module_status) 随内部函数的执行完毕而被回收。
// 但是这里将闭包对象的引用地址保存在当前执行上下文中,所以闭包对象 Closure(module_status) 并没有被销毁
state.set('number', newNumber); // 改变闭包对象 Closure(module_status) 的number属性,number == 1

context.render(); // 43行访问的闭包对象于77行改变的闭包对象是同一个对象
color.render();
})();

// 注意,module_status 的引用一直存在全局对象中没被释放,所以执行 module_status 时,还能访问到闭包对象Closure(module_status),以为输出1
console.log('-------', module_status.get('number'));

// 主模块
// var module_main = (function () {
// var state = module_status;
// var color = module_color;
// var context = module_context;

// setInterval(function () { // 这里使用定时器实现切换固定颜色的功能
// var newNumber = state.get('number') + 1;
// state.set('number', newNumber);

// color.render();
// context.render();

// console.log(1);
// }, 1000);
// })();

其他应用场景

使用其他模块暴露的接口

utils/index.js

1
export const a = 1;
1
2
3
4
5
6
7
import { a } from '../../../utils';

const Closure = () => {
console.log(a);
return <div>{a}</div>;
};
export default Closure;

在函数组件 Closure 中访问了 utils 模块中的变量 a,那么闭包对象就产生了。
pic.1708325527266

父子组件通信

pic.1708325535782
<Child />组件中执行foo时,闭包对象Closure(MyClosure)产生了。

hooks 缓存上次结果

本质是将上次的结果(闭包对象)缓存在函数体内
pic.1708325542236
另外还有以下关于闭包的使用

  • 高阶函数: 记忆函数
  • 状态管理:redux、mbox
  • 函数封装
  • BEM
  • vue scoped
  • css in js:借 js 的作用域,生成 css 作用域
  • css modules

闭包的优缺点

优点:

  1. 可以重复使用闭包对象内的变量,并且不会造成变量污染
  2. 可以用来定义私有属性和私有方法(单例模式与闭包)

缺点:

  1. 可能会导致堆内存消耗过大

闭包对象的回收

  • 如果是全局变量保存了内部函数的引用,如果没有手动释放这个对象,闭包对象会一直存在堆内存中
  • 如果是函数内的一个局部变量保存了内部函数的引用,函数出栈后如果没有引用了,闭包对象等待下一次垃圾回收器回收

防止验证码轰炸_谷歌人机校验_recaptchaV3

在注册页,往往需要通过短信或者邮箱进行验证再注册。
如果短信平台接口被恶意调用,短信通道金额会被严重消耗,严重影响平台的利益。

recaptcha V3 是谷歌开发的一款可以验证人机交互是否合法的插件。它是一个 JS API,我们可以自定义一个 0-1 分数,来决定本次交互的合法性。假设我们自定义分数为0.9,当谷歌给本次交互打的分数低于0.9时,则表示本次交互不合法,从而达到拦截目的。

用法

  1. 创建秘钥。可参考 https://blog.csdn.net/weixin_59127121/article/details/127092549?spm=1001.2101.3001.6650.5

公钥Site Key 和 私钥Secret Key

1
2
Site Key=XXXXXXXXXXXXXXXXXX
Secret Key=AAAAAAAAAAAAAAAAAA
  1. 引入谷歌提供的 script,将Site Key放上去
1
<script src="https://www.recaptcha.net/recaptcha/api.js?render=XXXXXXXXXXXXXXXXXX"></script>

:::info
注意:国内使用,需要将 www.google.com 替换成 www.recaptcha.net
:::

  1. 在需要人机验证的操作上调用grecaptcha.execut,填入Site Key,生成谷歌返回的token
1
2
3
const token = await window.grecaptcha.execute('XXXXXXXXXXXXXXXXXX', {
action: 'login',
});
  1. 通过后端接口验证token是否有效,如果成功再进行提交/登录等操作
1
2
3
4
5
6
7
console.log({ token });
const formData = new FormData();
formData.append('token', token);
const { data, code, msg } = await robotVerApi.post({
body: formData,
});
// 校验成功,进行注册等其他操作

遇到的问题

在未加入人机验证之前,我们遇到以下问题

  1. 短信验证平台被恶意轰炸
  2. 注册数量暴增(注册邀请人成功后有返利的原因)

解决办法

控制短信验证平台的频繁请求。
控制了短信验证之后,注册接口也就不会频繁请求了。

一开始的解决办法:

  1. 前端发起人机校验
  2. 后端进行 token 认证,返回状态
  3. 校验成功,前端请求短信平台

以上能防止一些恶意的页面频繁操作,但是通过数据发现短信平台接口还在暴增,也就是说攻击者绕过前面两个步骤,通过脚本在轰炸短信平台。

所以我们改把前端发送短信的操作放在后端,避免脚本攻击,步骤如下:

  1. 前端发起后端自定义的短信请求接口
  2. 后端人机校验,token 认证成功
  3. 后端请求短信平台
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 获取token
const token = await window.grecaptcha.execute('XXXXXXXXXXXXXXXXXX', {
action: 'login',
});

// 将token和邮箱传给后端,后端认证token并请求短信平台
const { data, msg, code } = await backendCustomApi.post({
body: { email, token },
});

// 将短信平台的状态返回
if (code === '000') {
console.log('邮箱已发送');
}

参考:

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×