Object.create()
用于创建一个新对象,并指定该对象的原型
用法
Object.create(proto, [propertiesObject])
- proto:新对象的原型(__proto__指向谁),可以是对象或者null
- propertiesObject:用于给对象定义新属性
const person = {
sayHi() {
console.log(`Hi ${this.name}`);
},
};
const dy = Object.create(person);
dy.name = 'dy';
dy.sayHi(); // Hi dy
const dy2 = Object.create(person, {
name: {
value: 'DY',
enumerable: true,
writable: true,
configurable: true,
},
});
dy2.sayHi(); // Hi DY
场景
-
原型继承
-
function createDog(name) { const animal = { eat() { console.log('eating'); }, }; const dog = Object.create(animal); dog.name = name; dog.bark = function () { console.log('barking'); }; return dog; } const d = createDog('dog01'); d.eat(); d.bark();
-
-
创建一个无原型对象(纯净字典)
-
const dict = Object.create(null); dict['key'] = 'value'; console.log(dict.toString); // undefined -
纯净字典:没有原型的对象,不像普通对象那样继承Object.prototype上的属性和方法
// 普通对象 obj --> Object.prototype --> null //纯净字典 dict --> null
-
-
克隆对象(浅拷贝并保留原型)
-
function clone(obj) { return Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj)) }
-
实现
- 用临时构造函数将prototype设置为传入的proto,然后返回构造函数的实例
function createObject(obj, propertiesObject) {
function F() {}
F.prototype = obj;
const newObj = new F();
if (propertiesObject && typeof propertiesObject === 'object') {
Object.defineProperties(newObj, propertiesObject);
}
return newObj;
}
与new的区别
| new | 依赖构造函数的prototype | 会执行构造函数 |
|---|---|---|
| Object.create() | 直接指定原型对象 | 不执行构造函数 |
new
调用构造函数并创建一个带有指定原型的新对象
用法
new Constructor(arg1, arg2, ...)
- 创建一个空对象
- 将空对象的__proto__指向构造函数的prototype
- 执行构造函数,把this绑定到新对象
- 如果构造函数返回的是对象,则返回该对象,否则返回1创建的空对象
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function () {
console.log(`Hi ${this.name}`);
};
const dy = new Person('dy', 18);
dy.sayHi();
场景
-
创建实例对象
- 用构造函数批量创建带有相同方法和不同数据的对象
-
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.sayHi = function () { console.log(`Hi ${this.name}`); }; const dy1 = new Person('dy1', 18); const dy2 = new Person('dy2', 18); const dy3 = new Person('dy3', 18); dy1.sayHi(); dy2.sayHi(); dy3.sayHi();
-
继承实现
-
function Animal(name) { this.name = name; } Animal.prototype.eat = function () { console.log(`${this.name} is eating`); }; function Dog(name) { Animal.call(this, name); } Dog.prototype = Object.create(Animal.prototype); Dog.prototype.constructor = Dog; const dog = new Dog('dog'); dog.eat();
-
-
自定义类的初始化
-
class Person { constructor(name) { this.name = name; } } const dy = new Person('dy');
-
实现
function myNew(Constructor, ...args) {
const obj = {};
Object.setPrototypeOf(obj, Constructor.prototype);
const result = Constructor.apply(obj, args);
return (result && (typeof result === 'object' || typeof result === 'function')) ? result : obj;
}
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() {
console.log(`Hi, I'm ${this.name}`);
};
const p1 = myNew(Person, "dy");
p1.sayHi(); // Hi, I'm dy
console.log(p1 instanceof Person); // true
instanceof
用法
object instanceof Constructor
- object:要检测的实例对象
- Constructor:构造函数(类)
- 检测object原型链上是否存在Contructor.prototype,不能用于基本类型的检查
function Person() {}
const p = new Person();
console.log(p instanceof Person); // true
console.log(p instanceof Object); // true
console.log([] instanceof Array); // true
console.log([] instanceof Object); // true
场景
-
类型判断
-
console.log([] instanceof Array); // true console.log({} instanceof Object); // true console.log(function(){} instanceof Function); // true
-
-
继承关系检查
-
class Animal {} class Dog extends Animal {} const dog = new Dog(); console.log(dog instanceof Dog); // true console.log(dog instanceof Animal); // true
-
实现
- instanceof会沿着object.proto([[prototype]])链一直向上查找,看是否有和Constructor.prototype严格相等的引用
function myInstanceof(obj, Constructor) {
// 如果obj不是对象或者为null 直接返回false
if (obj === null || (typeof obj !== 'object' && typeof obj !== 'function')) {
return false;
}
let proto = Object.getPrototypeOf(obj);
const prototype = Constructor.prototype;
while (proto) {
if (proto === prototype) {
return true;
}
proto = Object.getPrototypeOf(proto);
}
return false;
}
封装类型判断方法
- typeof:对基础类型有效,对null/array/object不够精确
- instanceof:只能判断引用类型数据,且受原型链影响
- Object.prototype.toString.call:能够很好区分
function type(value) {
if (value === null) return 'null';
if (typeof value !== 'object') return typeof value;
return Object.prototype.toString().call(value).slice(8,-1).toLowerCase();
}
call
用法
调用函数,并指定函数执行时的this指向,同时按顺序传递参数
func.call(thisArg, arg1, arg2, ...)
场景
-
改变this指向
-
function sayHi(greeting) { console.log(greeting + ", " + this.name); } const person = { name: "dy" }; greet.call(person, "Hello"); // Hello, dy
-
-
借用方法
-
const person1 = { name: "dy", sayHi() { console.log("Hi, I'm " + this.name); } }; const person2 = { name: "DY" }; person1.sayHi.call(person2); // Hi, I'm DY
-
-
伪数组转数组
-
function demo() { // arguments 是伪数组 const arr = Array.prototype.slice.call(arguments); console.log(arr); } demo(1, 2, 3); // [1, 2, 3]
-
实现
Function.prototype.myCall = function (context, ...args) {
context = context || globalThis;
const fnSymbol = Symbol('fn');
context[fnSymbol] = this;
const result = context[fnSymbol](...args);
delete context[fnSymbol];
return result;
}
apply
用法
func.apply(thisArg, [argsArray])
- argsArray:传入函数的参数,必须是数组或者类数组对象
const person = {
name: "dy",
};
function greet(age, city) {
console.log(`Hello, I'm ${this.name}, ${age} years old, from ${city}`);
}
greet.apply(person, [18, "BJ"]);
// Hello, I'm dy, 18 years old, from BJ
实现
Function.prototype.myApply = function (context, args) {
context = context || globalThis;
const fnSymbol = Symbol('fn');
context[fnSymbol] = this;
const result = args ? context[fnSymbol](...args) : context[fnSymbol]();
delete context[fnSymbol];
return result;
}
bind
用法
返回一个新函数,函数的this被绑定到传入的目标对象上
function greet(greeting) {
console.log(greeting + ', ' + this.name);
}
const person = { name: 'dy' };
const bound = greet.bind(person, 'Hello');
bound(); // Hello, dy
实现
Function.prototype.myBind = function (context, ...args) {
const self = this;
return function (...newArgs) {
if (this instanceof self) {
return new self(...args, ...newArgs);
}
return self.apply(context, args.concat(newArgs));
}
}
Object.setPrototypeOf
用法
用于动态设置对象的原型([[prototype]] / proto)
Object.setPrototypeOf(obj, prototype);
- obj:要修改原型的目标对象
- prototype:新的原型(可以是null)
const animal = { type: "animal" };
const dog = { name: "Buddy" };
Object.setPrototypeOf(dog, animal);
console.log(dog.type); // animal
console.log(Object.getPrototypeOf(dog) === animal); // true
场景
-
手动更改对象的继承关系
-
const a = {sayHi () {console.log('hi')}}; const b = {name: "b"}; Object.setPrototypeOf(b, a); b.sayHi(); // hi
-
实现
function mySetPrototypeOf(obj, proto) {
obj.__proto__ = proto;
return obj;
}
Object.freeze
用法
冻结对象,使对象不能被扩展、删除或修改属性的值
只能冻结对象的第一层属性
如果属性值是对象,对象内部是可变的
const obj = {name: 'dy', age: 18};
Object.freeze(obj);
obj.name = 'DY';
delete obj.age;
obj.hobby = 'eat';
console.log(obj); // {name: 'dy', age: 18}
const user = {
name: "dy",
info: { city: "ShanXi" }
};
Object.freeze(user);
user.info.city = "Beijing"; //
console.log(user.info.city); // "Beijing"
场景
-
常量对象
-
const CONFIG = Object.freeze({ API_URL: "https://api.example.com", TIMEOUT: 5000 });
-
-
防止对象被修改
-
function createUser(name) { return Object.freeze({ name }); } const u = createUser("dy"); u.name = "DY"; //'dy'
-
实现
-
浅冻结(同Object.freeze)
-
function shallowFreeze(obj) { Object.keys(obj).forEach(key => { Object.defineProperty(obj, key, { writable: false, configurable: false }) }) Object.preventExtensions(obj); return obj; }
-
-
深冻结(对象的对象也可冻结)
-
function deepFreeze(obj) { Object.freeze(obj); Object.keys(obj).forEach(key => { if (typeof obj[key] === 'object' && obj[key] !== null) { deepFreeze(obj[key]); } }); return obj; }
-
Promise.all
用法
用于将多个Promise组合成一个Promise,当所有Promise都成功时才会resolve,有一个失败就会reject
const p1 = Promise.resolve(1);
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(2);
}, 1000);
});
const p3 = Promise.resolve(3);
Promise.all([p1, p2, p3])
.then(values => {
console.log(values); // [1, 2, 3]
})
.catch(err => {
console.log(err);
});
- 输入:一个可迭代对象(通常是数组),里面的值可以是Promise值或者普通值
- 输出:一个新的Promise
- 成功:所有Promise都返回fulfilled,按顺序返回结果数组
- 失败:第一个rejected的原因会作为结果返回
注意
-
顺序与完成时间无关
- 结果是按照数组传入顺序而非完成时间顺序
-
Promise.all([ Promise.resolve('a'), new Promise(res => setTimeout(() => res('b'), 100)), Promise.resolve('c') ]).then(console.log); // ['a', 'b', 'c']
-
遇到第一个reject
-
Promise.all([ Promise.resolve(1), Promise.reject('error'), Promise.resolve(3) ]) .catch(console.error); // "error"
-
-
普通值转换为Promise
-
Promise.all([1, 2, Promise.resolve(3)]) .then(console.log); // [1, 2, 3]
-
-
空数组返回立即成功的Promise
-
Promise.all([]).then(console.log); // []
-
场景
-
同时请求多个接口,合并结果
- 页面需要多个接口数据才能渲染
-
Promise.all([ fetch('/api/user').then(res => console.log(res)), fetch('/api/orders').then(res => console.log(res)), fetch('/api/notifications').then(res => console.log(res)) ]).then(([user, orders, notifications]) => { console.log('全部数据已获取', { user, orders, notifications }); });
-
批量预加载资源
-
const preloadImage = src => new Promise((resolve, reject) => { const img = new Image(); img.onload = resolve; img.onerror = reject; img.src = src; }); Promise.all([ preloadImage('/img/1.png'), preloadImage('/img/2.png'), preloadImage('/img/3.png') ]).then(() => console.log('所有图片已预加载'));
-
-
多条校验一次完成
-
Promise.all([ checkUsername('dy'), checkEmail('dy.com') ]).then(([usernameOk, emailOk]) => { if (usernameOk && emailOk) console.log('注册信息可用'); });
-
实现
-
创建一个新的Promise
-
遍历传入的可迭代对象
- 用Promise.resolve统一转换成Promise
- Promise成功,存储到结果数组对应的位置
- Promise失败立即reject
-
当计数器等于传入Promise的数量,resolve结果数组
function myPromiseAll(promises) {
return new Promise((resolve, reject) => {
if (!Array.isArray(promises)) {
return reject(new TypeError('need array'));
}
const results = [];
let count = 0;
if (promises.length === 0) {
return resolve([]);
}
promises.forEach((p, index) => {
//遍历传入的所有 Promise(或类 Promise 对象)
Promise.resolve(p)
.then(value => {
//使用 Promise.resolve() 将 p 转换为 Promise(处理非 Promise 值)
results[index] = value; // 将结果按原始顺序存储在 results 数组中对应位置
count++;
if (count === promises.length) {
resolve(results);
}
})
.catch(reject);
});
});
}
Promise.race
用法
Promise.race(iterable)
- 第一个有结果(resolve,reject)的Promise决定Promise.race的最终状态和返回值
- 不关心其他Promise是否还在执行
- 结果可能是成功或者失败,取决于第一个完成的Promise
场景
-
网络请求超时控制
-
function fetchWithTimeout(url, ms) { const timeout = new Promise((_, reject) => { setTimeout(() => reject(new Error('timeout'), ms)); }); return Promise.race([fetch(url), timeout]); } fetchWithTimeout('/api/user', 3000) .then(res => console.log(res)) .catch(err => { console.error(err); });
-
-
取最快结果(CDN/镜像抢先)
-
const cdn1 = fetch('https://cdn1.example.com/file'); const cdn2 = fetch('https://cdn2.example.com/file'); Promise.race([cdn1, cdn2]) .then(res => console.log('最快的CDN返回:', res)) .catch(console.error);
-
-
用户交互等待
- 兼容用户主动与超时自动
-
function waitForUserAction(ms) { const clickPromise = new Promise(resolve => document.addEventListener('click', () => resolve('clicked'), { once: true }) ); const timeoutPromise = new Promise(resolve => setTimeout(() => resolve('timeout'), ms) ); return Promise.race([clickPromise, timeoutPromise]); } waitForUserAction(5000).then(console.log);
实现
- 接收一个可迭代对象作为参数
- 遍历其中每个元素,用Promise.resolve转成Promise
- 对每个Promise注册then和catch
- 谁先调用resolve/reject,直接把结果传给Promise.race的resolve/reject并结束流程
Promise.myRace = function (promises) {
return new Promise((resolve, reject) => {
for (const p of promises) {
Promise.resolve(p)
.then(resolve)
.catch(reject);
}
})
}
Promise.myRace([
new Promise(res => setTimeout(() => res('A'), 1000)),
new Promise(res => setTimeout(() => res('B'), 500))
]).then(console.log); // 'B'
Promise.settled
用法
用来并行执行多个Promise,不论成功或失败,都会在全部完成后返回一个“汇总数组”
Promise.allSettled(iterable)
- 返回一个新的Promise,状态总是fulfilled,值是一个数组,每个元素对应输入的Promise的最终状态
const p1 = Promise.resolve(100);
const p2 = Promise.reject("出错了");
const p3 = new Promise(resolve => setTimeout(() => resolve("完成"), 500));
Promise.allSettled([p1, p2, p3]).then(results => {
console.log(results);
});
/**
[
{ status: "fulfilled", value: 100 },
{ status: "rejected", reason: "出错了" },
{ status: "fulfilled", value: "完成" }
]
*/
场景
-
批量接口请求
- 页面需要请求多个接口,即使某个失败,也要正常展示其余内容
- 保证流程完整性
-
const apis = [fetch("/user"), fetch("/orders"), fetch("/messages")]; Promise.allSettled(apis).then(results => { results.forEach(r => { if (r.status === "fulfilled") { console.log("成功:", r.value); } else { console.error("失败:", r.reason); } }); });
实现
- 把每个都包装成“永远成功”
Promise.myAllSettled = function myAllSettled(promises) {
return Promise.all(
promises.map(p => {
return Promise.resolve(p).then(
value => ({ status: 'fulfilled', value }),
reason => ({ status: 'rejected', reason })
);
//.catch(reason => ({ status: 'rejected', reason }));
})
);
};
Promise.myAllSettled([p1, p2, p3]).then(results => {
console.log(results);
});
Promise.any
用法
只要有一个Promise成功,就返回。如果所有的Promise都报错才会报错
Promise.any(iterable);
-
一个可迭代对象
-
返回:一个新的Promise
- 第一个成功的结果--->fulfilled,返回该结果。只要有一个成功,立即返回
- 如果全部失败--->rejected,返回一个AggregateError,里面包含所有错误的原因
const p1 = Promise.reject("失败1");
const p2 = new Promise(res => setTimeout(() => res("成功2"), 100));
const p3 = new Promise(res => setTimeout(() => res("成功3"), 200));
Promise.any([p1, p2, p3]).then(result => {
console.log(result); // "成功2"
});
const p1 = Promise.reject("错误1");
const p2 = Promise.reject("错误2");
Promise.any([p1, p2])
.then(result => console.log("成功:", result))
.catch(err => {
console.log("全部失败:", err);
console.log(err.errors); // ["错误1", "错误2"]
});
实现
Promise.myAny = function (promises) {
return new Promise((resolve, reject) => {
let rejections = [];
let pending = promises.length;
if (pending === 0) {
return reject(new AggregateError('No promises resolved', []));
}
promises.forEach((p, i) => {
Promise.resolve(p).then(
value => resolve(value),
reson => {
rejections[i] = reson;
pending--;
if (pending === 0) {
reject(
new AggregateError(rejections, 'All promises were rejected')
);
}
}
);
});
});
};
柯里化函数
用法
把一个接受多个参数的函数,转化为一系列接受一个参数的函数
function add(a, b, c) {
return a + b + c;
}
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6
实现
-
普通版
-
function curry(fn) { return function curried(...args) { if (args.length >= fn.length) { return fn.apply(this, args); } else { return function (...nextArgs) { return curried.apply(this, args.concat(nextArgs)); }; } }; }
-
防抖(Debounce)
在事件被触发后,延迟一定时间执行回调,如果在延迟期间再次触发,则重新计时
- 减少频繁触发,尤其是高频事件(scroll,resize,input)
- 保证函数只在最后一次触发后执行
实现
-
输入搜索防抖
-
function search(query) { console.log(query); } const debounce = (fn, delay) => { let timer = null; return function (...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; }; const debouncedSearch = debounce(search, 500); document.querySelector('#input').addEventListener('input', event => { debouncedSearch(event.target.value); });
-
-
第一次立即执行
-
const debounce = (fn, delay) => { let timer = null; return function (...args) { if(!timer) fn.apply(this, args); // 第一次立即执行 clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; };
-
节流(throttle)
让一个函数在一定时间内最多执行一次
实现
-
时间戳版
- 立即执行一次,后续在规定时间间隔内不再触发
- 第一次立即执行,但最后一次可能丢失
-
function throttle(fn, delay) { let lastTime = 0; return function (...args) { const now = Date.now(); if (now - lastTime >= delay) { lastTime = now; return fn.apply(this, args); } }; } window.addEventListener( 'scroll', throttle(function () { console.log('scrolling...'); }, 1000) );
-
定时器版
- 等到一定时间后再执行(固定间隔执行)
- 第一次不会立即执行,最后一次不会丢失
-
function throttle(fn, delay) { let timer = null; return function (...args) { if (!timer) { timer = setTimeout(() => { fn.apply(this, args); timer = null; }, delay); } }; } document.getElementById('btn').onclick = throttle(() => { console.log("按钮点击:", Date.now()); }, 2000);
-
时间戳+定时器
-
function throttle(fn, delay) { let lastTime = 0; let timer = null; return function (...args) { const now = Date.now(); const remaining = delay - (now - lastTime); if (remaining <= 0) { if (timer) { clearTimeout(timer); timer = null; } fn.apply(this, args); lastTime = now; } else if (!timer) { timer = setTimeout(() => { fn.apply(this, args); lastTime = Date.now(); timer = null; }, remaining); } }; }
-
防抖升级版:竞争触发
不等待(不像传统防抖延迟发请求),每次输入都立即发请求,但只让“该用的结果生效”,避免“旧请求慢返回把新结果覆盖”的问题
策略
-
最新优先(takeLatest)
- 只让最后一次触发的结果生效
- 搜索联想、级联选择、表格筛选 --- 每次输入都请求,但UI只展示最后一次输入对应的数据
-
最快优先(fastesWins)
-
谁先返回就用谁
-
多个镜像/CDN/数据源同时请求,先到先用
-
0.1+0.2
现象
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false
原因
JS中数字(除BigInt)都遵循双精度浮点数标准,像0.1,0.2这样的十进制小数,无法用二进制浮点数表示
- 0.1 转换为二进制是无限循环小数 0.0001100110011...(无限)
- 0.2也是无限循环
- 存储时会被截断,导致 微小误差
当 0.1+0.2 时,误差累加,结果就变成了 0.30000000000000004。
解决方案
- 误差容忍
使用Number.EPSILON判断两个数
function equal(a, b) {
return Math.abs(a - b) < Number.EPSILON;
}
console.log(equal(0.1 + 0.2, 0.3)); // true
- 四舍五入
let sum = 0.1 + 0.2;
console.log(Number(sum.toFixed(10))); // 0.3
- 整数化后计算
console.log((0.1 * 10 + 0.2 * 10) / 10); // 0.3
大数加法
bigInt
const a = 12345678901234567890n;
const b = 98765432109876543210n;
console.log(a + b); // 111111111011111111100n
实现
-
把数字当字符串处理,每一位手动相加
-
function addBigInt(a, b) { a = a.toString(); b = b.toString(); const maxLength = Math.max(a.length, b.length); a = a.padStart(maxLength, '0'); b = b.padStart(maxLength, '0'); let carry = 0; let res = []; for (let i = maxLength - 1; i >= 0; i--) { const sum = parseInt(a[i]) + parseInt(b[i]) + carry; res.push(sum % 10); carry = Math.floor(sum / 10); } if (carry) { res.push(carry); } return res.reverse().join(''); }
-
大数减法
function subBigInt(a, b) {
a = a.toString();
b = b.toString();
a = a.replace(/^0+/, '') || '0';
b = b.replace(/^0+/, '') || '0';
let sign = '';
if (a.length < b.length || (a.length === b.length && a < b)) {
[a, b] = [b, a];
sign = '-';
}
const maxLength = Math.max(a.length, b.length);
a = a.padStart(maxLength, '0');
b = b.padStart(maxLength, '0');
let borrow = 0;
let res = [];
for (let i = maxLength - 1; i >= 0; i--) {
let x = parseInt(a[i]) - borrow;
const y = parseInt(b[i]);
if (x < y) {
x += 10;
borrow = 1;
} else {
borrow = 0;
}
res.push(x - y);
}
res = res.reverse().join('').replace(/^0+/, '') || '0';
return sign + res;
}
setTimeout递归setInterval
-
基本款
-
function run() { console.log('run', new Date().toLocaleTimeString()); setTimeout(run, 1000); } setTimeout(run, 1000);
-
-
带终止条件
-
let count = 0; function run() { console.log('第', ++count, '次执行'); if (count < 5) { setTimeout(run, 1000); // 只执行5次 } } setTimeout(run, 1000);
-
分隔千分位
-
非正则版
-
function formatNumber(n) { let numStr = n.toString(); let sign = ''; if (numStr[0] === '-') { sign = '-'; numStr = numStr.slice(1); } let [intPart, decimalPart] = numStr.split('.'); let result = ''; let count = 0; for (let i = intPart.length - 1; i >= 0; i--) { result = intPart[i] + result; count++; if (count % 3 === 0 && i !== 0) { result = ',' + result; } } return sign + (decimalPart ? `${result}.${decimalPart}` : result); }
-
-
正则版
-
function formatNumber(n) { let numStr = n.toString(); let sign = ''; if (numStr[0] === '-') { sign = '-'; numStr = numStr.slice(1); } let [intPart, decimalPart] = numStr.split('.'); intPart = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ','); return sign + (decimalPart ? `${intPart}.${decimalPart}` : intPart); }
-
深拷贝与浅拷贝
浅拷贝
只复制对象的第一层属性,如果属性是引用类型,只复制它的地址
const obj = {a: 1, b: {c: 2}};
const copy = {...obj};
copy.a = 100;
copy.b.c = 200;
console.log(obj); // { a: 1, b: { c: 200 } }
a 是基本类型:
- 浅拷贝时复制了值 1 本身
- copy.a = 100 修改的是 copy 对象中的值
- 原对象 obj.a 仍然保持为 1
b 是引用类型:
- 浅拷贝时只复制了引用地址,没有创建新对象
- copy.b 和 obj.b 指向同一个对象 { c: 2 }
- copy.b.c = 200 实际上修改了共同引用的对象
- 因此 obj.b.c 也变成了 200
常用方法
-
Object.assign({}, obj)
-
{...obj}
-
Array.prototype.slice()/contact()
深拷贝
不仅复制对象的第一层,还会递归地复制所有子对象,新对象与旧对象完全独立
const obj = { a: 1, b: { c: 2 } };
const copy = JSON.parse(JSON.stringify(obj));
copy.b.c = 200;
console.log(obj); // { a: 1, b: { c: 2 } }
常用方法
-
使用JSON.stringfy()将对象序列化,再将其通过JSON.parse()反序列化
- 会丢失undefined、function、symbol等属性
- 如果有正则表达式、Error会得到空对象
- 无法处理循环引用
const deepCopy = JSON.parse(JSON.stringify(obj));
实现
function deepClone(target, map = new WeakMap()) {
if (target === null || typeof target !== 'object') {
return target; // 基础类型直接返回
}
if (map.has(target)) {
return map.get(target); // 循环引用处理
}
let clone = Array.isArray(target) ? [] : {};
map.set(target, clone);
for (const key in target) {
if (target.hasOwnProperty(key)) {
clone[key] = deepClone(target[key], map); // 递归深拷贝
}
}
return clone;
}
const obj = { a: 1, b: { c: 2 } };
const copy = deepClone(obj);
copy.a = 100;
copy.b.c = 200;
console.log(obj); // { a: 1, b: { c: 200 } }
console.log(copy); // { a: 100, b: { c: 200 } }
数组乱序(洗牌)
实现
function shuffle(arr) {
const res = arr.slice();
for (let i = res.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[res[i], res[j]] = [res[j], res[i]];
}
return res;
}
数组判断相等
严格顺序比较(元素和顺序都必须相同)
function arraysEqual(arr1, arr2) {
if (arr1.length !== arr2.length) return false;
return arr1.every((item,index) => value === arr2[index]);
}
console.log(arraysEqual([1, 2, 3], [1, 2, 3])); // true
console.log(arraysEqual([1, 2, 3], [3, 2, 1])); // false
无序比较(元素相同但顺序可以不同)
function arraysEqual(arr1, arr2) {
if (arr1.length !== arr2.length) return false;
return arr1.slice().sort().every((item, index) => arr2.slice().sort()[index]);
}
深度比较(多维数组/对象)
function arraysEqual(arr1, arr2) {
return JSON.stringfy(arr1) === JSON.stringfy(arr2);
}
console.log(arraysEqual([1, [2, 3]], [1, [2, 3]])); // true
console.log(arraysEqual([1, [2, 3]], [1, [3, 2]])); // false
- 使用递归判断
function arraysEqual(x, y) {
if (x === y) return true;
if (Array.isArray(x) && Array.isArray(y)) {
if (x.length !== y.length) return false;
return x.every((item, index) => arraysEqual(item, y[index]));
}
if (x && y && typeof x === 'object' && typeof y ==== 'object') {
const keyX = Object.keys(x);
const keyY = Object.keys(y);
if (keyX.length !== keyY.length) return false;
return keyX.every(key => arraysEqual(x[key], y[key]));
}
return false;
}
console.log(arraysEqual([1, [2, 3]], [1, [2, 3]])); // true
console.log(arraysEqual({ a: 1, b: [2, 3] }, { a: 1, b: [2, 3] })); // true
多维数组求和
// 一维
const arr = [1, 2, 3, 4, 5];
const sum = arr.reduce((acc, cur) => arr + cur, 0);
// 扁平化
const arr1 = [1, [2, 3], [4, [5]]];
const sum1 = arr1.flat(Infinity).reduce((acc, cur) => arr + cur, 0);
//递归
const arr2 = [1, [2, 3], [4, [5]]];
function deepSum(arr) {
let sum = 0;
for (let a of arr) {
if (Array.isArray(a)) {
sum += deepSum(a);
} else {
sum += a;
}
}
return sum;
}
// 递归
const arr3 = [1, [2, 3], [4, [5]]];
const sum3 = arr3.reduce((acc, cur) =>{
return acc + (Array.isArray(cur) ? sum3(cur) : cur)
}, 0)
数组去重
-
Set
-
const arr = [1, 2, 2, 3, 3, 4]; const unique = [...new Set(arr)]; console.log(unique); // [1, 2, 3, 4]
-
-
Filter
-
const arr = [1, 2, 2, 3, 3, 4]; const unique = arr.filter((item, index) => arr.indexOf(item) === index); console.log(unique); // [1, 2, 3, 4]
-
数组扁平化
const arr = [1, [2, [3, [4, [5]]]]];
console.log(arr.flat(Infinity));
function flatten(arr) {
return arr.reduce((acc, val) => {
return acc.concat(Array.isArray(val) ? flatten(val) : val);
}, []);
}
console.log(flatten(arr));
function flatten1(arr) {
const res = [];
for (let a in arr) {
if (Array.isArray(a)) {
res = res.concat(flatten1(a));
} else {
res.push(a);
}
}
return res;
}
function flatten2(arr) {
while(arr.some(Array.isArray)) {
arr = [].concat(...arr);
}
return arr;
}
数组push
function myPush(arr, ...items) {
let len = arr.length;
for (let i = 0; i < items.length; i++) {
arr[len] = items[i];
len++;
}
}
const arr = [1, 2, 3];
const newLength = myPush(arr, 4, 5);
console.log(arr); // [1, 2, 3, 4, 5]
console.log(arr.length);
Array.prototype.myPush = function (...items) {
let len = this.length;
for (let i = 0; i < items.length; i++) {
this[len] = items[i];
len++;
}
};
const arr = [1, 2, 3];
arr.myPush(4, 5);
console.log(arr); // [1, 2, 3, 4, 5]
数组filter
稀疏数组:某些索引上没有元素或者没有赋值
[1,,3]
function myFilter(arr, callback, thisArg) {
const res = [];
for (let i = 0; i < arr.length; i++) {
// 跳过稀疏数组中没有值的地方
if (i in arr) {
const item = arr[i];
if (callback.call(thisArg, item, i, arr)) {
res.push(item);
}
}
}
return res;
}
const arr = [1, 2, 3, 4, 5];
const filtered = myFilter(arr, x => x > 2);
console.log(filtered); // [3, 4, 5]
数组map
Array.prototype.myMap = function (callback, thisArg) {
if (typeof callback !== 'function') {
throw new TypeError(callback + 'is not a function');
}
const arr = this;
const result = [];
for (let i = 0; i < arr.length; i++) {
// 跳过稀疏数组中没有值的地方
if (i in arr) {
result[i] = callback.call(thisArg, arr[i], i, arr);
}
}
return result;
};
const arr = [1, 2, 3];
const newArr = arr.myMap((item, index) => item * 2);
console.log(newArr); // [2, 4, 6]
数组reduce
Array.prototype.myReduce = function (callback, initialValue) {
if (typeof callback !== 'function') {
throw new TypeError(callback + ' is not a function');
}
const arr = this;
let accumulator;
let startIndex;
if (arguments.length > 2) {
accumulator = initialValue;
startIndex = 0;
} else {
let i = 0;
while (i < arr.length && !(i in arr)) {
i++;
}
if (i >= arr.length) {
throw new TypeError('Reduce of empty array with no initial value');
}
accumulator = arr[i];
startIndex = i + 1;
}
for (let i = startIndex; i < arr.length; i++) {
if (i in arr) {
accumulator = callback(accumulator, arr[i], i, arr);
}
}
return accumulator;
};
const arr = [1, 2, 3, 4];
const sum = arr.myReduce((acc, cur) => acc + cur, 0);
console.log(sum); // 10
数组转树
-
递归
-
const arr = [ { id: 1, parentId: 0, name: 'A' }, { id: 2, parentId: 1, name: 'B' }, { id: 3, parentId: 1, name: 'C' }, { id: 4, parentId: 2, name: 'D' }, { id: 5, parentId: 0, name: 'E' }, ]; function buildTree(arr, parentId = 0) { return arr .filter(item => item.parentId === parentId) .map(item => ({ ...item, children: buildTree(arr, item.id), })); } const tree = buildTree(arr); console.log(JSON.stringify(tree, null, 2));
-
-
map
-
function buildTree(arr) { const map = new Map(); const tree = []; arr.forEach(item => map.set(item.id, { ...item, children: [] })); arr.forEach(item => { const node = map.get(item.id); if (item.parentId === 0) { tree.push(node); } else { const parent = map.get(item.parentId); if (parent) { parent.children.push(node); } } }); return tree; } const tree = buildTree(arr); console.log(JSON.stringify(tree, null, 2));
-
对象迭代器
用法
JS中for..of、扩展运算符...、数组解构
实现
const myIterable = {
data: ['a','b','c'],
[Symbol.iterator]() {
let index = 0;
const arr = this.data;
return {
next() {
if (index < arr.length) {
return { value: arr[index++], done: false };
} else {
return { value: undefined, done: true}
}
}
}
}
}
for (const v of myIterable) {
console.log(v); // a b c
}
console.log([...myIterable]); // ['a','b','c']
const obj = {
name: 'dy',
age: 18,
city: 'bj',
[Symbol.iterator]() {
const entrues = Object.entries(this);
let index = 0;
return {
next() {
if (index < entrues.length) {
return { value: entrues[index++], done: false };
} else {
return { value: undefined, done: true };
}
},
};
},
};
for (const [key, value] of obj) {
console.log(key, value);
}
JSONP
原理
- 一种跨域请求数据的方式,基于
- 服务器返回的不是纯JSON,而是被一个回调函数包裹的js代码
- 前端通过定义一个全局函数作为回调,在
实现
function jsonp(url, params = {}, callbackName = 'callback') {
return new Promise((resolve, reject) => {
const cbName =
'jsonp_' + Date.now() + '_' + Math.floor(Math.random() * 10000);
params[callbackName] = cbName;
const queryString = Object.keys(params)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
.join('&');
const script = document.createElement('script');
script.src = `${url}?${queryString}`;
window[cbName] = data => {
resolve(data);
document.body.removeChild(script);
delete window[cbName];
};
script.onerror = () => {
reject(new Error(`JSONP request to ${url} failed`));
document.body.removeChild(script);
delete window[cbName];
}
});
}
封装ajax
function ajax(options) {
const {
url,
method = 'GET',
data = null,
async = true,
headers = {},
timeout = 0,
} = options;
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
let finalUrl = url;
if (method.toUpperCase() === 'GET' && data) {
const params = new URLSearchParams(data).toString();
finalUrl += (url.includes('?') ? '&' : '?') + params;
}
xhr.open(method, finalUrl, async);
for (let key in headers) {
xhr.setRequestHeader(key, headers[key]);
}
if (timeout) {
xhr.timeout = timeout;
}
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
try {
resolve(JSON.parse(xhr.responseText));
} catch (e) {
resolve(xhr.responseText);
}
} else {
reject(new Error(`Request failed with status ${xhr.status}`));
}
}
};
xhr.ontimeout = () => reject(new Error('Request timed out'));
xhr.onerror = () => reject(new Error('Network error'));
if (method.toUpperCase() === 'POST') {
if (!(data instanceof FormData)) {
xhr.setRequestHeader(
'Content-Type',
'application/x-www-form-urlencoded'
);
xhr.send(new URLSearchParams(data).toString());
} else {
xhr.send(data);
}
} else {
xhr.send();
}
});
}
双向绑定
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div>
<input type="text" id="input" />
<p id="text"></p>
</div>
<script>
const data = {};
Object.defineProperty(data, 'message', {
get() {
return this._message;
},
set(val) {
this._message = val;
document.getElementById('text').textContent = val;
document.getElementById('input').value = val;
},
});
const input = document.getElementById('input');
input.addEventListener('input', function (e) {
data.message = e.target.value;
});
data.message = 'Hello, 双向绑定!';
</script>
</body>
</html>
- proxy版
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div>
<input type="text" id="input2" />
<p id="text2"></p>
</div>
<script>
const input2 = document.getElementById('input2');
const text2 = document.getElementById('text2');
const state = {
message: '',
};
const data2 = new Proxy(state, {
set(target, key, value) {
target[key] = value;
if (key === 'message') {
text2.textContent = value;
input2.value = value;
}
return true;
},
get(target, key) {
return target[key];
},
});
input2.addEventListener('input', e => {
data2.message = e.target.value;
});
data2.message = 'Hello';
</script>
</body>
</html>
发布订阅
发布-订阅模式(Publish–Subscribe Pattern)是一种常见的设计模式,常用于模块解耦。
- 订阅者(Subscriber):对某个事件感兴趣,注册回调函数。
- 发布者(Publisher):当事件发生时,发布(触发)消息通知所有订阅者。
class EventBus {
constructor() {
this.events = {};
}
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(callback);
}
emit(eventName, ...args) {
if (this.events[eventName]) {
this.events[eventName].forEach(callback => callback(...args));
}
}
off(eventName, callback) {
if (!this.events[eventName]) return;
this.events[eventName] = this.events[eventName].filter(
cb => cb !== callback
);
}
once(eventName, callback) {
const onceWrapper = (...args) => {
callback(...args);
this.off(eventName, onceWrapper);
};
this.on(eventName, onceWrapper);
}
}
请求并发
-
Promise.all实现
- 无法控制并发数,一次性全发
-
function request(url, delay) { return new Promise(resolve => { setTimeout(() => { console.log(`请求${url}成功`); resolve(url); }, delay); }); } const urls = [ request('api/1', 1000), request('api/2', 2000), request('api/3', 1500), ]; Promise.all(urls) .then(results => { console.log('全部完成', results); }) .catch(err => console.error('出错', err));
-
限制并发数量
-
function request(tasks, limit = 3) { const results = []; let index = 0; let active = 0; return new Promise((resolve, reject) => { function next() { if (index === tasks.length && active === 0) { resolve(results); return; } } while (active < limit && index < tasks.length) { const currentIndex = index; const task = tasks[currentIndex]; index++; active++; task() .then(res => { results[currentIndex] = res; }) .catch(err => { results[currentIndex] = err; }) .finally(() => { active--; next(); }); } next(); }); } -
async function fetchWithLimit(tasks, limit = 3) { const pool = new Set(); const results = []; for (const task of tasks) { const p = task().then(result => { pool.delete(p); return result; }); pool.add(p); if (pool.size >= limit) { await Promise.race(pool); } results.push(p); } return Promise.all(results); }
-
Case
题目 — 实现有并发限制的 Promise 池
请你实现一个函数
promisePool(tasks, limit),用于并发执行一组返回Promise的函数(任务),限制最多同时运行limit个任务。要求如下:要求
tasks是一个函数数组,每个函数调用后返回一个Promise(例如:() => fetch(url))。- 最多同时运行
limit个任务(limit为正整数)。- 返回值是一个
Promise,当所有任务都完成(全部 resolve)时,返回Promiseresolve,值为和tasks顺序对应的结果数组。- 若任一任务 reject,则
promisePool应立即 reject(并取消/不再启动后续任务),reject 的原因为该任务的 error。- 保留任务的顺序 —— 返回数组中第
i项是第i个任务的结果(或按上面第4点直接 reject)。
const wait =
(t, val, shouldReject = false) =>
() =>
new Promise((resolve, reject) =>
setTimeout(() => (shouldReject ? reject(val) : resolve(val)), t)
);
const tasks = [wait(300, 'A'), wait(200, 'B'), wait(100, 'C'), wait(400, 'D')];
promisePool(tasks, 2).then(console.log).catch(console.error);
// 运行顺序示意(并发最多2个):启动 tasks[0] & tasks[1],随后当某个完成则启动 tasks[2],以此类推。
// 最终输出(resolve): ['A', 'B', 'C', 'D']
function promisePool(tasks, limit) {
return new Promise((resolve, reject) => {
let n = tasks.length;
let result = new Array(n);
let started = 0; // 已经启动的task数
let finished = 0; //已完成的task数
let active = 0; // 正在运行的task数
let stop = false; // 是否有reject
function nextTask() {
if (n === finished) {
return resolve(result);
}
while (!skytop && started < n && active < limit) {
let idx = started++;
active++;
let p;
try {
p = tasks[idx]();
} catch (e) {
stop = true;
return reject(e);
}
Promise.resolve(p)
.then(val => {
result[idx] = val;
active--;
finished++;
nextTask();
})
.catch(e => {
if (!stop) {
stop = true;
return reject(e);
}
});
}
}
nextTask();
});
}
交替亮灯
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
.lights {
display: flex;
gap: 20px;
margin-top: 40px;
justify-content: center;
}
.lamp {
width: 60px;
height: 60px;
border-radius: 50%;
background: #333;
transition: background 0.3s, transform 0.2s;
}
.lamp.active {
background: gold;
transform: scale(1.1);
}
</style>
</head>
<body>
<div class="lights">
<div class="lamp"></div>
<div class="lamp"></div>
<div class="lamp"></div>
</div>
<script>
const lamps = document.querySelectorAll('.lamp');
const delay = ms => new Promise(res => setTimeout(res, ms));
function setActive(index) {
lamps.forEach((lamp, i) =>
lamp.classList.toggle('active', i === index)
);
}
async function run() {
let index = 0;
while (true) {
setActive(index);
await delay(500);
index = (index + 1) % lamps.length;
}
}
run();
</script>
</body>
</html>
哈希路由
利用URL的#来实现前端路由,#后面的内容(hash)不会被浏览器发送到服务器
class HashRouter {
constructor() {
this.routes = {};
window.addEventListener('hashchange', this.load.bind(this));
window.addEventListener('load', this.load.bind(this));
}
register(path, callback) {
this.routes[path] = callback || function () {};
}
load() {
const path = location.hash.slice(1) || '/';
if (this.routes[path]) {
this.routes[path]();
} else {
console.log('404');
}
}
}
判断对象是否循环引用
循环引用:对象间相互引用形成一个环
const obj = {};
obj.self = obj; // obj 自己引用自己
function hasCircularReference(obj, seen = new WeakSet()) {
if (obj && typeof obj === 'object') {
if (seen.has(obj)) {
return true;
}
seen.add(obj);
for (const key in obj) {
if (Object.hasOwn(obj, key)) {
if (hasCircularReference(obj[key], seen)) {
return true;
}
}
}
}
return false;
}