数组去重
1. ES6方法 (Set + Array.from )
const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];
Array.from(new Set(array)); // [1, 2, 3, 5, 9, 8]
2. 使用map存储不重复的数字
const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];
uniqueArray(array); // [1, 2, 3, 5, 9, 8]
function uniqueArray(array) {
let map = {};
let res = [];
for(var i = 0; i < array.length; i++) {
if(!map.hasOwnProperty([array[i]])) {
map[array[i]] = 1;
res.push(array[i]);
}
}
return res;
}
3.使用 filter
/**
* 使用 filter 和 indexOf 实现数组去重。
*
* @param {Array} arr - 需要去重的数组。
* @returns {Array} 去重后的数组。
*/
const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];
function uniqueWithFilter(arr) {
// 使用 filter 方法,只保留第一次出现的元素
return arr.filter((item, index) => arr.indexOf(item) === index);
}
uniqueWithFilter(array) // [1, 2, 3, 5, 9, 8]
4. 使用 reduce
/**
* 使用 reduce 实现数组去重。
*
* @param {Array} arr - 需要去重的数组。
* @returns {Array} 去重后的数组。
*/
const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];
function uniqueWithReduce(arr) {
// 使用 reduce 方法构建一个新数组
return arr.reduce((accumulator, currentValue) => {
// 如果新数组中没有当前值,则添加到新数组中
if (!accumulator.includes(currentValue)) {
accumulator.push(currentValue);
}
return accumulator;
}, []);
}
uniqueWithReduce(array) // [1, 2, 3, 5, 9, 8]
5. 使用 ES6 ... 操作符和 Set
/**
* 使用 ES6 Spread 操作符和 Set 实现数组去重。
*
* @param {Array} arr - 需要去重的数组。
* @returns {Array} 去重后的数组。
*/
const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];
function uniqueWithSpreadAndSet(arr) {
// 先将数组转换为 Set,然后使用 Spread 操作符将其转换回数组
return [...new Set(arr)];
}
uniqueWithSpreadAndSet(array) // [1, 2, 3, 5, 9, 8]
数组扁平化flatten
1. 递归实现
普通的递归思路很容易理解,就是通过循环递归的方式,一项一项地去遍历,如果每一项还是一个数组,那么就继续往下遍历,利用递归程序的方法,来实现数组的每一项的连接:
let arr = [1, [2, [3, 4, 5]]];
function flatten(arr) {
let result = [];
for(let i = 0; i < arr.length; i++) {
if(Array.isArray(arr[i])) {
result = result.concat(flatten(arr[i]));
} else {
result.push(arr[i]);
}
}
return result;
}
flatten(arr); // [1, 2, 3, 4,5]
2. reduce 函数迭代
从上面普通的递归函数中可以看出,其实就是对数组的每一项进行处理,那么其实也可以用reduce 来实现数组的拼接,从而简化第一种方法的代码,改造后的代码如下所示:
let arr = [1, [2, [3, 4]]];
function flatten(arr) {
return arr.reduce(function(prev, next){
return prev.concat(Array.isArray(next) ? flatten(next) : next)
}, [])
}
console.log(flatten(arr));// [1, 2, 3, 4,5]
3. 扩展运算符实现
这个方法的实现,采用了扩展运算符和 some 的方法,两者共同使用,达到数组扁平化的目的:
let arr = [1, [2, [3, 4]]];
function flatten(arr) {
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
}
console.log(flatten(arr)); // [1, 2, 3, 4,5]
4. split 和 toString
可以通过 split 和 toString 两个方法来共同实现数组扁平化,由于数组会默认带一个 toString 的方法,所以可以把数组直接转换成逗号分隔的字符串,然后再用 split 方法把字符串重新转换为数组,如下面的代码所示:
let arr = [1, [2, [3, 4]]];
function flatten(arr) {
return arr.toString().split(',');
}
console.log(flatten(arr)); // [1, 2, 3, 4,5]
5. ES6 中的 flat
我们还可以直接调用 ES6 中的 flat 方法来实现数组扁平化。flat 方法的语法:arr.flat([depth])
其中 depth 是 flat 的参数,depth 是可以传递数组的展开深度(默认不填、数值是 1),即展开一层数组。如果层数不确定,参数可以传进 Infinity,代表不论多少层都要展开:
let arr = [1, [2, [3, 4]]];
function flatten(arr) {
return arr.flat(Infinity);
}
console.log(flatten(arr)); // [1, 2, 3, 4,5]
6. 正则和 JSON 方法
在第4种方法中已经使用 toString 方法,其中仍然采用了将 JSON.stringify 的方法先转换为字符串,然后通过正则表达式过滤掉字符串中的数组的方括号,最后再利用 JSON.parse 把它转换成数组:
let arr = [1, [2, [3, [4, 5]]], 6];
function flatten(arr) {
let str = JSON.stringify(arr);
str = str.replace(/([|])/g, '');
str = '[' + str + ']';
return JSON.parse(str);
}
console.log(flatten(arr)); // [1, 2, 3, 4,5]
对象扁平化
/* 题目*/
var entryObj = {
a: {
b: {
c: {
dd: 'abcdd'
}
},
d: {
xx: 'adxx'
},
e: 'ae'
}
}
// 要求转换成如下对象
var outputObj = {
'a.b.c.dd': 'abcdd',
'a.d.xx': 'adxx',
'a.e': 'ae'
}
function flat(obj, path = '', res = {}, isArray) {
for (let [k, v] of Object.entries(obj)) {
if (Array.isArray(v)) {
let _k = isArray ? `${path}[${k}]` : `${path}${k}`;
flat(v, _k, res, true);
} else if (typeof v === 'object') {
let _k = isArray ? `${path}[${k}].` : `${path}${k}.`;
flat(v, _k, res, false);
} else {
let _k = isArray ? `${path}[${k}]` : `${path}${k}`;
res[_k] = v;
}
}
return res;
}
console.log(flat({ a: { aa: [{ aa1: 1 }] } }))
数字千分位分割
formatNumber(number) {
if (typeof number !== "number") {
return null;
}
if (isNaN(number)) {
return null;
}
let result = [];
let tmp = number + "";
let num = number;
let suffix = "";
if (tmp.indexOf(".") !== -1) {
suffix = tmp.substring(tmp.indexOf(".") + 1);
num = parseInt(tmp.substring(0, tmp.indexOf(".")));
}
while (num > 0) {
result.unshift(num % 1000);
num = Math.floor(num / 1000);
}
let ret = result.join(",");
if (suffix !== "") {
ret += "." + suffix;
}
return ret;
}
## 写法二 (包含小数)
let format = n => {
let num = n.toString() // 转成字符串
let decimals = ''
// 判断是否有小数
num.indexOf('.') > -1 ? decimals = num.split('.')[1] : decimals
let len = num.length
if (len <= 3) {
return num
} else {
let temp = ''
let remainder = len % 3
decimals ? temp = '.' + decimals : temp
if (remainder > 0) { // 不是3的整数倍
return num.slice(0, remainder) + ',' + num.slice(remainder, len).match(/\d{3}/g).join(',') + temp
} else { // 是3的整数倍
return num.slice(0, len).match(/\d{3}/g).join(',') + temp
}
}
}
format(12323.33) // '12,323.33'
### 不包含小数
let format = n => {
let num = n.toString()
let len = num.length
if (len <= 3) {
return num
} else {
let remainder = len % 3
if (remainder > 0) { // 不是3的整数倍
return num.slice(0, remainder) + ',' + num.slice(remainder, len).match(/\d{3}/g).join(',')
} else { // 是3的整数倍
return num.slice(0, len).match(/\d{3}/g).join(',')
}
}
}
format(1232323) // '1,232,323'
实现斐波那契数列
// 递归
function fn (n){
if(n==0) return 0
if(n==1) return 1
return fn(n-2)+fn(n-1)
}
// 优化
function fibonacci2(n) {
const arr = [1, 1, 2];
const arrLen = arr.length;
if (n <= arrLen) {
return arr[n];
}
for (let i = arrLen; i < n; i++) {
arr.push(arr[i - 1] + arr[ i - 2]);
}
return arr[arr.length - 1];
}
// 非递归
function fn(n) {
let pre1 = 1;
let pre2 = 1;
let current = 2;
if (n <= 2) {
return current;
}
for (let i = 2; i < n; i++) {
pre1 = pre2;
pre2 = current;
current = pre1 + pre2;
}
return current;
}
实现数组的 push、filter、map 方法
// push
Array.prototype.myPush = function (...args) {
const length = this.length
for (let i = 0; i < args.length; i++) {
this[this.length + i] = args[i]
}
return this.length
}
// filter
Array.prototype.myFilter = function (callback) {
const newArr = []
for (let i = 0; i < this.length; i++) {
if (callback(this[i], i, this)) {
newArr.push(this[i])
}
}
return newArr
}
// map
Array.prototype.myMap = function (callback) {
const newArr = []
for (let i = 0; i < this.length; i++) {
newArr.push(callback(this[i], i, this))
}
return newArr
}
const list = [1, 2, 3, 4, 5]
console.log(list.myPush(6)) // 6
console.log(list.myFilter((i) => i > 3)) //[ 4, 5, 6 ]
console.log(list.myMap((i) => i + 1)) // [ 2, 3, 4, 5, 6, 7 ]
实现 jsonp
// 动态的加载js文件
function addScript(src) {
const script = document.createElement('script');
script.src = src;
script.type = "text/javascript";
document.body.appendChild(script);
}
addScript("http://xxx.xxx.com/xxx.js?callback=handleRes");
// 设置一个全局的callback函数来接收回调结果
function handleRes(res) {
console.log(res);
}
// 接口返回的数据格式
handleRes({a: 1, b: 2});
获取URL中的参数
/?&/igm,前面是?或者&,任意字符直到遇到=,使用非贪婪模式,等号后面是非&符号的任意字符,然后去匹配就好了- 理论上可以用
matchAll,然后用迭代器去处理
function name(url) {
const _url = url || window.location.href;
const _urlParams = _url.match(/[?&](.+?=[^&]+)/igm);
return _urlParams ? _urlParams.reduce((a,b) => {
const value = b.slice(1).split('=');
a[value[0]] = value[1];
return a;
}, {}) : {}
}
function getParams() {
const params = {};
const search = location.search.substring(1); // 去掉问号
const pairs = search.split('&'); // 按 & 分割参数
for (let i = 0; i < pairs.length; i++) {
const pair = pairs[i].split('=');
const key = decodeURIComponent(pair[0]); // 解码参数名
const value = decodeURIComponent(pair[1] || ''); // 解码参数值(如果没有值,则默认为 "")
params[key] = value; // 存储为对象属性
}
return params;
}
## 使用 URLSearchParams
const getSearchParams = () => {
const search = new URLSearchParams(window.location.search)
const paramsObj = {}
for (const [key, value] of search.entries()) {
paramsObj[key] = value
}
return paramsObj
}
函数柯里化
function curry(fn, args) {
let length = fn.length;
args = args || [];
return function() {
let subArgs = args.slice(0);
subArgs = subArgs.concat(arguments);
if(subArgs.length >= length) {
return fn.apply(this, subArgs);
} else {
return curry.call(this, fn, subArgs);
}
}
}
// 更好理解的方式
function curry(func, arity = func.length) {
function generateCurried(preArgs) {
return function curried(nextArgs) {
const args = [...preArgs, ...nextArgs];
if(args.length >= arity) {
return func(...args);
} else {
return generateCurried(args);
}
}
}
return generateCurried([]);
}
const myCurried = (fn, ...args) => {
if (args.length < fn.length) {
// 未接受完参数
return (..._args) => myCurried(fn, ...args, ..._args)
} else {
// 接受完所有参数,直接执行
return fn(...args)
}
}
function add(a, b, c) { return a + b + c }
const curriedAdd = myCurried(add)
## es6实现方式
function curry(fn, ...args) {
return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
}
实现 sleep 函数
作用:暂停 JavaScript 的执行一段时间后再继续执行
function sleep(time) {
return new Promise((resolve) => {
setTimeout(resolve, time)
})
}
// 使用
async function test() {
console.log('start')
await sleep(2000)
console.log('end')
}
test()
实现 Object.create
创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
// 定义一个名为 create 的函数,接受一个参数 obj,该参数是一个对象。
function create(obj) {
// 定义一个空的构造函数 Func。
function Func() {}
// 将传入的对象 obj 设置为 Func 的原型。
// 这意味着通过 Func 构造函数创建的新对象将继承 obj 的所有属性和方法。
Func.prototype = obj;
// 修正原型链中的构造函数指向。
// 因为当我们设置 `Func.prototype = obj` 之后,Func.prototype.constructor 默认指向的是 Object 构造函数,
// 所以我们需要显式地将其设置回 Func,以保持正确的原型链。
Func.prototype.constructor = Func;
// 使用 new Func() 创建一个新的实例,并返回这个实例。
// 这个新实例将会继承 obj 的所有属性和方法。
return new Func();
}
实现 instanceof 方法
instanceof 运算符用于判断构造函数的 prototype\color{tomato}{prototype}prototype 属性是否出现在对象的原型链的任何位置。
// 定义一个名为 myInstanceof 的函数,它接受两个参数:一个对象 obj 和一个构造函数 ctor。
function myInstanceof (obj,ctor) {
// 获取 obj 的原型对象
let proto = Object.getPrototypeOf(obj);
// 获取 ctor 的原型对象
let prototype = ctor.prototype;
// 开始一个无限循环,用于遍历原型链
while(true) {
// 如果当前原型对象不存在 (即原型链已经到达顶端),说明 obj 不是 ctor的实例
if(!proto) {
return false;
}
// 如果当前原型对象等于 ctor 的原型对象, 说明 obj 是 ctor 的实例
if (proto === prototype) {
return true;
}
/*
如果当前原型对象不是 ctor 的原型对象,则继续向上查找原型链
将当前原型对象的原型赋值给 proto, 继续循环。
*/
proto = Object.getPrototypeOf(prototype)
}
}
实现 new 关键字
在调用 new 之后会发生这几个步骤
-
- 创建一个空对象
-
- 设置原型: 将空白的对象的原型设置为函数的
prototype对象
- 设置原型: 将空白的对象的原型设置为函数的
-
- 让函数的
This指向这个对象, 执行构造函数的代码 (为空白对象添加属性)
- 让函数的
-
-
判断函数的返回值
- 4.1. 如果是引用类型,直接返回,比如构造函数主动返回了一个对象: function T() {retrun {x :1}}
- 4.2. 如果不是引用类型, 返回空白对象。
-
// 调用方法: objectFactory(构造函数,构造函数参数)
// 定义一个名为 objectFactory 的函数,用于模拟 new 操作符的行为。
function objectFactory () {
// 初始化 newObject 为 null, 稍后将被替换为新创建的对象
let newObject = null;
// 从 arguments 中 移除 第一个参数 (即构造函数),并将其赋值给 constructor
// 使用 Array.prototype.shift.call(arguments) 是为了模拟数组的 shift 方法,因为 arguments 不是真正的数组。
// let constructor = Array.prototype.shift.call(arguments)
let constructor = Array.prototype.slice(1).call(arguments);
let result = null;
// 检查 constructor 是否是一个函数, 如果不是,则输出错误信息并返回
if (typeof consteuctor !== "function") {
console.error ('type error');
return
}
// 使用 Object.create 方法创建一个新对象,该对象的原型为 constructor 的 prototype
// 这使得新对象能够继承 constructor 的原型上的 属性和方法
newObject = Object.create (constructor.prototype);
// 使用 apply 方法调用 constructor, 将 newObject 作为 this 的上下文, 并将剩余的 arguments 作为参数传递给 constructor。
// 这一步实际上是执行 构造函数的逻辑, 并将新创建的对象作为 This 上下文。
result = constructor.apply (newObject, arguments);、
// 检查 constructor 返回的结果 是否是一个非空对象或者函数
// 如果是, 则返回这个结果,否则返回新创建的对象 newObject。
let flag = result && (typeof result === "function" || typeof result === 'object' )
return flag ? result : newObject;
}
## 另一写法
function myNew(Con, ...arg) {
let obj = Object.create(Con.prototype)
let result = Con.apply(obj, arg)
return typeof result === 'object' ? result : obj
}
拦截构造函数调用
function Person(name) {
if (new.target !== undefined) { // 检查 `new.target` 是否定义
this.name = name; // 如果定义,则设置 `this.name` 为传入的名字
} else {
throw new Error('必须使用 new 命令生成实例'); // 如果未定义,则抛出错误
}
}
## 另一种写法
function Person(name) {
// 检查 `new.target` 是否指向 `Person` 构造函数
if (new.target === Person) {
// 如果指向 `Person`,则设置 `this.name` 为传入的名字
this.name = name;
} else {
// 如果不指向 `Person`,则抛出错误
throw new Error('必须使用 new 命令生成实例');
}
}
// 因为使用了 `new` 关键字,并且 `new.target` 指向 `Person`
var person = new Person('张三'); // 正确,
// 因为没有使用 `new` 关键字,`new.target` 为 `undefined`
var notAPerson = Person.call(person, '张三'); // 报错,
防抖函数
防抖是n秒内会重新计时
/**
* 创建一个防抖函数,确保原函数不会在指定时间内被连续调用多次。
*
* @param {Function} fn - 需要被防抖处理的函数。
* @param {number} wait - 在调用 fn 之后等待的时间(毫秒),在此期间重复调用将不会触发 fn。
* @returns {Function} 返回一个包装后的函数,该函数会在 wait 毫秒内抑制 fn 的多次调用。
*/
function debounce(fn, wait) {
// 定义一个全局变量用于保存定时器的句柄
let timer = null;
// 返回一个新函数, 这个函数会在 wait 毫秒内抑制 fn 的多次调用
return function() {
// 使用 apply 和 arguments 保证 fn 能够获取正确的 this 上下文所有参数
if(timer) {
// 如果有活跃的定时器, 清除它, 以确保不会重复触发 fn
clearTimeout(timer);
timer = null;
}
// 设置新的定时器, 在 wait 毫秒后 执行 fn, 并传递调用时所有参数
timer = setTimeout(() => {
fn.apply(this, arguments);
}, wait);
}
}
节流函数
n 秒内不重新计时
/**
* 创建一个节流(throttle)函数,确保原函数不会在指定时间内被连续调用多次。
*
* @param {Function} fn - 需要被节流处理的函数。
* @param {number} delay - 在调用 fn 之后等待的时间(毫秒),在此期间重复调用将不会触发 fn。
* @returns {Function} 返回一个包装后的函数,该函数会在 delay 毫秒内抑制 fn 的多次调用。
*/
function throttle(fn, delay) {
// 定义一个全局变量用于保存定时器的句柄
let timer = null;
// 返回一个新的函数,这个函数会在 delay 毫秒内抑制 fn 的多次调用
return function() {
// 如果已经有活跃的定时器,则直接返回,不执行 fn
if (timer) return;
// 设置新的定时器,在 delay 毫秒后执行 fn,并传递调用时的所有参数
timer = setTimeout(() => {
// 清除定时器,以便下次调用可以再次设置新的定时器
timer = null;
// 使用 apply 和 arguments 保证 fn 能够获取正确的 this 上下文和所有参数
return fn.apply(this, arguments);
}, delay);
};
}
实现 call 函数
执行步骤:
- 判断
call的调用者是否为函数,不是函数需要抛出错误,call调用者就是上下文this,也就是需要被调用的函数 - 判断需要被调用的函数的的上下文对象是否传入,不存在就设置为
window - 处理传入的参数,截取第一个参数后的所有参数,作为被调用函数
- 将需要被调用的函数,绑在传入的上下文上,作为一个属性
- 使用传入的上下文调用这个函数,并返回结果
- 删除绑定的属性
- 返回结果
/**
* 模拟 Function.prototype.call 方法,用于改变函数的 this 上下文并调用该函数。
*
* @param {Object} context - 要设置为函数 this 上下文的对象。
* @param {...any} args - 传递给函数的参数列表。
* @returns {*} 函数的返回值。
*/
Function.prototype.myCall = function(context) {
// 检查 this 是否是一个函数
if (typeof this !== 'function') {
throw new TypeError('need function'); // 如果不是函数,抛出错误
}
// 获取除第一个参数(context)之外的所有参数,并将其转换为数组
let args = Array.prototype.slice.call(arguments, 1);
// 如果没有提供 context 参数,则默认使用 window 对象(浏览器环境下)
// 或者 globalThis(Node.js 环境下)
context = context || (typeof window !== 'undefined' ? window : globalThis);
// 在 context 对象上定义一个临时方法 fn,并将其指向 this 函数
context.fn = this;
// 调用 context.fn 并传递参数
let result = context.fn(...args);
// 删除 context 对象上的 fn 属性
delete context.fn;
// 返回函数的执行结果
return result;
};
实现apply函数
- 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用
call等方式调用的情况 - 判断传入上下文对象是否存在,如果不存在,则设置为 window
- 将函数作为上下文对象的一个属性
- 判断参数值是否传入
- 使用上下文对象调用这个方法,并保存返回结果
- 删除刚才新增的属性。
- 返回结果
/**
* 模拟 Function.prototype.apply 方法,用于改变函数的 this 上下文并调用该函数。
*
* @param {Object} context - 要设置为函数 this 上下文的对象。
* @param {Array} [argsArray] - 传递给函数的参数数组。
* @returns {*} 函数的返回值。
*/
Function.prototype.myApply = function (context) {
// 判断调用对象是否为函数
if (typeof this !== "function") {
throw new TypeError("Error"); // 如果不是函数,抛出错误
}
let result = null;
// 判断 context 是否存在,如果未传入则为 window
context = context || window;
// 将函数设为对象的方法
context.fn = this;
// 调用方法
if (arguments[1]) {
// 如果第二个参数存在,使用 ...arguments[1] 将参数数组展开
result = context.fn(...arguments[1]);
} else {
// 如果没有提供参数数组,则直接调用 fn
result = context.fn();
}
// 将属性删除
delete context.fn;
// 返回结果
return result;
};
实现bind
-
先判断调用者是否为函数
-
缓存当前需要
bind的函数,就是上面的调用者,也是bind函数的上下文 -
返回一个函数,利用闭包原理实现对
this的保存 -
函数内部用
apply函数来处理函数调用- 需要判断函数作为构造函数的情况,这个时候的*
this*就是当前调用这个闭包函数的this - 作为普通函数,直接使用传入的上下文就好了
- 需要判断函数作为构造函数的情况,这个时候的*
/**
* 模拟 Function.prototype.call 方法,用于改变函数的 this 上下文并调用该函数。
*
* @param {Object} context - 要设置为函数 this 上下文的对象。
* @param {...any} args - 传递给函数的参数列表。
* @returns {*} 函数的返回值。
*/
Function.prototype.myCall = function(context) {
// 检查 this 是否是一个函数
if (typeof this !== 'function') {
throw new TypeError('need function'); // 如果不是函数,抛出错误
}
// 获取除第一个参数(context)之外的所有参数,并将其转换为数组
let args = Array.prototype.slice.call(arguments, 1);
// let args = [...arguments].slice(1);
// 如果没有提供 context 参数,则默认使用 window 对象(浏览器环境下)
// 或者 globalThis(Node.js 环境下)
context = context || (typeof window !== 'undefined' ? window : globalThis);
// 定义一个临时方法 fn,并将其指向 this 函数
fn = this;
return function Fn() {
// 根据调用方式,传入不同绑定值
return fn.apply(
this instanceof Fn ? this : context,
args.concat(...arguments)
);
};
浅拷贝
浅拷贝是创建一个新对象,这个新对象有着原始对象属性值的一份精确拷贝。如果属性值是基本数据类型,如字符串、数字、布尔值等,拷贝的就是值本身。但如果属性值是引用类型(如对象、数组),拷贝的只是其引用,而不是引用指向的实际对象。
// ES6 的 Object.assign 方法用于合并多个对象的属性到一个目标对象中。
// 例如:
const target = {};
const source1 = { a: 1 };
const source2 = { b: 2 };
const merged = Object.assign(target, source1, source2);
console.log(merged); // 输出: { a: 1, b: 2 }
// 扩展运算符(spread operator)用于将对象的属性展开到新的对象中。
// 例如:
const obj1 = { a: 1 };
const obj2 = { b: 2 };
const merged = { ...obj1, ...obj2 };
console.log(merged); // 输出: { a: 1, b: 2 }
// 数组的浅拷贝可以通过多种方式实现,例如使用 slice 或 concat 方法。
// 例如:
const original = [1, 2, 3];
const copy = original.slice(); // 浅拷贝
const copy2 = original.concat(); // 浅拷贝
// 手动实现浅拷贝
function shallowCopy(object) {
// 检查 object 是否为 null 或不是一个对象
if (!object || typeof object !== 'object') return;
// 创建一个新对象,如果 object 是数组,则创建一个新数组
let newObj = Array.isArray(object) ? [] : {};
// 遍历 object 的所有属性
for (let key in object) {
// 检查属性是否是 object 的自身属性(而非继承来的属性)
if (object.hasOwnProperty(key)) {
// 将属性复制到新对象中
newObj[key] = object[key];
}
}
// 返回新对象
return newObj;
}
深拷贝
深拷贝(Deep Copy)是指创建一个对象的完全独立的副本,不仅复制对象本身,还包括对象内部的所有嵌套对象。换句话说,深拷贝会递归地复制对象及其所有子对象,确保新创建的对象与原始对象之间没有任何引用关系。
深拷贝的特点
完全独立:深拷贝创建的对象与原始对象之间没有任何引用关系,即使原始对象发生改变,也不会影响到深拷贝的对象。递归复制:对于对象内部的嵌套对象,深拷贝也会递归地进行复制。适用于所有数据类型:深拷贝适用于基本数据类型(如数字、字符串、布尔值等)和复杂数据类型(如对象、数组等)。
深拷贝的应用场景
- 当需要在不改变原始数据的情况下对数据进行修改时。
- 当需要将数据结构传递给函数,而不希望函数修改原始数据时。
- 当需要在前端框架中更新状态时,以确保视图更新。
可能的问题
json方法出现函数或symbol类型的值的时候,会失效- 处理循环引用问题
- 处理可迭代类型的数据
- 处理包装类型
- 处理普通类型
## JSON.parse 和 JSON.stringify
/**
* 使用 JSON.stringify 将对象转换为 JSON 字符串,再使用 JSON.parse 将 JSON 字符串转换回 JavaScript 对象。
*这种方法简单易用,但对于某些类型的数据(如函数、循环引用、日期对象等)无法正确处理。
*/
function deepCopy(obj) {
return JSON.parse(JSON.stringify(obj));
}
## 递归函数
/**
* 使用递归函数手动实现深拷贝,这种方法可以处理各种类型的数据,包括函数和循环引用
* 适用域所有数据类型, 但是实现起来相对复杂
* 实现深拷贝功能,递归地复制对象及其所有子对象。
* @param {any} obj - 需要进行深拷贝的目标对象。
* @returns {any} 深拷贝后的对象。
*/
function deepCopy(obj) {
// 如果目标是 null,则直接返回 null
if (obj === null) return null;
// 如果目标不是对象,则直接返回目标
if (typeof obj !== 'object') return obj;
// 根据目标对象的类型创建一个新对象或数组
let copy = Array.isArray(obj) ? [] : {};
// 遍历目标对象的所有属性
for (let key in obj) {
// 检查属性是否是对象自身的属性
if (obj.prototype.hasOwnProperty(key)) {
// 对每个属性递归调用 deepCopy 方法
copy[key] = deepCopy(obj[key]);
}
}
// 返回深拷贝后的对象
return copy;
}
## 第三方库
/**
* 使用 lodash 等第三方库提供的深拷贝方法,如 `_.cloneDeep`。
* 这些库通常提供了高性能的实现,并处理了各种边缘情况。
*/
const _ = require('lodash');
// 定义一个原始对象
const originalObj = {
name: 'Alice', // 名字
age: 30, // 年龄
address: { // 地址
city: 'New York', // 城市
zip: 10001 // 邮编
},
hobbies: ['reading', 'swimming'] // 爱好
};
// 使用 _.cloneDeep 函数对 originalObj 进行深拷贝
const deepCopiedObj = _.cloneDeep(originalObj);
## 简单版本参考vue版本
/**
* 实现深拷贝功能,递归地复制对象及其所有子对象,处理循环引用。
*
* @param {any} target - 需要进行深拷贝的目标对象。
* @param {WeakMap} [cache=new WeakMap()] - 用于存储已拷贝的对象引用,防止循环引用。
* @returns {any} 深拷贝后的对象。
*/
const deepClone = (target, cache = new WeakMap()) => {
// 如果目标是 null 或者不是对象,则直接返回目标
if (target === null || typeof target !== 'object') {
return target;
}
// 检查目标对象是否已经被拷贝过
if (cache.get(target)) {
return target;
}
// 根据目标对象的类型创建一个新对象或数组
const copy = Array.isArray(target) ? [] : {};
// 将目标对象和新创建的拷贝对象存入缓存中
cache.set(target, copy);
// 遍历目标对象的所有属性
Object.keys(target).forEach(key => {
// 对每个属性递归调用 deepClone 方法
copy[key] = deepClone(target[key], cache);
});
// 返回深拷贝后的对象
return copy;
};
实现 Object.assign
就是实现一个浅拷贝
/**
* 模拟 Object.assign 方法,用于合并多个对象的属性到一个目标对象中。
*
* @param {Object} target - 目标对象,将被合并的对象属性添加到此对象中。
* @param {...Object} source - 一个或多个源对象,其属性将被合并到目标对象中。
* @returns {Object} 合并后的目标对象。
*/
Object.myAssign = function (target, ...source) {
// 检查目标对象是否为 null
if (target === null) {
throw new TypeError('can not be null'); // 如果是 null,则抛出错误
}
// 使用 Object() 方法确保目标对象是一个对象
let ret = Object(target);
// 遍历所有源对象
source.forEach(obj => {
// 检查源对象是否为 null
if (obj !== null) {
// 遍历源对象的所有可枚举属性
for (let key in obj) {
// 检查属性是否是对象自身的属性
if (obj.hasOwnProperty(key)) {
// 将源对象的属性复制到目标对象中
ret[key] = obj[key];
}
}
}
});
// 返回合并后的目标对象
return ret;
};
手写 ajax
function get() {
//创建ajax实例
let req = new XMLHTTPRequest()
if (req) {
//执行open 确定要访问的链接 以及同步异步
req.open('GET', 'http://test.com/?keywords=手机', true)
//监听请求状态
req.onreadystatechange = function () {
if (req.readystate === 4) {
if (req.statue === 200) {
console.log('success')
} else {
console.log('error')
}
}
}
//发送请求
req.send()
}
}
实现一个深度比较 isEqual
function isEqual(obj1, obj2) {
//不是对象,直接返回比较结果
if (typeof obj1 !== 'object' || typeof obj2 !== 'object') {
return obj1 === obj2
}
//都是对象,且地址相同,返回true
if (obj2 === obj1) return true
//是对象或数组
let keys1 = Object.keys(obj1)
let keys2 = Object.keys(obj2)
//比较keys的个数,若不同,肯定不相等
if (keys1.length !== keys2.length) return false
for (let k of keys1) {
//递归比较键值对
let res = isEqual(obj1[k], obj2[k])
if (!res) return false
}
return true
}
const obj1 = {
a: 100,
b: {
x: 100,
y: 200,
},
}
const obj2 = {
a: 200,
b: {
x: 100,
y: 200,
},
}
console.log(isEqual(obj1, obj2)) //false
设计一个图片懒加载 SDK
const throttle = (fn, delay) => {
let timer = null
return function (...args) {
if (!timer) {
setTimeout(() => {
fn.apply(this, args)
timer = null
}, delay)
}
}
}
const lazyLoadImages = () => {
const images = document.querySelectorAll('img[data-src]')
images.forEach((img) => {
const rect = img.getBoundingClientRect()
if (rect.top < window.innerHeight) {
img.src = img.dataSet.src
img.removeAttribute('data-src')
}
})
}
const throttledLazyLoad = throttle(lazyLoadImages, 100)
window.addEventListener('scroll', throttledLazyLoad)
手写 Promise
const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";
function MyPromise(fn) {
// 保存初始化状态
var self = this;
// 初始化状态
this.state = PENDING;
// 用于保存 resolve 或者 rejected 传入的值
this.value = null;
// 用于保存 resolve 的回调函数
this.resolvedCallbacks = [];
// 用于保存 reject 的回调函数
this.rejectedCallbacks = [];
// 状态转变为 resolved 方法
function resolve(value) {
// 判断传入元素是否为 Promise 值,如果是,则状态改变必须等待前一个状态改变后再进行改变
if (value instanceof MyPromise) {
return value.then(resolve, reject);
}
// 保证代码的执行顺序为本轮事件循环的末尾
setTimeout(() => {
// 只有状态为 pending 时才能转变,
if (self.state === PENDING) {
// 修改状态
self.state = RESOLVED;
// 设置传入的值
self.value = value;
// 执行回调函数
self.resolvedCallbacks.forEach(callback => {
callback(value);
});
}
}, 0);
}
// 状态转变为 rejected 方法
function reject(value) {
// 保证代码的执行顺序为本轮事件循环的末尾
setTimeout(() => {
// 只有状态为 pending 时才能转变
if (self.state === PENDING) {
// 修改状态
self.state = REJECTED;
// 设置传入的值
self.value = value;
// 执行回调函数
self.rejectedCallbacks.forEach(callback => {
callback(value);
});
}
}, 0);
}
// 将两个方法传入函数执行
try {
fn(resolve, reject);
} catch (e) {
// 遇到错误时,捕获错误,执行 reject 函数
reject(e);
}
}
MyPromise.prototype.then = function(onResolved, onRejected) {
// 首先判断两个参数是否为函数类型,因为这两个参数是可选参数
onResolved =
typeof onResolved === "function"
? onResolved
: function(value) {
return value;
};
onRejected =
typeof onRejected === "function"
? onRejected
: function(error) {
throw error;
};
// 如果是等待状态,则将函数加入对应列表中
if (this.state === PENDING) {
this.resolvedCallbacks.push(onResolved);
this.rejectedCallbacks.push(onRejected);
}
// 如果状态已经凝固,则直接执行对应状态的函数
if (this.state === RESOLVED) {
onResolved(this.value);
}
if (this.state === REJECTED) {
onRejected(this.value);
}
};
手写 Promise.then
then 方法返回一个新的 promise 实例,为了在 promise 状态发生变化时(resolve / reject 被调用时)再执行 then 里的函数,我们使用一个 callbacks 数组先把传给then的函数暂存起来,等状态改变时再调用。
那么,怎么保证后一个 **then** 里的方法在前一个 **then** (可能是异步)结束之后再执行呢? 我们可以将传给 then 的函数和新 promise 的 resolve 一起 push 到前一个 promise 的 callbacks 数组中,达到承前启后的效果:
- 承前:当前一个
promise完成后,调用其resolve变更状态,在这个resolve里会依次调用callbacks里的回调,这样就执行了then里的方法了 - 启后:上一步中,当
then里的方法执行完成后,返回一个结果,如果这个结果是个简单的值,就直接调用新promise的resolve,让其状态变更,这又会依次调用新promise的callbacks数组里的方法,循环往复。。如果返回的结果是个promise,则需要等它完成之后再触发新promise的resolve,所以可以在其结果的then里调用新promise的resolve
/**
* then 方法用于处理 Promise 的成功和失败情况,并返回一个新的 Promise 对象。
*
* @param {Function} onFulfilled - 当 Promise 成功时调用的回调函数。
* @param {Function} [onRejected] - 当 Promise 失败时调用的回调函数。
* @returns {MyPromise} 一个新的 Promise 对象。
*/
then(onFulfilled, onRejected) {
// 保存当前 Promise 的 this 上下文
const self = this;
// 返回一个新的 Promise 对象
return new MyPromise((resolve, reject) => {
// 封装前一个 Promise 成功时执行的函数
let fulfilled = () => {
try {
// 调用 onFulfilled 回调函数,并传入当前 Promise 的值
const result = onFulfilled(self.value);
// 检查 result 是否是一个 MyPromise 实例
if (result instanceof MyPromise) {
// 如果 result 是一个 MyPromise 实例,则继续使用 then 方法处理
result.then(resolve, reject);
} else {
// 如果 result 不是一个 MyPromise 实例,则直接调用 resolve 方法
resolve(result);
}
} catch (err) {
// 如果 onFulfilled 回调函数抛出异常,则调用 reject 方法
reject(err);
}
};
// 封装前一个 Promise 失败时执行的函数
let rejected = () => {
try {
// 调用 onRejected 回调函数,并传入当前 Promise 的原因
const result = onRejected(self.reason);
// 检查 result 是否是一个 MyPromise 实例
if (result instanceof MyPromise) {
// 如果 result 是一个 MyPromise 实例,则继续使用 then 方法处理
result.then(resolve, reject);
} else {
// 如果 result 不是一个 MyPromise 实例,则直接调用 reject 方法
reject(result);
}
} catch (err) {
// 如果 onRejected 回调函数抛出异常,则调用 reject 方法
reject(err);
}
};
// 根据当前 Promise 的状态决定如何处理
switch (self.status) {
case PENDING: // 如果当前状态为 pending
// 将 fulfilled 和 rejected 函数添加到回调队列中
self.onFulfilledCallbacks.push(fulfilled);
self.onRejectedCallbacks.push(rejected);
break;
case FULFILLED: // 如果当前状态为 fulfilled
// 立即执行 fulfilled 函数
fulfilled();
break;
case REJECTED: // 如果当前状态为 rejected
// 立即执行 rejected 函数
rejected();
break;
}
});
}
注意:
- 连续多个
then里的回调方法是同步注册的,但注册到了不同的callbacks数组中,因为每次then都返回新的promise实例(参考上面的例子和图) - 注册完成后开始执行构造函数中的异步事件,异步完成之后依次调用
callbacks数组中提前注册的回调
手写 Promise.all
一般来说,Promise.all 用来处理多个并发请求,也是为了页面数据构造的方便,将一个页面所用到的在不同接口的数据一起请求过来,不过,如果其中一个接口失败了,多个请求也就失败了,页面可能啥也出不来,这就看当前页面的耦合程度了
/**
* 实现 Promise.all 功能,等待所有 Promise 对象都完成(resolve 或 reject)。
*
* @param {Array<Promise>} promises - 一个 Promise 对象的数组。
* @returns {Promise} 一个新的 Promise 对象,当所有输入的 Promise 对象都完成时,返回一个包含所有结果的数组。
*/
function promiseAll(promises) {
// 检查输入是否为数组
if (!Array.isArray(promises)) {
throw new TypeError('argument must be an array');
}
// 初始化计数器和结果数组
let resolvedCounter = 0;
let promiseNum = promises.length;
let resolvedResult = [];
// 返回一个新的 Promise 对象
return new Promise(function (resolve, reject) {
// 遍历所有的 Promise 对象
for (let i = 0; i < promiseNum; i++) {
// 使用 Promise.resolve 确保传入的是 Promise 对象
Promise.resolve(promises[i]).then(
function (value) {
// 当一个 Promise 对象完成时,增加计数器
resolvedCounter++;
// 存储该 Promise 对象的结果到结果数组中
resolvedResult[i] = value;
// 如果所有 Promise 对象都已完成,调用 resolve 方法
if (resolvedCounter === promiseNum) {
resolve(resolvedResult);
}
},
function (error) {
// 如果任何一个 Promise 对象失败,立即调用 reject 方法
reject(error);
}
);
}
});
}
// 测试代码
let p1 = new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(1);
}, 1000);
});
let p2 = new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(2);
}, 2000);
});
let p3 = new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(3);
}, 3000);
});
// 使用 promiseAll 测试
promiseAll([p3, p1, p2]).then(res => {
console.log(res); // [3, 1, 2]
});
手写 Promise.race
该方法的参数是 Promise 实例数组, 然后其 then 注册的回调方法是数组中的某一个 Promise 的状态变为 fulfilled 的时候就执行. 因为 Promise 的状态只能改变一次, 那么我们只需要把 Promise.race 中产生的 Promise 对象的 resolve 方法, 注入到数组中的每一个 Promise 实例中的回调函数中即可.
/**
* 实现 Promise.race 功能,返回一个 Promise,该 Promise 会在给定的 Promises 中任意一个最先完成(resolve 或 reject)时完成。
*
* @param {Array<Promise>} args - 一个包含 Promise 对象的数组。
* @returns {Promise} 一个新的 Promise 对象,当给定的 Promises 中任意一个最先完成时,这个新的 Promise 也会完成。
*/
Promise.race = function (args) {
// 返回一个新的 Promise 对象
return new Promise((resolve, reject) => {
// 遍历所有的 Promise 对象
for (let i = 0, len = args.length; i < len; i++) {
// 使用 .then 方法监听每个 Promise 的完成状态
args[i].then(
function (value) {
// 当一个 Promise 完成时,立即调用 resolve 方法
resolve(value);
},
function (reason) {
// 当一个 Promise 失败时,立即调用 reject 方法
reject(reason);
}
);
}
});
};
简单实现async/await中的async函数
async/await语法糖就是使用 Generator函数+自动执行器 来运作的,注意只要要实现async 函数就是实现一个generate函数+执行器的语法糖
/**
* 定义一个异步函数 getNum,用于模拟异步操作,并在1秒后返回一个数值加1的结果。
*
* @param {number} num - 初始数值。
* @returns {Promise} 一个 Promise 对象,将在1秒后解析为 num + 1。
*/
function getNum(num) {
return new Promise((resolve, reject) => {
// 使用 setTimeout 模拟异步操作
setTimeout(() => {
// 在1秒后解析 Promise,返回 num + 1
resolve(num + 1);
}, 1000);
});
}
/**
* 自动执行器函数,用于递归地执行 Generator 函数中的异步操作。
*
* @param {Function} func - 一个 Generator 函数。
*/
function asyncFun(func) {
// 创建 Generator 函数的实例
var gen = func();
/**
* 处理 Generator 函数中的下一步操作。
*
* @param {*} data - 从上一步异步操作返回的数据。
*/
function next(data) {
// 调用 Generator 函数的 next 方法,并传递上一步异步操作返回的数据
var result = gen.next(data);
// 如果 Generator 函数已经执行完毕,则返回最终结果
if (result.done) return result.value;
// 如果 Generator 函数还没有执行完毕,则继续处理下一步异步操作
result.value.then(function (data) {
// 递归调用 next 方法处理下一步操作
next(data);
});
}
// 开始执行 Generator 函数的第一步操作
next();
}
// 定义一个 Generator 函数,用于按顺序执行异步操作
var func = function* () {
// 第一步异步操作:获取初始数值加1的结果
var f1 = yield getNum(1);
// 第二步异步操作:获取第一步操作的结果加1的结果
var f2 = yield getNum(f1);
// 打印第二步操作的结果
console.log(f2);
};
// 使用 asyncFun 自动执行器函数执行 Generator 函数
asyncFun(func);