壹 ❀ 引

深拷贝与浅拷贝的区别,实现深拷贝的几种方法一文中,我们阐述了深浅拷贝的概念与区别,普及了部分具有迷惑性的浅拷贝api。当然,我们也实现了乞丐版的深拷贝方法,能解决部分拷贝场景,虽然它仍有很多缺陷。那么这一篇文章我们将从零手写一个强大的深拷贝方法,在方法逐渐升级的过程中,我们也能亲身感受深拷贝中需要考虑的边界问题,那么本文开始。

贰 ❀ 从零手写深拷贝

贰 ❀ 壹 从基础对象复制开始

在上文中,我们已经得知深拷贝主要还是考虑引用数据类型,假定现在有一个deepClone方法,假设我们传递的拷贝参数是基础类型,我们完全可以原封不动返回即可。而引用数据类型类型繁多,可以猜到这个方法要做的事应该集中在类型判断,以及针对不同对象类型如何去复制。

先不说深拷贝,我们由浅至深,如何复制一个{}呢?你可能毫不犹豫能写出如下代码:

const deepClone = (obj) => {
// 创建一个新对象
const obj_ = {};
for (let i in obj) {
// 按key依次拷贝
obj_[i] = obj[i];
};
return obj_;
};

我们可以使用for...in遍历对象的所有key,从而达到复制所有属性的目的。但这个实现只能满足复制value是基本数据类型的例子:

const obj = {
name: '听风',
age: 29
}

一旦属性的value有引用数据类型,上述方法就只能达到浅拷贝的作用了,比如:

const obj = {
name: '听风',
age: 29,
other: {
gender: 'male'
}
};
const o = deepClone(obj);
// 修改原对象引用数据类型的值
obj.other.gender = null;
// 可以看到简单的复制没办法解决深拷贝的问题
console.log(obj, o)

贰 ❀ 贰 增加对象判断与递归深度复制

前文我们已经说过了,假设拷贝传递的是基本类型的值,我们只需原封不动返回即可;其次,考虑到对象的某个值可能还是对象,比如上面的other,我们继续遍历{gender: 'male'}复制,这样依次把gender属性拿过来,就不可能有引用的问题了,因此这里我们将方法升级:

const deepClone = (obj) => {
// 是对象吗?是就新建对象开始复制
if (typeof obj === 'object') {
const obj_ = {};
for (let i in obj) {
// 不管是不是对象,直接递归,外面的typeof会帮我们做判断是否要继续遍历
obj_[i] = deepClone(obj[i]);
};
return obj_;
// 不是对象?直接返回
} else {
return obj;
};
};

有同学第一眼看上去可能就比较奇怪,为什么obj_[i] = deepClone(obj[i]);这一句不继续加一个是否是对象的判断?不是对象没必要递归啊。其实递归的诀窍就是,想好当前要做什么以及什么时候跳出递归,之后递归会重复帮你做好你预期的操作。

比如当遇到name: '听风'这个属性后,再次调用deepClone后因为typeof的判断不是对象,会直接原封不动返回,所以并不会有什么大的性能浪费。

贰 ❀ 叁 兼容数组类型

除了{}类型,数组也是我们打交道非常多的引用类型,很明显上述代码并不满足数组的拷贝,我们需要对obj的类型进一步细化,如下:

const deepClone = (obj) => {
// 是对象吗?是就新建对象开始复制
if (typeof obj === 'object') {
// 是对象,我们进一步确定是数组还是{}
const obj_ = Array.isArray(obj) ? [] : {};
for (let i in obj) {
// 不管是不是对象,直接递归,外面的typeof会帮我们做判断是否要继续遍历
obj_[i] = deepClone(obj[i]);
};
return obj_;
// 不是对象?直接返回
} else {
return obj;
};
};

现在我们再来测试对象值是对象以及数组的情况,可以看到此时已经满足了预期。

const obj = {
name: '听风',
age: 29,
other: {
gender: 'male',
arr: [1, 2, 3]
}
};
const o = deepClone(obj);
obj.other.gender = null;
obj.other.arr[0] = 1;
console.log(obj, o)

贰 ❀ 肆 解决typeof类型判断误差

上述的实现我们都依赖了typeof来判断参数是不是一个对象,如果不是对象哪来的for...in呢?但typeof一直有一个javascript遗留bug,那就是typeof null的类型是object,所以如果参数传递null,我们的深拷贝方法会返回一个{}而不是null,这里得优化下。

这里我查阅了typeof MDN,具体类型如下:

类型 结果
Undefined "undefined"
Null "Object"
Boolean "boolean"
Number "Number"
String "String"
Symbol "Symbol"
Bigint "bigint"
Function "Function"
其它任何对象 "object"

我们总结下,只要参数不是null,且类型结果不是objectfunction,那说明这个参数一定不是对象类型,我们来定义一个更精准的对象类型判断方法isObject,同时优化之前的代码:

const isObject = (obj) => {
const type = typeof obj;
return obj !== null && (type === 'object' || type === 'function');
}; const deepClone = (obj) => {
// 如果不是对象直接返回
if (!isObject(obj)) {
return obj;
};
// 是对象,我们进一步确定是数组还是{}
const obj_ = Array.isArray(obj) ? [] : {};
for (let i in obj) {
// 不管是不是对象,直接递归,外面的typeof会帮我们做判断是否要继续遍历
obj_[i] = deepClone(obj[i]);
};
return obj_;
};

贰 ❀ 伍 解决对象循环引用

虽然不常见,但对象其实可以将自己设置成自己的属性,比如:

const obj = {
name: '听风',
age: 29,
other: {
gender: 'male',
arr: [1, 2, 3]
}
};
obj.obj = obj;

我们现在来使用自己定义的深拷贝方法拷贝上述对象,你会发现直接爆栈:

为什么?我们来打印一下obj的结构就清楚了:

key遇到obj时,因为是对象类型,继续递归,结果发现这个key可以无限递归下去,直接爆栈。

那怎么解决呢?我们可以想想被拷贝的原对象是怎么诞生的,将对象的key指向自己即可。也就是在拷贝时,我们只要保证执行一次obj['obj'] = obj即可,只要让自己指向自己,这个循环引用自然就会诞生,并不需要我们无限递归来模拟这个循环引用。

怎么跳出这个递归呢?设想下,obj在第一次传入后,开始第一次递归,然后把自己又作为参数传了下去,后续做的事情完全是相同的,那我们是不是可以记录我们要拷贝的obj以及它拷贝的后的结果,当下次遇到相同的obj跳出递归,直接返回之前的结果就好了。

考虑到我们要记录的参数可能是对象类型,使用普通的对象肯定不行,而es6新增的Map数据类型key就可以使用对象:

const deepClone = (obj, map = new Map()) => {
// 如果不是对象直接返回
if (!isObject(obj)) {
return obj;
};
// 之前有拷贝过吗?
if (map.has(obj)) {
return map.get(obj);
};
// 是对象,我们进一步确定是数组还是{}
const obj_ = Array.isArray(obj) ? [] : {};
// 存储当前拷贝的对象,以及我们要返回的对象
map.set(obj, obj_);
for (let i in obj) {
// 不管是不是对象,直接递归,外面的typeof会帮我们做判断是否要继续遍历
obj_[i] = deepClone(obj[i], map);
};
return obj_;
};

此时再拷贝上述的循环引用的对象,你会发现爆栈的问题已经得到解决,我们成功拷贝了一个循环引用的问题。

贰 ❀ 陆 兼容其它可遍历引用数据类型

虽然上文我们做了isObject的判断,但事实上我们也只做了{}[]两种数据的拷贝,像正则,日期以及new Number(1)这种对象其实都未兼顾。

说直白点就是,isObject只是告诉了我们这个参数是不是对象,是对象后我们得进一步细化,看看它到底是什么类型,毕竟有些对象根本不可遍历,那我们现在的代码就无法拷贝这类对象。

我们可以通过如下代码来精准获取当前参数的对象类型:

const getObjectType = (obj) => {
return Object.prototype.toString.call(obj);
}

举个例子,比如当传入一个函数或者一个正则就能精准得到它的对象类型:

Object.prototype.toString.call(function (){});// '[object Function]'
Object.prototype.toString.call(/1/);// '[object RegExp]'

我们可以列举常见的部分对象类型,将其定义成常量便于后续使用:

// 可遍历类型
const arrType = '[object Array]';
const objType = '[object Object]';
const mapType = '[object Map]';
const setType = '[object Set]';
const argType = '[object Arguments]'; // 不可遍历
const boolType = '[object Boolean]';
const numType = '[object Number]';
const strType = '[object String]';
const dateType = '[object Date]';
const errType = '[object Error]';
const regexpType = '[object Regexp]';
const symbolType = '[object Symbol]';
const funType = '[object Function]'; // 将可遍历类型做个集合
const traverseTypes = [arrType, objType, mapType, setType, argType];

其实初略做个分类,大家虽然都是对象,但并不是所有对象都可以遍历,比如日期,数字对象,这种我们都不太好直接遍历。

而数组,普通对象{}arguments以及新增的Map Set虽然都可以遍历,但像Map添加属性通过add方法,并不是传统的key-value赋值形式,所以并不能通过for...in一招通吃。

有同学这里可能就已经有疑问了,不是数字,字符串直接返回吗?怎么对象还考虑这些呢?这是因为我们创建数字习惯使用对象字面量的创建方式,比如:

const s = '听风';

但我们依然可以通过构造器String来创建一个字符串对象:

const s = new String('听风');

OK,让我们回到上文已实现的深拷贝,目前我们根据isArray来判断是否是一个数组,从而初始化obj_是一个[]或者{},很显然这种做法没办法满足需求,当时一个对象时,我们希望直接创建一个同类型的空对象,然后再往这个空对象上复制属性。

怎么做呢?其实这里我们可以借用传递参数的constructor属性,访问到该参数的原始构造函数,举个例子:

const num = 1;
const arr = [];
const bool = true; num.__proto__.constructor === Number;// true
arr.__proto__.constructor === Array;// true
bool.__proto__.constructor === Boolean;// true

因此只要当前可以认定这是一个值得深拷贝的对象,我们直接通过.constructor访问到构造器,然后执行new操作即可。

若对于这一步有疑惑,说明你对于javascript中的原型掌握不是很扎实,这里可以阅读博主JS 疫情宅在家,学习不能停,七千字长文助你彻底弄懂原型与原型链一文,这里就不多赘述了。

让我们改写深拷贝方法,让它能根据任意对象类型创建对应空对象:

const deepClone = (obj, map = new Map()) => {
// 如果不是对象直接返回
if (!isObject(obj)) {
return obj;
}; // 获取当前参数的对象类型
const objType = getObjectType(obj);
// 根据constructor找到原始构造器,创建初始化对象
let obj_ = new obj.constructor(); // 解决循环引用问题
if (map.has(obj)) {
return map.get(obj);
};
// 存储当前拷贝的对象,以及我们要返回的对象
map.set(obj, obj_); // 拷贝Set
if (objType === setType) {
obj.forEach((val, key) => {
obj_.add(deepClone(val, map));
});
return obj_;
}; // 拷贝Map
if (objType === mapType) {
obj.forEach((val, key) => {
obj_.set(key, deepClone(val, map));
});
return obj_;
}; // 如果是数组或者{}
for (let i in obj) {
// 不管是不是对象,直接递归,外面的typeof会帮我们做判断是否要继续遍历
obj_[i] = deepClone(obj[i], map);
};
return obj_;
}; const obj = {
name: '听风',
arr: [1, 2, 3],
set: new Set([1, 2, 3]),
map: new Map([
['age', 29]
])
}; const o = deepClone(obj);
obj.name = '1';
obj.set.add(4);
obj.map.set('sex', 'male');
console.log(obj, o);

上述代码我们兼容了五种可遍历的对象类型,Set Map需要特有的复制的方式,除此之外的对象,数组以及arguments均可通过for...in复制,运行了例子发现非常顺利。

题外话,我在查阅深拷贝资料时,发现有不少评论说使用Object.create(obj.constructor.prototype)来取代new obj.constructor()的做法,因为前者是直接使用你要复制对象的原型来创建空对象,这要比后者再次调用构造器性能要好,这个说法是有问题的,我们来看个例子:

const arr1 = Object.create([].constructor.prototype);
const arr2 = new [].constructor(); arr1[0] = 1;;
arr2[0] = 1;
console.log(arr1.length, arr2.length); // 0 1

可以看到,通过以数组原型创建的空数组,它自身居然没有带length属性,假设我们以此拷贝出了一个数组,你会发现虽然它有元素,但因为缺少自己的length从而无法成功遍历。

贰 ❀ 柒 兼容不可遍历类型

OK,让我们继续分析剩余不可遍历或者说不便于遍历的对象类型,像布尔值,数字,字符串以及日期这类对象,我们要拷贝比较简单,我们可以通过valueOf访问到对象的原始值,举个例子:

const num = new Number(1);
console.log(num.vauleOf());// 1

但需要注意的是Symbol这个类型它不能使用new调用,且Symbole本身就是基础数据类型,一般情况下我们让它跟普通的数字一样,传入原封不动返回,但我们需要额外考虑包装对象形式的Symbol,比如:

const s = Object(Symbol(1));
Object.prototype.toString.call(s);// '[object Symbol]'

这种形式的对象,我们可以也利用Object(obj.valueOf())进行返回即可。

现在,我们将这些不方便遍历的类型单独做个抽离,能遍历的对象还是用使用上述实现,具体实现如下:

// 拷贝不便于遍历的对象类型
const cloneOtherType = (obj, type) => {
switch (type) {
case boolType:
case numType:
case strType:
case dateType:
return new obj.constructor(obj.valueOf());
case symbolType:
return Object(obj.valueOf());
case regexpType:
// 待实现
case funType:
// 待实现
}
}; const deepClone = (obj, map = new Map()) => {
// 如果不是对象直接返回
if (!isObject(obj)) {
return obj;
}; // 获取当前参数的对象类型
const objType = getObjectType(obj); // 根据constructor找到原始构造器,创建初始化对象
let obj_;
if (traverseTypes.includes(objType)) {
// 如果是可遍历类型,直接创建空对象
obj_ = new obj.constructor();
} else {
// 若不是,则走额外的处理
return cloneOtherType(obj, objType);
}
// 相同代码省略.....
};

现在我们只差正则和函数的实现了,先来说说正则。

贰 ❀ 捌 实现正则深拷贝

声明一个正则一般有两种写法,常见的正则字面量直接创建,或者使用new结合正则构造器来创建,如下:

cosnt reg1 = /123/g;
const reg2 = new Regexp(/123/,'g');

当我们接受一个正则时,肯定也是希望通过new然后传入参数来得到一个新的正则对象,这里我们就得提取两部分,一部分是正则的匹配文本(123),一部分是正则的修饰符(g)。至于前者,我们能通过source属性访问,后者则可以通过flags访问,举个例子:

const reg = /1/ig;
const {source, flags} = reg;
console.log(source, flags);// 1 gi

所以我们很容易写出如下的代码:

return new Regexp(obj.source, obj.flags);

但需要注意的是,正则表达式有个lastIndex属性,用于指定下次匹配从什么时候开始,举个例子:

const reg = /echo/g;
const str = 'echo echo echo';
let res;
// exec匹配到会返回一个数组,匹配不到返回null
while (res = reg.exec(str) !== null) {
console.log(reg.lastIndex); // 4 9 14
}

第一匹配到echo后,下一次匹配的位置很明显是第一个空格处,所以索引肯定是4,第二次匹配成功后lastIndex就变成9了,以此类推。而lastIndex这个东西是可以手动设置的,我们改改上面的例子:

const reg = /echo/g;
const str = 'echo echo echo';
reg.lastIndex = 9;
let res;
// exec匹配到会返回一个数组,匹配不到返回null
while (res = reg.exec(str) !== null) {
console.log(reg.lastIndex); // 14
}

所以你看看,单纯拿sourceflags还不够,我们还得把传递的正则的lastIndex也抄过来,不然匹配的行为可能跟原正则不一致,因此完整的正则拷贝应该是:

// 克隆正则
const cloneRegexp = (obj) => {
const {
resource,
flags,
lastIndex
} = obj;
const obj_ = new Regexp(resource, flags);
obj_.lastIndex = lastIndex;
return obj_;
}

贰 ❀ 玖 实现函数克隆

实话实说,一般针对函数的拷贝,我们都是原封不动的返回,即便博主工作了5年,说实话也没遇到要拷贝函数的场景。这里我也查阅了网上一些拷贝函数的思路,简单说说。

第一种思路与正则一样,把函数转为字符串,然后正则匹配函数的参数,函数体,最后通过new Function()的形式创建一个新函数,但考虑到函数柯里化,闭包,等等复杂的场景,以及函数还存在自调用函数,箭头函数,匿名函数等复杂因素,目前我能找到的此类实现其实都有问题,所以这里我就不做代码补充了。

第二种,借用eval,看个例子:

const foo = x => console.log(x + 1);
const fn = eval(foo.toString())
fn(1);// 2

通过toString将函数转为字符串后,借用eval执行再次得到函数,看似可以,但只要函数是函数声明,这种做法就完全行不通了:

function foo(x) {
console.log(x + 1);
};
console.log(foo.toString())
const fn = eval(foo.toString());
fn(1);// 报错,fn是undefined

第三种,借用bind返回一个boundFn,也算是投机取巧的一种做法,并不符合我们心中的函数深拷贝,所以综合来说,不如不拷贝,毕竟本身就没这个需求在,如果面试真的问到,可以阐述以上三种做法,其中复杂性我想面试官自己也能体会。

叁 ❀ 总

那么总结上述所有改写,我们直接贴上完整版代码:

// 可遍历类型
const arrType = '[object Array]';
const objType = '[object Object]';
const mapType = '[object Map]';
const setType = '[object Set]';
const argType = '[object Arguments]'; // 不可遍历
const boolType = '[object Boolean]';
const numType = '[object Number]';
const strType = '[object String]';
const dateType = '[object Date]';
const errType = '[object Error]';
const regexpType = '[object Regexp]';
const symbolType = '[object Symbol]';
const funType = '[object Function]'; // 将可遍历类型做个集合
const traverseTypes = [arrType, objType, mapType, setType, argType]; const isObject = (obj) => {
const type = typeof obj;
return obj !== null && (type === 'object' || type === 'function');
}; const getObjectType = (obj) => {
return Object.prototype.toString.call(obj);
}; // 克隆正则
const cloneRegexp = (obj) => {
const {
resource,
flags,
lastIndex
} = obj;
const obj_ = new Regexp(resource, flags);
obj_.lastIndex = lastIndex;
return obj_;
} // 拷贝不便于遍历的对象类型
const cloneOtherType = (obj, type) => {
switch (type) {
case boolType:
case numType:
case strType:
case dateType:
return new obj.constructor(obj.valueOf());
case symbolType:
return Object(obj.valueOf());
case regexpType:
return cloneRegexp(obj);
case funType:
return obj;
}
} const deepClone = (obj, map = new Map()) => {
// 如果不是对象直接返回
if (!isObject(obj)) {
return obj;
}; // 获取当前参数的对象类型
const objType = getObjectType(obj); // 根据constructor找到原始构造器,创建初始化对象
let obj_;
if (traverseTypes.includes(objType)) {
// 如果是可遍历类型,直接创建空对象
obj_ = new obj.constructor();
} else {
// 若不是,则走额外的处理
return cloneOtherType(obj, objType);
} // 解决循环引用问题
if (map.has(obj)) {
return map.get(obj);
};
// 存储当前拷贝的对象,以及我们要返回的对象
map.set(obj, obj_); // 拷贝Set
if (objType === setType) {
obj.forEach((val, key) => {
obj_.add(deepClone(val, map));
});
return obj_;
}; // 拷贝Map
if (objType === mapType) {
obj.forEach((val, key) => {
obj_.set(key, deepClone(val, map));
});
return obj_;
}; // 如果是数组或者{}
for (let i in obj) {
// 不管是不是对象,直接递归,外面的typeof会帮我们做判断是否要继续遍历
obj_[i] = deepClone(obj[i], map);
};
return obj_;
};

如果你是跟着本文思路一步步走下来,上述代码理论上来说不存在难以理解的点,简单测试下例子,也暂未发现有什么问题,不过再怎么说,上述实现也不能用于生产环境,毕竟当下就有更专业的三方库来帮我解决深拷贝的问题。

你也许会想,面试真的会让你手写一个深拷贝吗?我没遇到过手写,但确实遇到过讲解实现思路以及有哪些边界情况需要考虑的问题,若真手写,能将上述思路实现到七七八八,就已经非常优秀了,看似在实现深拷贝,其实这段实现中,我想大家也发现考核了javascript中很多基础知识,大的有原型链与继承,es6Map Set,小的知识点甚至考虑到了正则的lastIndex;所以站在我的角度,这篇文章也能作为js综合复习的一个入口。

写到最后,若文章存在错误以及有所疑问,也欢迎留言讨论,那么到这里本文结束。

JS 从零手写一个深拷贝(进阶篇)的更多相关文章

  1. 用node.js从零开始去写一个简单的爬虫

    如果你不会Python语言,正好又是一个node.js小白,看完这篇文章之后,一定会觉得受益匪浅,感受到自己又新get到了一门技能,如何用node.js从零开始去写一个简单的爬虫,十分钟时间就能搞定, ...

  2. 使用 js,自己写一个简单的滚动条

    当我们给元素加上 overflow: auto;  的时候,就会出现滚动条,然而浏览的不同,滚动条的样式大不一样,有些甚至非常丑. 于是就想着自己写一个滚动条,大概需要弄清楚一下这几个点: 1.滚动条 ...

  3. Spring Security 实战干货:从零手写一个验证码登录

    1. 前言 前面关于Spring Security写了两篇文章,一篇是介绍UsernamePasswordAuthenticationFilter,另一篇是介绍 AuthenticationManag ...

  4. 使用Node.js原生API写一个web服务器

    Node.js是JavaScript基础上发展起来的语言,所以前端开发者应该天生就会一点.一般我们会用它来做CLI工具或者Web服务器,做Web服务器也有很多成熟的框架,比如Express和Koa.但 ...

  5. js如何手写一个new

    手写new 看一下正常使用new function Dog(name){ this.name = name } Dog.prototype.sayName = function(){ console. ...

  6. 用node.js给C#写一个数据表的实体类生成工具

    虽然微软提供了T4模板,但是我感觉非常难用.哪儿比得上直接用脚本来写模板来的爽. 因为要给一个老项目做周边的工具,需要连接到数据库. 我习惯性用EntityFrameworkCore来做,因为毕竟从出 ...

  7. 第一个Three.js程序——动手写一个简单的场景

    三维场景基本要素: 步骤: 代码: 源码: <!DOCTYPE html> <html lang="en"> <head> <meta c ...

  8. turn.js中文API 写一个翻页效果的参数详细解释

    $('.flipbook').turn({     width: 922,     height: 600,     elevation: 50,     gradients: true,     a ...

  9. js eval函数写一个简单的计算器

    <!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8" ...

  10. 原生js 基于canvas写一个简单的前端 截图工具

    先看效果 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <titl ...

随机推荐

  1. Linux telnet安装及端口测试联通性

    安装步骤: 可使用该文中的步骤进行安装,已经过本人验证,是可以安装成功的: https://blog.csdn.net/doubleqinyan/article/details/80492421 安装 ...

  2. 2023年SWPU NSS 秋季招新赛 (校外赛道) MISC复盘WP

    GIF Code 题目描述: 扫一扫即可获取Flag 给了一个含二维码的动图,分离一下得到九张二维码碎片,根据文件名数字按顺序组装,在线扫码即可 NSSCTF{3f0ac91b-3e0e-a7e2-7 ...

  3. [转帖]MySQL运维实战(2)MySQL用户和权限管理

    https://segmentfault.com/a/1190000044514403 作者:俊达 引言 MySQL数据库系统,拥有强大的控制系统功能,可以为不同用户分配特定的权限,这对于运维来说至关 ...

  4. [转帖]tidb-系统内核调优及对比

    一.背景 验证系统调优对性能的影响,用sysbench做了一些简单的测试,具体调整方法可见官方文档 二.特殊说明 1.透明大页查看 # 查看透明大页是否开启,[]在always处表示开启,[]在nev ...

  5. 人大金仓学习之四-kmonitor

    人大金仓学习之四-kmonitor 背景 kmonitor 其实时一个很好的工具和思路 开元的软件封装一下, 减轻技术复杂度,提高部署效率 并且能够更加快速的定位解决问题. 能够极大的提升客户体验. ...

  6. [转帖]一文解决内核是如何给容器中的进程分配CPU资源的?

    https://zhuanlan.zhihu.com/p/615570804   现在很多公司的服务都是跑在容器下,我来问几个容器 CPU 相关的问题,看大家对天天在用的技术是否熟悉. 容器中的核是真 ...

  7. Oracle存储过程的基本学习

    Oracle存储过程的基本学习 摘要 这个简要学习应该会分为上下两部分 第一部分是存储过程的学习. 第二部分是python的学习. 核心目标是查询Oracle数据库中的主键数据. 如果有主键upper ...

  8. Spring缓存是如何实现的?如何扩展使其支持过期删除功能?

    前言:在我们的应用中,有一些数据是通过rpc获取的远端数据,该数据不会经常变化,允许客户端在本地缓存一定时间. 该场景逻辑简单,缓存数据较小,不需要持久化,所以不希望引入其他第三方缓存工具加重应用负担 ...

  9. js文件下载blob

    使用axios文件下载 if (tableDataSource.selectedRowKeys.length > 0) { //本次请求你携带token axios.defaults.heade ...

  10. 【NSSCTF-Round#16】 Web和Crypto详细完整WP

    每天都要加油哦!    ------2024-01-18  11:16:55 [NSSRound#16 Basic]RCE但是没有完全RCE <?php error_reporting(0); ...