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 类继承:使用 classextends 关键字。
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));