前端面试-手撕题

1. 手撕实现防抖节流函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 防抖函数 n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时
* @param {Function} func - 要执行的函数
* @param {number} wait - 等待时间(毫秒)
* @returns {Function} - 防抖包装后的函数
*/
function debounce(func, wait) {
let timer = null;

return function executedFunction(...args) {
if(timer){
clearTimeout(timer);
}
timer = setTimeout(() => {
func.apply(this, args);
},wait);
};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 节流函数(定时器版本)n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效。
* @param {Function} func - 要执行的函数
* @param {number} wait - 最小时间间隔(毫秒)
* @returns {Function} - 节流包装后的函数
*/
function throttle(func, wait) {
let timeout = null;

return function throttledFunction(...args) {
// 如果没有定时器,则设置一个
if (!timeout) {
timeout = setTimeout(() => {
func.apply(this, args);
timeout = null; // 执行后清除
}, wait);
}
};
}

2. 用 setTimeout 实现“每秒执行一次”的任务

  • 方法一:递归 setTimeout(推荐)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function runEverySecond() {
    console.log('执行任务:', new Date().toLocaleTimeString());

    // 任务完成后,再设置下一次执行
    setTimeout(runEverySecond, 1000);
    }

    // 启动
    runEverySecond();

3.手撕instanceof

核心:获取对象的原型,判断对象的原型是否等于构造函数的 prototype 对象。原型链的向上查找。

1
2
3
4
5
6
7
8
9
10
11
12
function myInstanceof(left, right) {
if (typeof left !== 'object' || left === null) return false;
// getProtypeOf是Object对象自带的API,能够拿到参数的原型对象
let proto = Object.getPrototypeOf(left);
while (true) {
if (proto === null) return false;
if (proto === right.prototype) return true;
proto = Object.getPrototypeOf(proto);
}
}
console.log(myInstanceof('111', String)); //false
console.log(myInstanceof(new String('111'), String)); //true

4.手撕new

new操作符用于创建一个给定构造函数的实例对象

原理流程:

  • 创建一个新的对象obj。
  • 将对象与构建函数通过原型链连接起来。
  • 将构建函数中的this绑定到新建的对象obj上。
  • 根据构建函数返回类型作判断,如果是原始值则被忽略,如果是返回对象,需要正常处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function myNew(func, ...args){
//1. 创建一个空对象
const obj = {};
//2. 新对象原型指向构造函数原型对象
obj.__proto__ = func.prototype;
//3. 将构造函数的this指向新建对象
let result = func.apply(obj, args);
return result instanceof Object ? result : obj;
}

//测试
function Person(name, age) {
this.name = name;
this.age = age;
}

let p =myNew(Person, '张三', 18);
console.log(p);

5.手撕call\apply\bind

call\apply\bind都是函数对象自带的,作用是改变函数执行时的上下文,简而言之就是改变函数运行时的this指向。
他们三个的区别:

特性 call apply bind
立即执行 ✅ 是 ✅ 是 ❌ 否(返回新函数)
参数传递方式 列表形式:fn.call(obj, arg1, arg2) 数组形式:fn.apply(obj, [arg1, arg2]) 列表形式:fn.bind(obj, arg1, arg2)
改变 this 指向
常用于 一次性调用,传参明确 一次性调用,参数在数组中 需要延迟执行或保存上下文
  1. call

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    Function.prototype.myCall = function (context, ...args) {
    context = (context!== null && context !== undefined)? Object(context) : window;
    const key = Symbol('key');
    context[key] = this;
    const result = context[key](...args);
    delete context[key];
    return result;
    }

    function greet(greeting, punctuation) {
    console.log(`${greeting}, I'm ${this.name}${punctuation}`);
    }
    const person = { name: 'Alice' };
    greet.myCall(person,'Hi', '~~');
    // 输出: Hi, I'm Alice~~
  2. apply

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    Function.prototype.myApply = function (context, argsArray) {
    context = (context!== null && context !== undefined)? Object(context) : window;
    const key = Symbol('key');
    context[key] = this;
    const result = context[key](...argsArray);
    delete context[key];
    return result;
    }

    function greet(greeting, punctuation) {
    console.log(`${greeting}, I'm ${this.name}${punctuation}`);
    }
    const person = { name: 'Alice' };
    greet.myApply(person,['Hi', '~~']);
    // 输出: Hi, I'm Alice~~
  3. bind

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    Function.prototype.myBind = function (context, ...args) {
    const fn = this;
    return function (...args2) {
    if(new.target){
    return new fn(...args, ...args2);
    }
    return fn.call(context, ...args, ...args2);
    };
    }

    function greet(greeting, punctuation) {
    console.log(`${greeting}, I'm ${this.name}${punctuation}`);
    }
    const person = { name: 'Alice' };
    const newFn = greet.myBind(person,'Hi');
    newFn('~~');
    // 输出: Hi, I'm Alice~~

6.手撕 浅拷贝 深拷贝

浅拷贝

只复制对象的第一层属性,如果属性是引用类型(如数组、对象),则复制的是引用地址,而非新对象。

  • 手写浅拷贝函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function shallowCopy(obj) {
    if (typeof obj !== 'object' || obj === null) {
    return obj;
    }

    // 判断是数组还是对象
    const result = Array.isArray(obj) ? [] : {};

    // 遍历所有可枚举属性(包括自有属性)
    for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
    result[key] = obj[key];
    }
    }

    return result;
    }
  • 使用内置方法(等效浅拷贝)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 对象
    const obj1 = { a: 1, b: { c: 2 } };
    const obj2 = Object.assign({}, obj1);
    // 或
    const obj3 = { ...obj1 };

    // 数组
    const arr1 = [1, 2, { x: 3 }];
    const arr2 = arr1.slice();
    // 或
    const arr3 = [...arr1];

深拷贝

递归复制对象的所有层级,所有引用类型都会创建新的副本,完全断开与原对象的联系。

  • 手写深拷贝函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function deepCopy(obj) {
    if (typeof obj !== 'object' || obj === null) {
    return obj;
    }

    // 判断是数组还是对象
    const result = Array.isArray(obj) ? [] : {};

    // 遍历所有可枚举属性(包括自有属性)
    for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
    result[key] = deepCopy(obj[key]);
    }
    }

    return result;
    }
  • 手写深拷贝函数(处理循环引用)

    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 deepCopy(obj) {
    const cache = new Map();
    function _deepCopy(obj) {
    if (typeof obj !== 'object' || obj === null) {
    return obj;
    }
    if (cache.has(obj)) {
    return cache.get(obj);
    }

    // 判断是数组还是对象
    const result = Array.isArray(obj) ? [] : {};
    cache.set(obj, result);
    // 遍历所有可枚举属性(包括自有属性)
    for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
    result[key] = deepCopy(obj[key]);
    }
    }
    return result;
    }
    return _deepCopy(obj);

    }
  • 使用 JSON.parse(JSON.stringify())(有局限)

    缺点

    • 会忽略 undefined、Symbol、函数
    • 无法处理 Date(变成字符串)
    • 无法处理 RegExp、Error 等特殊对象
    • 循环引用会报错
    1
    const deepCopy = JSON.parse(JSON.stringify(obj));

7. 手写Ajax请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function ajax(url) {
return new PrOmise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url,true);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(xhr.responseText);
}
else {
reject(xhr.statusText);
}
}
}
xhr.send();
})
}

8. 手写数组去重

  1. 使用 Set

    1
    2
    3
    4
    5
    6
    7
    function unique1(array){
    return Array.from(new Set(array))
    }
    function unique2(array){
    return [...new Set(array)]
    }

  2. 使用filter

    1
    2
    3
    4
    5
    function unique1(array){
    return Array.filter((item,index)=>{
    return array.indexOf(item)===index
    })
    }

9. 手写数组扁平化

1
2
3
4
5
6
7
8
9
10
11
12
13
function flatten(array) {
const result = [];

for(let i=0;i<array.length;i++){
if(Array.isArray(array[i])){
result.push(...flatten(array[i]));
}
else{
result.push(array[i]);
}
}
return result;
}

10. 手写promise系列

1.构造器

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
class MyPromise {
#state = 'pending';
#result = undefined;
constructor(executor) {
const resolve = (value) => {
// if (this.#state !== 'pending') return;
// this.#state = 'fulfilled';
// this.#result = value;
this.#changeState('fulfilled', value);
};
const reject = (reason) => {
// if (this.#state !== 'pending') return;
// this.#state = 'rejected';
// this.#result = reason;
this.#changeState('rejected', reason);
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
#changeState(state, result) {
if (this.#state !== 'pending') return;
this.#state = state;
this.#result = result;
}
}
const promise = new MyPromise((resolve, reject) => {
resolve(1);
reject(2);
})

2.手写then方法

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
class MyPromise {
#state = 'pending';
#result = undefined;
constructor(executor) {
const resolve = (value) => {
this.#changeState('fulfilled', value);
};
const reject = (reason) => {
this.#changeState('rejected', reason);
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
#changeState(state, result) {
if (this.#state !== 'pending') return;
this.#state = state;
this.#result = result;
}
then(onFulfilled,onRejected){
return new MyPromise((resolve, reject) => {
if(this.#state === 'fulfilled'){
resolve(onFulfilled(this.#result));
}
else if(this.#state === 'rejected'){
reject(onRejected(this.#result));
}
else{
//这里要考虑then的链式调用,以及异步
}
})
}_
}
const p = new MyPromise((resolve, reject) => {
resolve(1);
reject(2);
})
p.then((res) => {},(res) => {})

思路:
上述代码存在难以处理异步的问题,因为异步等待是挂起状态。
then

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
class MyPromise {
#state = 'pending';
#result = undefined;
#handlers = [];
constructor(executor) {
const resolve = (value) => {
this.#changeState('fulfilled', value);
};
const reject = (reason) => {
this.#changeState('rejected', reason);
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
#isPromise(value){
if(value!==null && (typeof value === 'object' || typeof value.then === 'function')){
return typeof value.then === 'function';
}
return false;
}
#changeState(state, result) {
if (this.#state !== 'pending') return;
this.#state = state;
this.#result = result;
this.#run();
}
#runOne(callback,resolve,reject){
//替换run中的复用
if(typeof callback !== 'function'){
const settled = this.#state === 'fulfilled' ? resolve : reject;
settled(this.#result);
return ;
}
try{
const data = callback(this.#result);
resolve(data);
} catch (error) {
reject(error);
}
}
#run(){
if(this.#state === 'pending') return;
while(this.#handlers.length){
const {onFulfilled,onRejected,resolve,reject} = this.#handlers.shift();
if(this.#state === 'fulfilled'){
if(typeof onFulfilled === 'function'){
try{
const data = onFulfilled(this.#result);
resolve(data);
} catch (error) {
reject(error);
}
}
else resolve(this.#result);
}
else if(this.#state === 'rejected'){
if(typeof onRejected === 'function'){
try{
const data = onRejected(this.#result);
resolve(data);
} catch (error) {
reject(error);
}
}
else reject(this.#result);
}
}
}
then(onFulfilled,onRejected){
return new MyPromise((resolve, reject) => {
this.#handlers.push({onFulfilled,onRejected,resolve,reject});
this.#run();
})
}
}
const p = new MyPromise((resolve, reject) => {
resolve(1);
reject(2);
})
p.then((res) => {},(res) => {})

11. 手写promise catch finally 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
then(onFulfilled,onRejected){
return new MyPromise((resolve, reject) => {
this.#handlers.push({onFulfilled,onRejected,resolve,reject});
this.#run();
})
}
catch(onRejected){
return this.then(null,onRejected);
}
finally(onFinally){
return this.then((data)=>{
onFinally();
return data;
},(err)=>{
onFinally();
throw err;
});
}

12. 手写promise resolve reject 方法

resolvereject 方法,其实有两种不同的语境:

  1. 作为 Promise 构造函数的参数
  2. 作为 Promise 类上的静态方法Promise.resolve()Promise.reject()

Promise.resolve(value) 是一个 静态方法,它返回一个 已解决(fulfilled)状态Promise 对象
Promise.reject(reason) 是一个 静态方法,它返回一个 已拒绝(rejected)状态 的 Promise 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static resolve(value){
if(value instanceof MyPromise) return value;
let _resolve,_reject;
const p = new MyPromise((resolve, reject) => {
_resolve = resolve;
_reject = reject;
})
if(p.#isPromiseLike(value)){
value.then(_resolve,_reject);
} else{
_resolve(value);
}
return p;
}

static reject(reason){
return new MyPromise((resolve, reject) => {
reject(reason);
})
}

12. 手写 Promise.all 并讲解思路

Promise.all(iterable) 接收一个可迭代对象(通常是数组),返回一个新的 Promise:

  • 全部成功:当所有 Promise 都成功时,返回的 Promise 才成功,结果是按顺序排列的成功值数组
  • 任意失败:只要有一个 Promise 失败,返回的 Promise 就立即失败,原因为第一个失败的原因。
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
function myPromiseAll(promises){
return new Promise((resolve, reject) => {
if(!Array.isArray(promises)){
return reject(new TypeError('arguments must be an array'));
}
const result = [];
const complete = 0; // 已完成计数器
for(let i = 0; i < promises.length; i++){
Promise.resolve(promises[i])
.then(data => {
result[i] = data;
complete++;
// 判断是否全部完成
if(complete === promises.length){
resolve(result);
}
})
.catch(err => {
// 只要有一个失败,就返回失败
reject(err);
})
}
})

}

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!