JavaScript面试宝典

1. JS 由哪三部分组成?

JavaScript 由以下三部分组成:

  1. ECMAScript(ES):JavaScript 的核心语法,如变量、作用域、数据类型、函数、对象等。
  2. DOM(文档对象模型):用于操作 HTML 和 XML 文档的 API,可以动态修改网页内容、结构和样式。
  3. BOM(浏览器对象模型):用于操作浏览器窗口和页面,例如 windownavigatorlocationhistoryscreen 等对象。

2. JS 有哪些内置对象?

JavaScript 具有以下内置对象:

  1. 基本对象ObjectFunctionBooleanSymbol
  2. 数值对象NumberBigIntMath
  3. 字符串对象String
  4. 数组对象Array
  5. 日期对象Date
  6. 正则对象RegExp
  7. 错误对象ErrorTypeErrorSyntaxErrorReferenceError
  8. 集合对象SetMapWeakSetWeakMap
  9. 异步对象PromiseAsyncFunction

3. 操作数组的方法有哪些?

数组方法可以分为几类:

① 增加元素
  • push(value):在数组末尾添加元素,返回新长度。
  • unshift(value):在数组头部添加元素,返回新长度。
  • splice(index, 0, value):在指定位置插入元素。
② 删除元素
  • pop():删除数组最后一个元素,并返回该元素。
  • shift():删除数组第一个元素,并返回该元素。
  • splice(index, count):删除指定位置的 count 个元素。
③ 查找元素
  • indexOf(value):返回元素第一次出现的位置,找不到返回 -1
  • find(callback):返回符合条件的第一个元素,没有符合条件的返回 undefined
  • findIndex(callback):返回符合条件的元素索引,找不到返回 -1
  • includes(value):判断数组是否包含某个元素,返回 true/false
④ 其他常用方法
  • map(callback):返回一个新数组,每个元素由回调函数处理。
  • filter(callback):筛选符合条件的元素,返回新数组。
  • reduce(callback, initialValue):累加数组值,常用于计算总和、扁平化数组。
  • sort(callback):对数组进行排序(默认按 Unicode 编码排序)。
  • reverse():反转数组元素顺序。
  • concat(arr):合并数组,返回新数组。
  • slice(start, end):返回数组的部分片段,不修改原数组。

4. JS 对数据类型的检测方式有哪些?

  • typeof:适用于基本数据类型,但 null 误判为 "object"
  • instanceof:判断对象是否属于某个构造函数的实例。
  • Object.prototype.toString.call(value):返回精准数据类型,如 "[object Array]"
  • Array.isArray(value):判断是否为数组。

5. 说一下闭包,闭包有什么特点?

闭包(Closure) 是指函数可以访问其外部作用域的变量,即使外部函数已经执行结束。
特点

  1. 可以访问外部函数的变量,即使外部函数执行完毕。
  2. 变量不会被垃圾回收(可能导致内存泄露)。
  3. 适用于模块化开发,模拟私有变量。

示例

function outer() {
    let count = 0;
    return function inner() {
        count++;
        console.log(count);
    };
}
const counter = outer();
counter(); // 1
counter(); // 2

闭包(Closure)是指一个函数能够访问其外部作用域中的变量,即使在外部函数执行结束后,仍然可以保留对外部变量的访问权限。闭包的主要使用场景如下:


1. 数据私有化(模拟私有变量)

闭包可以创建私有变量,防止外部直接访问或修改数据。

示例:模拟私有变量

function createCounter() {
    let count = 0; // 作为私有变量
    return {
        increment: function() {
            count++;
            console.log(count);
        },
        decrement: function() {
            count--;
            console.log(count);
        },
        getCount: function() {
            return count;
        }
    };
}

const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
console.log(counter.getCount()); // 2
console.log(counter.count); // undefined(无法直接访问私有变量)

应用场景

  • 需要封装变量,避免外部随意修改(如计数器、缓存管理、权限控制)。

2. 事件监听器 & 回调

闭包常用于事件监听器或回调函数,使得事件处理函数能够访问外部作用域中的变量。

示例:按钮点击计数

function setupButton() {
    let count = 0;
    document.getElementById("myButton").addEventListener("click", function() {
        count++;
        console.log(`Button clicked ${count} times`);
    });
}
setupButton();

应用场景

  • addEventListener 回调中保留数据(如点击次数、鼠标移动距离等)。

3. 函数柯里化(参数预处理)

柯里化(Currying)是指将一个接收多个参数的函数,转换为多个接收单一参数的函数。

示例:实现加法柯里化

function add(x) {
    return function(y) {
        return x + y;
    };
}

const addFive = add(5);
console.log(addFive(3)); // 8
console.log(addFive(10)); // 15

应用场景

  • 预设部分参数,提高代码复用性(如 bind 预设 this)。
  • 在 Redux、Lodash 等库中广泛应用。

4. 延迟执行(定时器)

闭包可以用于在定时器或异步操作中保留执行上下文。

示例:定时器

function delayedMessage(msg, delay) {
    setTimeout(function() {
        console.log(msg);
    }, delay);
}

delayedMessage("Hello, Closure!", 2000);

应用场景

  • setTimeout / setInterval 相关的任务管理(如轮询、延迟加载)。

5. 模拟块级作用域(ES5 以前)

在 ES6 之前,JavaScript 没有 letconst 关键字,使用闭包可以创建局部作用域,避免变量污染。

示例:IIFE(立即执行函数表达式)

(function() {
    var secret = "I am private";
    console.log(secret);
})();
console.log(typeof secret); // undefined(无法访问)

应用场景

  • 在 ES5 及以前,使用 IIFE 防止变量污染全局作用域。
  • 现代 JavaScript 用 letconst 取代该用法。

6. 记忆化(缓存计算结果,提高性能)

闭包可用于缓存计算结果,避免重复计算,提升性能。

示例:缓存计算结果

function memoize(fn) {
    let cache = {};
    return function(arg) {
        if (cache[arg]) {
            console.log("Fetching from cache:", arg);
            return cache[arg];
        }
        console.log("Calculating result for:", arg);
        cache[arg] = fn(arg);
        return cache[arg];
    };
}

const square = memoize(x => x * x);
console.log(square(4)); // 计算并存入缓存
console.log(square(4)); // 直接从缓存获取
console.log(square(5)); // 计算并存入缓存

应用场景

  • 计算密集型任务的优化(如斐波那契数列、递归)。
  • 缓存 API 请求结果,减少重复网络请求。

7. 迭代器 & 生成唯一 ID

闭包可以用来创建迭代器或唯一 ID 生成器。

示例:唯一 ID 生成器

function createIdGenerator() {
    let id = 0;
    return function() {
        return id++;
    };
}

const getId = createIdGenerator();
console.log(getId()); // 0
console.log(getId()); // 1
console.log(getId()); // 2

应用场景

  • 生成唯一标识符(如任务 ID、DOM 元素 ID)。

8. 绑定 this(模拟 bind 方法)

闭包可以用于创建绑定 this 的新函数。

示例:手写 bind

function myBind(fn, context) {
    return function(...args) {
        return fn.apply(context, args);
    };
}

const obj = { name: "Alice" };
function sayName(greeting) {
    console.log(greeting + ", " + this.name);
}

const boundSayName = myBind(sayName, obj);
boundSayName("Hello"); // Hello, Alice

应用场景

  • 事件处理时确保 this 绑定正确。

总结

使用场景 说明 示例
数据私有化 模拟私有变量,防止外部访问 计数器、权限管理
事件监听器 保留外部变量的数据 统计按钮点击次数
函数柯里化 预处理参数,提升复用性 add(5)(10)
定时器 异步任务执行 setTimeout 回调
模拟块级作用域 防止变量污染 IIFE
缓存优化 记忆化函数,减少重复计算 Fibonacci、API 请求缓存
生成唯一 ID 生成不重复的 ID 任务队列管理
绑定 this 确保回调 this 指向正确 myBind

面试高频问题:

  1. 闭包的本质是什么?

    • 一个函数可以访问其外部作用域的变量,即使外部函数执行结束后,变量依然可用。
  2. 闭包有哪些常见应用场景?

    • 数据私有化、事件监听、定时器、柯里化、缓存优化、唯一 ID 生成等。
  3. 闭包会导致内存泄漏吗?如何避免?

    • 是的,闭包可能导致变量无法被垃圾回收。
    • 解决方案:手动解除引用,如 element.onclick = null 释放 DOM 事件闭包,或者减少不必要的闭包使用。

闭包是 JavaScript 重要的特性之一,熟练掌握它的应用能提升代码质量和性能!

6. 前端的内存泄露怎么理解?

内存泄露 是指程序不再使用某些对象,但垃圾回收机制无法释放它们,导致内存占用增加。
常见原因:

  1. 全局变量未释放window.variable 一直存在)
  2. 未清理的定时器setInterval 没有 clearInterval
  3. 闭包未正确释放(函数执行后仍然引用外部变量)
  4. 未移除的 DOM 事件监听element.addEventListener 没有 removeEventListener

解决方案

  • 避免全局变量,使用 let/const 限制作用域。
  • setInterval 用完后及时 clearInterval()
  • 及时 removeEventListener() 解除事件绑定。
  • 手动置 null 解除对象引用。

7. 事件委托是什么?

事件委托(Event Delegation) 是将事件监听器绑定在父级元素上,利用事件冒泡机制处理子元素事件,提高性能。
示例

document.getElementById('parent').addEventListener('click', function(event) {
    if (event.target.tagName === 'BUTTON') {
        console.log('Button clicked:', event.target.innerText);
    }
});

这样即使新 button 动态添加到 #parent,仍然可以触发事件。


8. 基本数据类型和引用数据类型的区别?

数据类型 存储位置 赋值方式 比较方式
基本类型 栈内存 拷贝值 值比较
引用类型 堆内存 赋引用 地址比较

9. 说一下原型链。

原型链(Prototype Chain) 是 JavaScript 继承机制的核心。
每个对象都有 __proto__,指向其构造函数的 prototype,形成一个链式结构。
详细参照链接:js原型与原型链

10. new 操作符具体做了什么?

  1. 创建一个新对象 obj
  2. obj.__proto__ 关联到构造函数的 prototype
  3. 执行构造函数,并绑定 this 到新对象。
  4. 如果构造函数返回对象,则返回该对象,否则返回 obj

示例:

function Person(name) {
    this.name = name;
}
const p = new Person("Alice");
console.log(p.name); // Alice

11. callapplybind 三者有什么区别?

callapplybind 是 JavaScript 中用于更改函数 this 指向的方法,它们的主要区别如下:

方法 作用 参数 是否立即执行 返回值
call 绑定 this 并调用函数 thisArg, arg1, arg2, ... 调用结果
apply 绑定 this 并调用函数 thisArg, [arg1, arg2, ...] 调用结果
bind 绑定 this返回新函数 thisArg, arg1, arg2, ... 新函数

1. call 方法

call(thisArg, arg1, arg2, ...) 方法可以手动指定 this立即调用 该函数,参数按顺序传递。

示例 1:基本用法

function greet(greeting, punctuation) {
    console.log(greeting + ", " + this.name + punctuation);
}
const person = { name: "Alice" };
greet.call(person, "Hello", "!"); // Hello, Alice!
  • this 被绑定到 person,输出 "Hello, Alice!"
  • 参数 "Hello""!" 依次传入。

示例 2:继承构造函数

function Parent(name) {
    this.name = name;
}
function Child(name, age) {
    Parent.call(this, name); // 调用 Parent 构造函数
    this.age = age;
}
const child = new Child("Bob", 10);
console.log(child.name, child.age); // Bob 10
  • Child 通过 call 调用了 Parent,继承了 name 属性。

2. apply 方法

apply(thisArg, [arg1, arg2, ...]) 也是手动指定 this立即调用 该函数,但参数必须是 数组

示例 1:基本用法

function sum(a, b, c) {
    return a + b + c;
}
console.log(sum.apply(null, [1, 2, 3])); // 6
  • null 代表 this,因为 sum 本身不依赖 this
  • apply 传入参数数组 [1, 2, 3]

示例 2:获取数组中的最大/最小值

const numbers = [3, 8, 2, 7, 4];
console.log(Math.max.apply(null, numbers)); // 8
console.log(Math.min.apply(null, numbers)); // 2
  • Math.maxMath.min 只能接收多个单独的参数,而 apply 允许传递数组。

3. bind 方法

bind(thisArg, arg1, arg2, ...)callapply 的最大区别是:

  • 不会立即调用函数,而是返回一个新的函数
  • 新函数永久绑定 this,无论以后如何调用,它的 this 都不会改变。

示例 1:基本用法

function greet(greeting, punctuation) {
    console.log(greeting + ", " + this.name + punctuation);
}
const person = { name: "Alice" };
const boundGreet = greet.bind(person, "Hello");
boundGreet("!"); // Hello, Alice!
  • bind 生成的新函数 boundGreet 绑定了 thisperson
  • "Hello" 作为 greeting 预设进去,调用时只需提供 punctuation

示例 2:延迟执行

const obj = {
    name: "Charlie",
    sayName: function() {
        console.log(this.name);
    }
};
const say = obj.sayName.bind(obj);
setTimeout(say, 1000); // 1 秒后打印 "Charlie"
  • bind 绑定 this,即使 setTimeout 在全局环境中执行,this 仍然指向 obj

4. callapplybind 的主要区别

方法 是否立即执行 参数传递 适用场景
call 依次传递参数 立即执行,适用于手动指定 this 的方法调用
apply 以数组形式传递 立即执行,适用于参数数量不固定的情况(如 Math.max
bind 依次传递参数 返回新函数,适用于事件绑定、延迟调用等

5. 适用场景总结

场景 推荐方法
立即调用函数,并更改 this call
立即调用函数,并且参数是数组 apply
需要返回一个新函数,稍后执行 bind
继承构造函数 call
事件绑定,避免 this 丢失 bind
setTimeout 绑定 this bind

6. 面试高频考点

  1. 为什么 bind 返回的是新函数?

    • 因为 bind 不会立即执行,而是返回一个永久绑定 this 的新函数,适用于回调和事件处理。
  2. callapply 什么时候使用?

    • 当参数已知,使用 call(传递参数更直观)。
    • 当参数是动态数组,使用 apply(例如 Math.max.apply(null, array))。
  3. 为什么 bindsetTimeout 中很重要?

    • 因为 setTimeout 内部的 this 默认指向 window,使用 bind 可以确保 this 指向原对象。
const obj = { name: "Bob" };
setTimeout(function() {
    console.log(this.name); // undefined
}, 1000);

setTimeout(function() {
    console.log(this.name); // Bob
}.bind(obj), 1000);

这三个方法是 JavaScript 面试的高频考点,掌握它们的区别和应用场景能帮助你更高效地编写代码!

12… JS 是如何实现继承的?**

JavaScript 主要通过 原型链ES6 class 语法 实现继承:

1. 原型链继承(Prototype Inheritance)

每个 JavaScript 对象都有一个 __proto__ 指向它的原型对象,子类可以通过 prototype 继承父类的方法和属性。

function Parent(name) {
    this.name = name;
}
Parent.prototype.sayHello = function() {
    console.log("Hello, " + this.name);
};

function Child(name, age) {
    Parent.call(this, name); // 继承父类的属性
    this.age = age;
}

// 继承父类方法
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

const child = new Child("Tom", 10);
child.sayHello(); // Hello, Tom
2. ES6 class 继承

ES6 引入 class 语法,使用 extends 关键字更直观地实现继承。

class Parent {
    constructor(name) {
        this.name = name;
    }
    sayHello() {
        console.log("Hello, " + this.name);
    }
}

class Child extends Parent {
    constructor(name, age) {
        super(name); // 继承父类构造函数
        this.age = age;
    }
}

const child = new Child("Tom", 10);
child.sayHello(); // Hello, Tom

区别

  • prototype 继承是基于原型链,而 class 继承是 ES6 语法糖,底层仍然依赖原型。
  • super 关键字可以更方便地调用父类方法。

13. JS 的设计原理是什么?

JavaScript 的设计基于以下原则:

  1. 单线程:JavaScript 主要用于 Web 页面,设计为单线程避免 UI 渲染冲突。
  2. 事件驱动:依赖 事件循环(Event Loop) 执行异步任务,如 setTimeoutPromise
  3. 动态类型:变量类型动态变化,无需声明类型。
  4. 原型继承:JS 采用 原型链 作为继承机制,而非类继承。
  5. 函数式编程:JS 允许高阶函数、闭包等,支持函数式编程风格。

14. JS 中关于 this 指向的问题

this 的指向取决于 调用方式

1. 全局作用域
console.log(this); // 在浏览器中:window,在 Node.js 中:global
2. 对象方法
const obj = {
    name: "Tom",
    sayHello() {
        console.log(this.name); // this 指向 obj
    }
};
obj.sayHello(); // Tom
3. 构造函数
function Person(name) {
    this.name = name;
}
const p = new Person("Tom");
console.log(p.name); // Tom(this 指向实例对象)
4. call / apply / bind
function sayHi() {
    console.log(this.name);
}
const user = { name: "Alice" };

sayHi.call(user); // Alice
sayHi.apply(user); // Alice
const boundSayHi = sayHi.bind(user);
boundSayHi(); // Alice
5. 箭头函数
const obj = {
    name: "Tom",
    sayHello: function() {
        setTimeout(() => {
            console.log(this.name); // this 继承自 obj
        }, 1000);
    }
};
obj.sayHello(); // Tom

总结

  • 箭头函数的 this 由外层作用域决定。
  • bind 可手动绑定 thiscall/apply 可立即执行。

15. script 标签里的 async 和 defer 有什么区别?

你可能感兴趣的:(javascript,面试,前端)