13个 JavaScript 面试难题及代码实现
共 14200字,需浏览 29分钟
·
2024-07-14 21:51
今天我将带你深入了解 14 个常见的 JavaScript 高级面试问题。这些问题涵盖了 JavaScript 的面向对象、事件循环机制、Promise 等高级概念,以及函数柯里化和深度复制等实用技术。
我们不仅从概念层面对每个问题进行了分析,还提供了具体的代码实现。
那我们现在就开始吧。
1.this关键字指向
this关键字指向当前执行上下文中的一个对象。在函数中,this关键字通常指向函数的调用者。
问题:以下代码输出什么?为什么?
const obj = {
name: 'obj',
getName: function() {
return function() {
return this.name;
}
}
}
const fn = obj.getName();
fn();
答案:undefined
分析:因为getName函数内部是在全局作用域内执行的,这里的this指向window/global,而window/global没有name属性,所以返回undefined。
如果想让内部函数的this也指向obj,可以使用箭头函数或者bind绑定this:
const obj = {
name: 'obj',
getName: function() {
return () => {
return this.name;
}
}
}
2.闭包的实现与应用
问题:实现一个计数器工厂函数:
function createCounter() {
let count = 0;
return function() {
return count++;
}
}
const counter1 = createCounter();
const counter2 = createCounter();
counter1(); // 1
counter1(); // 2
counter2(); // 1
解析:不同的计数器之所以能独立递增,是因为利用了闭包的特性。createCounter函数创建了一个闭包,在其外层作用域中可以访问变量count,counter1和counter2引用不同的闭包函数实例,从而实现了计数独立性。
3.事件循环机制
问:对事件循环机制做一下解释说明。
答:事件循环机制主要有以下几个流程:
同步任务在主线程执行,形成执行上下文栈。
执行栈中同步任务执行完后,系统会读取队列中的异步任务,如Promise.then()、setTimeout、AJAX回调等。
异步任务会被加入到任务队列中
清空执行栈后,系统会检查任务队列,如果不为空,则取出第一个任务放到执行栈中执行。
主线程重复了栈和队列交替执行的过程,从而实现了线程的排队执行。
事件循环允许在同一个线程中交替执行同步任务和异步任务,充分利用CPU资源。这对于支持UI交互和响应性的JavaScript来说很重要。
4.Promise对象
问题:实现Promise的简单版本:
Promise对象是处理异步事件的异步编程解决方案。Promise对象可以表示异步操作的状态,包括:
pending
fulfilled
rejected
Promise对象的实现如下:
class MyPromise {
constructor(executor) {
this._state = "pending";
this._value = undefined;
this._reason = undefined;
this._onFulfilledCallbacks = [];
this._onRejectedCallbacks = [];
executor(this.resolve.bind(this), this.reject.bind(this));
}
resolve(value) {
if (this._state !== "pending") {
return;
}
this._state = "fulfilled";
this._value = value;
setTimeout(() => {
for (const callback of this._onFulfilledCallbacks) {
callback(value);
}
});
}
reject(reason) {
if (this._state !== "pending") {
return;
}
this._state = "rejected";
this._reason = reason;
setTimeout(() => {
for (const callback of this._onRejectedCallbacks) {
callback(reason);
}
});
}
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
if (this._state === "pending") {
this._onFulfilledCallbacks.push((value) => {
setTimeout(() => {
try {
const result = onFulfilled(value);
resolve(result);
} catch (error) {
reject(error);
}
});
});
this._onRejectedCallbacks.push((reason) => {
setTimeout(() => {
try {
const result = onRejected(reason);
resolve(result);
} catch (error) {
reject(error);
}
});
});
} else {
setTimeout(() => {
try {
if (this._state === "fulfilled") {
const result = onFulfilled(this._value);
resolve(result);
} else {
const result = onRejected(this._reason);
resolve(result);
}
} catch (error) {
reject(error);
}
});
}
});
}
catch(onRejected) {
return this.then(null, onRejected);
}
isFulfilled() {
return this._state === "fulfilled";
}
isRejected() {
return this._state === "rejected";
}
}
分析:
MyPromise类是一个自定义的Promise类,其构造函数接受一个执行函数作为参数。
构造函数中的执行函数会被立即执行,并接受两个参数resolve和reject,用于修改Promise的状态。
resolve方法用于将Promise的状态从“pending”修改为“fulfilled”,并将值传递给后续的handler。
reject方法用于将Promise的状态从“pending”修改为“rejected”,并将原因传递给后续的handler。
then方法用于注册一个回调函数,在Promise完成或被拒绝时执行,它接受两个参数onFulfilled和onRejected,分别在Promise完成或被拒绝时调用。
then方法返回一个新的MyPromise实例,以支持链式调用。如果 onFulfilled 或 onRejected 返回一个值,它将被用作下一个 MyPromise 实例的解析值。
catch 方法是 then(null, onRejected) 的简写形式。
isFulfilled 方法用于检查 Promise 是否处于已实现状态。
isRejected 方法用于检查 Promise 是否处于已拒绝状态。
5.类继承实现
原型链是每个对象的一个属性,它指向该对象构造函数的原型对象。一个构造函数的原型对象指向另一个构造函数的原型对象,依此类推。
问题:要实现一个 People 类,可以通过构造函数或 new 操作符实例化对象。同时,它有一个继承 Person 类的方法。Person 类有一个 sayHi 方法:
class Person {
constructor(name) {
this.name = name;
}
sayHi() {
console.log(`Hello ${this.name}`)
}
}
class People extends Person {
constructor(name) {
super(name);
}
method() {
console.log('people method')
}
}
const people = new People('John')
people.sayHi() // Hello John
people.method() // people method
分析:通过构造函数调用super继承属性,原型链实现方法继承。
6.MVC与MVVM模式
问:简述MVC与MVVM的概念和区别?
答:MVC模式中:
Model负责管理数据逻辑
View负责显示界面
Controller连接Model与View并传递数据
MVVM模式中:
Model负责管理数据逻辑
View负责显示界面
ViewModel作为View与Model的交互代理,将Model同步到View,将View的变化同步回Model。
区别在于:
MVVM中没有Controller的角色,View直接将数据绑定到ViewModel。
ViewModel负责将数据转换成View可以识别的格式,提供给View使用。
ViewModel可以将View的变化通知回Model层,实现双向数据绑定。
MVVM可以解耦View与Model之间的紧耦合,利于单元测试和组件开发。
7.Ajax实现
问题:实现一个ajax请求函数:
ajax('/api/users', {
method: 'GET'
})
.then(data => {
console.log(data)
})
回答:
function ajax(url, options) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const method = options.method || 'GET';
const headers = options.headers || {};
const body = options.body || null;
const timeout = options.timeout || 0;
xhr.open(method, url);
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response);
} else {
reject(new Error(xhr.statusText));
}
};
xhr.onerror = () => reject(xhr.error);
xhr.ontimeout = () => reject(new Error('Request timeout'));
xhr.timeout = timeout;
for (const [header, value] of Object.entries(headers)) {
xhr.setRequestHeader(header, value);
}
xhr.send(body);
});
}
实现一个支持Promise的ajax请求函数:
使用XMLHttpRequest对象发送请求
初始化open方法,配置请求方法和url
添加onload和onerror回调函数
onload判断状态码是否在200–300范围内resolve,否则reject
onerror直接reject
请求成功后resolve返回response,失败后reject报错。
支持options配置请求参数和请求体
返回一个Promise对象,可以使用then/catch进行外部处理
分析:使用Promise封装异步ajax请求,实现同步编程风格。
8.JSONP跨域实现
问题:实现一个JSONP跨域请求:
jsonp('/api/data', {
params: {
name: 'jsonp'
}
})
回答:
function jsonp(url, options) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
const callbackName = `jsonpCallback_${Date.now()}_${Math.floor(Math.random() * 10000)}`;
const timer = setTimeout(() => {
cleanup();
reject(new Error('JSONP request timeout'));
}, options.timeout || 5000);
function cleanup() {
delete window[callbackName];
clearTimeout(timer);
script.remove();
}
window[callbackName] = function(data) {
cleanup();
resolve(data);
};
options.params = options.params || {};
options.params['callback'] = callbackName;
const paramsArr = Object.keys(options.params).map(key => {
return `${encodeURIComponent(key)}=${encodeURIComponent(options.params[key])}`;
});
script.src = `${url}?${paramsArr.join('&')}`;
script.onerror = () => {
cleanup();
reject(new Error('JSONP request error'));
};
document.body.appendChild(script);
});
}
分析:创建脚本节点script.src,设置回调函数callbackName,解析参数并拼接URL,动态插入body中实现JSONP跨域请求,返回Promise接口。
9.实现深度克隆
问题:实现一个函数deepClone,实现对象的深度克隆:
答案:
function deepClone(source, clonedMap) {
clonedMap = clonedMap || new Map();
if (source === null || typeof source !== 'object') {
return source;
}
if (clonedMap.has(source)) {
return clonedMap.get(source);
}
var result;
var type = getType(source);
if (type === 'object' || type === 'array') {
result = type === 'array' ? [] : {};
clonedMap.set(source, result);
for (var key in source) {
if (source.hasOwnProperty(key)) {
result[key] = deepClone(source[key], clonedMap);
}
}
} else {
result = source;
}
return result;
}
function getType(source) {
return Object.prototype.toString
.call(source)
.replace(/^\[object (.+)\]$/, '$1')
.toLowerCase();
}
const obj = {
a: 1,
b: {
c: 2
}
}
const clone = deepClone(obj)
分析:递归实现对象和数组的深度克隆,直接返回基本类型,引用类型递归分层调用深度克隆。
10.函数柯里化
问题:实现一个可以把1+2+3加起来的add函数:
function add() {
// When executing for the first time, define an array specifically to store all parameters
var _args = [].slice.call(arguments);
// Declare a function internally, use the characteristics of closure to save _args and collect all parameter values
var adder = function () {
var _adder = function() {
// [].push.apply(_args, [].slice.call(arguments));
_args.push(...arguments);
return _adder;
};
//Using the characteristics of implicit conversion, implicit conversion is performed when it is finally executed, and the final value is calculated and returned.
_adder.toString = function () {
return _args.reduce(function (a, b) {
return a + b;
});
}
return _adder;
}
// return adder.apply(null, _args);
return adder(..._args);
}
var a = add(1)(2)(3)(4); // f 10
var b = add(1, 2, 3, 4); // f 10
var c = add(1, 2)(3, 4); // f 10
var d = add(1, 2, 3)(4); // f 10
// You can use the characteristics of implicit conversion to participate in calculations
console.log(a + 10); // 20
console.log(b + 20); // 30
console.log(c + 30); // 40
console.log(d + 40); // 50
// You can also continue to pass in parameters, and the result will be calculated using implicit conversion again.
console.log(a(10) + 100); // 120
console.log(b(10) + 100); // 120
console.log(c(10) + 100); // 120
console.log(d(10) + 100); // 120
//In fact, the add method in Shangli is the curried function of the following function, but we do not use the general formula to convert, but encapsulate it ourselves.
function add(...args) {
return args.reduce((a, b) => a + b);
}
分析:add函数的柯里化是通过递归调用一个不断接受参数的函数来实现的。
11.实现promise.all方法
问题:实现一个myAll方法,类似Promise.all:
myAll([
myPromise1,
myPromise2
]).then(([res1, res2]) => {
//...
})
回答:
function myAll(promises) {
return new Promise((resolve, reject) => {
const result = new Array(promises.length);
let count = 0;
promises.forEach((p, index) => {
p.then(res => {
result[index] = res;
count++;
if (count === promises.length) {
resolve(result);
}
})
.catch(reject);
});
});
}
分析:利用Promise.all原理,通过计数器和结果数组同步Promise状态。
12.实现Instanceof
问题:实现一个instanceof操作符
答案:
function instanceof(left, right) {
if (arguments.length !== 2) {
throw new Error("instanceof requires exactly two arguments.");
}
if (left === null) {
return false;
}
if (typeof left !== "object") {
return false;
}
let proto = Object.getPrototypeOf(left);
while (proto !== null) {
if (right.prototype === proto) {
return true;
}
proto = Object.getPrototypeOf(proto);
}
return false;
}
以上代码用于判断一个对象是否是另一个对象的实例。
JavaScript 中的 instanceof 运算符可用于判断一个对象是否是另一个对象的实例。但是,instanceof 运算符有一些限制,例如:
instanceof 运算符只能确定与原型链直接相连的对象。
instanceof 运算符无法检测具有循环原型链的对象。
因此,以上代码提供了一个更通用的 instanceof 函数,可以确定任意两个对象之间的关系。
该函数的实现原理是:
instanceof 函数接收两个参数:left 和 right。
首先,代码检查参数数量是否为 2,如果不是,则抛出错误。
接下来,代码检查左操作数 left 是否为 null。如果是,则直接返回 false,因为 null 不能是任何对象的实例。
然后,代码检查左操作数 left 的类型是否为对象。如果不是,则直接返回false,因为只有对象才能是构造函数的实例。
接下来,代码使用Object.getPrototypeOf()获取左操作数left的原型,并将其赋值给变量proto。
在循环中,代码继续遍历proto的原型链,直到proto为null。
在循环中,代码检查右操作数right的原型是否等于当前的proto,如果相等,则表示左操作数left是右操作数right的实例,返回true。
如果在循环结束时没有找到匹配的原型,即proto为null,则表示左操作数left不是右操作数right的实例,返回false。
该函数可以在以下场景中使用:
判断一个对象是否是另一个对象的实例。
判断一个对象是否从另一个对象继承而来。
判断一个对象是否属于某个特定的类。
13. 实现 debounce 防抖功能
问题:实现 debounce 防抖功能
答案:
function debounce(fn, delay = 500) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
// Return to clear function
return () => {
clearTimeout(timer);
}
}
}
// use
const debouncedFn = debounce(fn, 1000);
const cancel = debouncedFn();
// clear
cancel();
本文完
学习更多技能
请点击下方公众号