JavaScript 面试题
以下为你提供一些常见的 JavaScript 面试题及答案:
1. 什么是 JavaScript 中的闭包,它有什么作用?
闭包是指有权访问另一个函数作用域中的变量的函数。即使该函数已经执行完毕,其作用域内的变量也不会被销毁,而是会被闭包所引用。
作用:
- 读取函数内部的变量:可以在函数外部访问函数内部的变量。
- 让这些变量的值始终保持在内存中:避免变量在函数执行完毕后被销毁。
示例代码:
function outerFunction() {
let count = 0;
return function innerFunction() {
return ++count;
};
}
const counter = outerFunction();
console.log(counter()); // 输出 1
console.log(counter()); // 输出 2
2. 解释 JavaScript 中的原型链
在 JavaScript 中,每个对象都有一个内部属性 [[Prototype]]
,它指向该对象的原型对象。当访问一个对象的属性或方法时,JavaScript 首先会在该对象本身查找,如果找不到,就会沿着原型链向上查找,直到找到该属性或方法,或者到达原型链的末尾(即 Object.prototype
)。
示例代码:
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}`);
};
const person = new Person('John');
person.sayHello(); // 输出 "Hello, my name is John"
3. 简述 JavaScript 中的事件冒泡和事件捕获
- 事件冒泡:事件从最具体的元素(触发事件的元素)开始,逐级向上传播到最不具体的元素(通常是
document
对象)。例如,当点击一个嵌套在多个元素中的按钮时,事件会先在按钮上触发,然后依次在其父元素、祖父元素等上触发,直到传播到 document
对象。
- 事件捕获:与事件冒泡相反,事件从最不具体的元素(通常是
document
对象)开始,逐级向下传播到最具体的元素(触发事件的元素)。
在 addEventListener
方法中,可以通过第三个参数来指定是使用事件冒泡(默认,false
)还是事件捕获(true
)。
示例代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<div id="outer">
<div id="inner">Click me</div>
</div>
<script>
const outer = document.getElementById('outer');
const inner = document.getElementById('inner');
// 事件冒泡
outer.addEventListener('click', function () {
console.log('Outer div clicked (bubble)');
}, false);
inner.addEventListener('click', function () {
console.log('Inner div clicked (bubble)');
}, false);
// 事件捕获
outer.addEventListener('click', function () {
console.log('Outer div clicked (capture)');
}, true);
inner.addEventListener('click', function () {
console.log('Inner div clicked (capture)');
}, true);
</script>
</body>
</html>
4. 如何实现 JavaScript 中的继承?
- 原型链继承:通过将子类的原型指向父类的实例来实现继承。
function Parent() {
this.name = 'parent';
}
Parent.prototype.sayHello = function() {
console.log('Hello from parent');
};
function Child() {}
Child.prototype = new Parent();
const child = new Child();
console.log(child.name); // 输出 'parent'
child.sayHello(); // 输出 'Hello from parent'
function Parent() {
this.name = 'parent';
}
function Child() {
Parent.call(this);
this.childName = 'child';
}
const child = new Child();
console.log(child.name); // 输出 'parent'
console.log(child.childName); // 输出 'child'
function Parent() {
this.name = 'parent';
}
Parent.prototype.sayHello = function() {
console.log('Hello from parent');
};
function Child() {
Parent.call(this);
this.childName = 'child';
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
const child = new Child();
console.log(child.name); // 输出 'parent'
console.log(child.childName); // 输出 'child'
child.sayHello(); // 输出 'Hello from parent'
- 寄生组合继承:对组合继承的优化,避免了多次调用父类构造函数。
function Parent() {
this.name = 'parent';
}
Parent.prototype.sayHello = function() {
console.log('Hello from parent');
};
function Child() {
Parent.call(this);
this.childName = 'child';
}
function inheritPrototype(child, parent) {
const prototype = Object.create(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}
inheritPrototype(Child, Parent);
const child = new Child();
console.log(child.name); // 输出 'parent'
console.log(child.childName); // 输出 'child'
child.sayHello(); // 输出 'Hello from parent'
- ES6 类继承:使用
class
和 extends
关键字。
class Parent {
constructor() {
this.name = 'parent';
}
sayHello() {
console.log('Hello from parent');
}
}
class Child extends Parent {
constructor() {
super();
this.childName = 'child';
}
}
const child = new Child();
console.log(child.name); // 输出 'parent'
console.log(child.childName); // 输出 'child'
child.sayHello(); // 输出 'Hello from parent'
5. 什么是 JavaScript 中的异步编程,有哪些实现方式?
异步编程是指在执行代码时,不会阻塞后续代码的执行,而是在后台处理某些任务,当任务完成后再执行相应的回调函数。常见的实现方式有:
- 回调函数:将一个函数作为参数传递给另一个函数,当异步操作完成后调用该回调函数。
function fetchData(callback) {
setTimeout(() => {
const data = 'Some data';
callback(data);
}, 1000);
}
fetchData((data) => {
console.log(data); // 输出 'Some data'
});
- Promise:是一种更优雅的处理异步操作的方式,避免了回调地狱。
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const data = 'Some data';
resolve(data);
}, 1000);
});
}
fetchData()
.then((data) => {
console.log(data); // 输出 'Some data'
})
.catch((error) => {
console.error(error);
});
- async/await:是基于 Promise 的语法糖,使异步代码看起来更像同步代码。
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const data = 'Some data';
resolve(data);
}, 1000);
});
}
async function main() {
try {
const data = await fetchData();
console.log(data); // 输出 'Some data'
} catch (error) {
console.error(error);
}
}
main();
6. 解释 JavaScript 中的作用域和作用域链
- 作用域:定义了变量和函数的可访问范围。在 JavaScript 中有全局作用域和函数作用域(ES6 引入了块级作用域)。全局作用域中的变量和函数可以在任何地方访问,而函数作用域中的变量和函数只能在该函数内部访问。
// 全局作用域
let globalVar = 'global';
function myFunction() {
// 函数作用域
let localVar = 'local';
console.log(globalVar); // 可以访问全局变量
console.log(localVar); // 可以访问局部变量
}
myFunction();
console.log(globalVar); // 可以访问全局变量
// console.log(localVar); // 报错,无法访问局部变量
- 作用域链:当访问一个变量时,JavaScript 首先会在当前作用域中查找,如果找不到,就会沿着作用域链向上查找,直到找到该变量或到达全局作用域。作用域链是由多个作用域嵌套而成的。
7. 如何判断一个变量的类型?
- typeof 运算符:返回一个表示数据类型的字符串,如
'number'
、'string'
、'boolean'
、'object'
、'function'
、'undefined'
等。但对于 null
和数组,typeof
都会返回 'object'
。
console.log(typeof 123); // 输出 'number'
console.log(typeof 'hello'); // 输出 'string'
console.log(typeof true); // 输出 'boolean'
console.log(typeof {}); // 输出 'object'
console.log(typeof []); // 输出 'object'
console.log(typeof null); // 输出 'object'
console.log(typeof function() {}); // 输出 'function'
console.log(typeof undefined); // 输出 'undefined'
- instanceof 运算符:用于判断一个对象是否是某个构造函数的实例。
const arr = [];
console.log(arr instanceof Array); // 输出 true
- Object.prototype.toString.call():可以准确地判断各种数据类型。
console.log(Object.prototype.toString.call(123)); // 输出 '[object Number]'
console.log(Object.prototype.toString.call('hello')); // 输出 '[object String]'
console.log(Object.prototype.toString.call(true)); // 输出 '[object Boolean]'
console.log(Object.prototype.toString.call({})); // 输出 '[object Object]'
console.log(Object.prototype.toString.call([])); // 输出 '[object Array]'
console.log(Object.prototype.toString.call(null)); // 输出 '[object Null]'
console.log(Object.prototype.toString.call(function() {})); // 输出 '[object Function]'
console.log(Object.prototype.toString.call(undefined)); // 输出 '[object Undefined]'
8. 简述 JavaScript 中的垃圾回收机制
JavaScript 中的垃圾回收机制是自动管理内存的一种方式,它会自动回收不再使用的内存空间。常见的垃圾回收算法有:
- 标记清除算法:这是最常用的垃圾回收算法。它分为两个阶段:标记阶段和清除阶段。在标记阶段,垃圾回收器会从根对象(如全局对象)开始,标记所有可以访问到的对象;在清除阶段,会清除所有未被标记的对象。
- 标记整理算法:是对标记清除算法的改进,在清除阶段,会将所有存活的对象移动到内存的一端,然后清除掉另一端的所有未标记对象,这样可以避免内存碎片化。
9. 如何实现深拷贝和浅拷贝?
- 浅拷贝:只复制对象的一层属性,如果对象的属性是引用类型,则只复制引用,而不复制对象本身。可以使用
Object.assign()
或扩展运算符来实现浅拷贝。
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = Object.assign({}, obj1);
// 或者使用扩展运算符
// const obj2 = { ...obj1 };
obj2.b.c = 3;
console.log(obj1.b.c); // 输出 3,说明修改 obj2 的嵌套属性会影响 obj1
- 深拷贝:会递归地复制对象的所有属性,包括嵌套的对象,使得新对象和原对象完全独立。可以使用 JSON.parse(JSON.stringify()) 来实现简单的深拷贝,但它有一些局限性,如不能处理函数、正则表达式等。
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = JSON.parse(JSON.stringify(obj1));
obj2.b.c = 3;
console.log(obj1.b.c); // 输出 2,说明修改 obj2 的嵌套属性不会影响 obj1
也可以手动实现深拷贝函数:
function deepClone(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
let clone = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key]);
}
}
return clone;
}
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = deepClone(obj1);
obj2.b.c = 3;
console.log(obj1.b.c); // 输出 2,说明修改 obj2 的嵌套属性不会影响 obj1
10. 什么是 JavaScript 中的节流和防抖?
- 节流:在一定时间内,只执行一次函数。常用于处理高频事件,如滚动、窗口缩放等。可以通过定时器来实现节流。
function throttle(func, delay) {
let timer = null;
return function() {
if (!timer) {
func.apply(this, arguments);
timer = setTimeout(() => {
timer = null;
}, delay);
}
};
}
function handleScroll() {
console.log('Scroll event');
}
window.addEventListener('scroll', throttle(handleScroll, 500));
- 防抖:在一定时间内,只有最后一次调用函数才会执行。常用于输入框的搜索提示、按钮点击等场景。可以通过定时器来实现防抖。
function debounce(func, delay) {
let timer = null;
return function() {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, arguments);
}, delay);
};
}
function handleInput() {
console.log('Input event');
}
const input = document.getElementById('input');
input.addEventListener('input', debounce(handleInput, 500));