前端面试题-JavaScript中级篇

以下为JavaScript中级篇面试考察点总结,具体知识点不会太详细,主要梳理面试核心考察点,为面试做准备。中级JavaScript开发者被期望具备独立解决复杂问题的能力,并对语言的内在机制有深刻的理解。

一、 this 指向

四大绑定规则

  1. 默认绑定 : 独立函数调用,非严格模式下指向全局对象 (window),严格模式下为 undefined

  2. 隐式绑定 : 作为对象方法调用,this 指向该对象。

  3. 显式绑定 : 通过 .call(), .apply(), .bind() 强制指定 this

  4. new 绑定 : 通过 new 调用构造函数,this 指向新创建的实例。

箭头函数的 this

箭头函数不创建自己的 this,它会捕获其定义时所在的词法作用域的 this 值。

代码示例:

js 复制代码
const name = 'window';
const person = {
  name: 'Mickey',
  regularFunc: function() {
    setTimeout(function() {
      // 回调函数独立调用,适用默认绑定
      console.log('regularFunc this:', this.name); // 'window'
    }, 100);
  },
  arrowFunc: function() {
    setTimeout(() => {
      // 箭头函数捕获了 arrowFunc 这个方法的 this,即 person 对象
      console.log('arrowFunc this:', this.name); // 'Mickey'
    }, 100);
  }
};

person.regularFunc();
person.arrowFunc();

二、 原型与原型链

这是JavaScript实现继承和属性共享的核心机制。

  • prototype : 每个函数都拥有的属性,指向一个对象,用于存放实例需要共享的方法和属性。

  • __proto__ : 每个对象 都拥有的属性,指向其构造函数的 prototype

  • 原型链 : 当访问一个对象的属性时,引擎会先在对象自身查找,若未找到,则沿着 __proto__ 链向上查找,直至链的顶端 null

代码示例 (ES5继承):

js 复制代码
function Animal(name) {
  this.name = name;
}
Animal.prototype.speak = function() { console.log('Animal speaks'); };

function Dog(name) {
 // 1. 借用构造函数继承属性
  Animal.call(this, name);
}

// 2. 核心:创建Animal.prototype的副本,作为Dog.prototype
Dog.prototype = Object.create(Animal.prototype);
// 3. 修复constructor指向
Dog.prototype.constructor = Dog;

const myDog = new Dog('Buddy');
myDog.speak(); // 'Animal speaks' - 继承自Animal.prototype
console.log(myDog instanceof Animal); // true

三、 作用域与闭包

作用域

定义了变量和函数的可访问范围。

  • 全局作用域: 在代码任何地方都可访问。

  • 函数作用域: 变量在声明它们的函数内部及子函数内部可访问。

  • 块级作用域 (ES6) : 由 letconst 声明的变量,只在 {} 代码块内可访问。

闭包

指一个函数能够访问并操作其词法作用域(定义时的作用域)中的变量,即使该函数在其词法作用域之外被执行。

闭包的应用 (数据封装):

js 复制代码
function createCounter() {
  let count = 0; // 私有变量,外部无法直接访问

  return {
    increment: function() {
      count++;
    },
    getValue: function() {
      return count;
    }
  };
}

const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.getValue()); // 2
// console.log(counter.count); // undefined, 无法直接访问

四、 异步编程 (Promise, async/await)

这是处理非阻塞操作的核心,事件循环 (Event Loop) 是其底层机制。

Promise

一个代表了异步操作最终完成或失败的对象。它有三种状态:

  • pending (进行中)

  • fulfilled (已成功)

  • rejected (已失败)。

async/await

  • Promise 的语法糖,允许以同步的方式编写异步代码,极大提升了可读性。

  • await 必须在 async 函数内部使用,它会暂停 async 函数的执行,等待 Promise 解决。

代码示例 (事件循环):

js 复制代码
console.log('1. Script Start'); // 同步任务

setTimeout(() => {
  console.log('5. setTimeout (Macro-task)'); // 宏任务
}, 0);

new Promise((resolve) => {
  console.log('2. Promise Constructor'); // 同步任务
  resolve();
}).then(() => {
  console.log('4. Promise.then (Micro-task)'); // 微任务
});

async function main() {
  console.log('3. async function'); // 同步任务
  await Promise.resolve(); // await后面的代码被放入微任务队列
  console.log('6. After await (Micro-task 2)'); // 微任务
}
main();

// 最终输出: 1, 2, 3, 4, 6, 5

五、 高阶函数

一个函数如果接收函数作为参数,或将函数作为返回值,那么它就是高阶函数。

  • 常见内置高阶函数 : Array.prototype.map, filter, reduce

  • 自定义高阶函数 (函数柯里化 Currying 示例) :

js 复制代码
// 一个简单的加法函数
function add(x, y) {
  return x + y;
}

// 柯里化转换器
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function(...args2) {
        return curried.apply(this, args.concat(args2));
      }
    }
  };
}

const curriedAdd = curry(add);
const addFive = curriedAdd(5); // 返回一个新函数,等待接收第二个参数

console.log(addFive(3)); // 8
console.log(curriedAdd(5)(3)); // 8

六、 ES6+ 新特性

1. 解构赋值 (Destructuring Assignment)

解构赋值语法是一种JavaScript表达式,可以将数组中的值或对象中的属性解包成独立的变量。

代码示例:

js 复制代码
// --- 对象解构 ---
const user = {
  id: 42,
  is_verified: true,
  profile: {
    name: 'Mickey',
    age: 30
  }
};

// 基础解构
const { id, is_verified } = user;
console.log(id); // 42
console.log(is_verified); // true

// 解构并重命名变量
const { id: userID } = user;
console.log(userID); // 42

// 解构不存在的属性并提供默认值
const { last_login = 'N/A' } = user;
console.log(last_login); // 'N/A'

// 深度嵌套解构
const { profile: { name, age } } = user;
console.log(name); // 'Mickey'
console.log(age); // 30

// --- 数组解构 ---
const rgb = [255, 204, 0];

// 基础解构
const [red, green, blue] = rgb;
console.log(red); // 255

// 忽略某些元素
const [r, , b] = rgb;
console.log(r); // 255
console.log(b); // 0

2. 展开/剩余语法

... 语法根据其使用场景,既可以作为"展开语法",也可以作为"剩余语法"。

代码示例:

js 复制代码
// --- 展开语法 (Spread) ---
// 用于数组:克隆与合并
const arr1 = ['a', 'b'];
const arr2 = [...arr1, 'c', 'd']; // ['a', 'b', 'c', 'd']
const arrClone = [...arr1]; // 创建一个 arr1 的浅拷贝

// 用于对象 (ES2018): 克隆与合并
const obj1 = { x: 1, y: 2 };
const obj2 = { ...obj1, z: 3 }; // { x: 1, y: 2, z: 3 }

// 用于函数调用:将数组元素作为独立参数传入
function sum(x, y, z) {
  return x + y + z;
}
const numbers = [1, 2, 3];
console.log(sum(...numbers)); // 6

// --- 剩余语法 (Rest) ---
// 用于函数参数:将不确定的参数收集到一个数组中
function logArguments(firstArg, ...restOfArgs) {
  console.log('第一个参数:', firstArg);
  console.log('其余参数:', restOfArgs); // restOfArgs 是一个真数组
}
logArguments('hello', 'world', 123, true);
// 输出:
// 第一个参数: hello
// 其余参数: [ 'world', 123, true ]

// 用于数组和对象解构
const [first, ...rest] = [10, 20, 30, 40];
console.log(first); // 10
console.log(rest);  // [20, 30, 40]

3. Map/Set 数据结构

  • Map: 键值对的集合,键可以是任意类型的值。解决了传统对象键必须是字符串或Symbol的限制。

  • Set: 值的集合,其中每个值都必须是唯一的。

代码示例:

js 复制代码
// --- Set ---
// 允许存储任何类型的唯一值,无论是原始值或者是对象引用。
const mySet = new Set();
mySet.add(1);
mySet.add(5);
mySet.add(5); // 重复添加,将被忽略
mySet.add('some text');
const o = {a: 1, b: 2};
mySet.add(o);

console.log(mySet.has(1)); // true
mySet.delete(5);
console.log(mySet.size); // 3

// Set 的一个主要用途是数组去重
const arrayWithDuplicates = [1, 2, 3, 2, 1, 4];
const uniqueArray = [...new Set(arrayWithDuplicates)];
console.log(uniqueArray); // [1, 2, 3, 4]

// --- Map ---
// 键可以是任意类型
const myMap = new Map();
const keyString = 'a string';
const keyObj = {};
const keyFunc = function() {};

myMap.set(keyString, "value associated with 'a string'");
myMap.set(keyObj, 'value associated with keyObj');
myMap.set(keyFunc, 'value associated with keyFunc');

console.log(myMap.get(keyString)); // "value associated with 'a string'"
console.log(myMap.get(keyObj));   // "value associated with keyObj"
console.log(myMap.size); // 3

// Map 的迭代
for (const [key, value] of myMap) {
  console.log(`${String(key)} = ${value}`);
}

七、 性能优化基础

1. 防抖 (Debounce)

防抖的核心思想是:在事件被触发n秒后再执行回调,如果在这n秒内事件又被触发,则重新计时。这适用于只需响应用户最终操作状态的场景,如输入框搜索联想。

代码示例:

js 复制代码
/**
 * 防抖函数
 * @param {Function} func - 需要执行的回调函数
 * @param {number} delay - 延迟时间 (ms)
 * @returns {Function} - 经过防抖处理的新函数
 */
function debounce(func, delay) {
  let timerId; // 通过闭包维持 timerId

  return function(...args) {
    // 保存函数调用时的 this 上下文和参数
    const context = this;

    // 如果存在定时器,则清除它,实现重新计时
    clearTimeout(timerId);

    // 设置新的定时器
    timerId = setTimeout(() => {
      // 使用 apply 将保存的上下文和参数传递给原函数
      func.apply(context, args);
    }, delay);
  };
}

// --- 使用场景 ---
// HTML: <input type="text" id="searchInput" />
const searchInput = document.getElementById('searchInput');

function handleSearch(event) {
  // 这里的 this 指向 searchInput 元素
  console.log(`Searching for: ${this.value}...`);
  // 在这里可以发起API请求
}

// 将 handleSearch 函数进行防抖处理,延迟500ms执行
const debouncedSearch = debounce(handleSearch, 500);

// 绑定事件
searchInput.addEventListener('input', debouncedSearch);

2. 节流 (Throttle)

节流的核心思想是:在规定时间内,事件处理函数最多执行一次。这适用于需要以固定频率响应的场景,如scroll滚动加载、resize窗口调整。

代码示例 (定时器版):

js 复制代码
/**
 * 节流函数
 * @param {Function} func - 需要执行的回调函数
 * @param {number} limit - 时间间隔 (ms)
 * @returns {Function} - 经过节流处理的新函数
 */
function throttle(func, limit) {
  let inThrottle = false; // 节流阀状态

  return function(...args) {
    const context = this;
    if (!inThrottle) {
      // 当节流阀关闭时,执行函数
      func.apply(context, args);
      // 打开节流阀
      inThrottle = true;
      // 在 limit 时间后,关闭节流阀,允许下一次执行
      setTimeout(() => {
        inThrottle = false;
      }, limit);
    }
  };
}

// --- 使用场景 ---
// HTML: <div id="scroll-container" style="height: 300px; overflow: auto;">...内容...</div>
const container = document.getElementById('scroll-container');

function onScroll() {
  console.log('Scroll event processed at', new Date().toLocaleTimeString());
}

// 每1秒最多处理一次 scroll 事件
const throttledScroll = throttle(onScroll, 1000);

container.addEventListener('scroll', throttledScroll);

八、 跨域

1. CORS (Cross-Origin Resource Sharing)

CORS 是目前跨域解决方案的标准。它需要后端服务器进行配置。

前端代码 (非常简单):

前端的 fetch 请求与普通请求无异。浏览器会自动处理CORS相关的握手(如发送Preflight请求)。

js 复制代码
// 前端向 http://api.example.com 发起请求
// 假设前端页面位于 http://app.mydomain.com

fetch('http://api.example.com/data', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Custom-Header': 'MyValue' // 自定义请求头会触发Preflight
  },
  body: JSON.stringify({ name: 'Mickey' })
})
.then(response => {
  if (!response.ok) throw new Error('Network response was not ok');
  return response.json();
})
.then(data => console.log(data))
.catch(error => console.error('CORS Error:', error));

后端代码 (Node.js/Express 示例):

关键在于服务器响应中添加的 Access-Control-* HTTP头。

js 复制代码
const express = require('express');
const cors = require('cors'); // 使用 cors 中间件会更方便
const app = express();

// --- 手动设置CORS头 (原理) ---
// app.use((req, res, next) => {
//   // 允许来自 http://app.mydomain.com 的请求
//   res.setHeader('Access-Control-Allow-Origin', 'http://app.mydomain.com');
//   // 允许的HTTP方法
//   res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
//   // 允许的请求头
//   res.setHeader('Access-Control-Allow-Headers', 'X-Custom-Header, Content-Type');
//   // 允许携带凭证(如cookies)
//   res.setHeader('Access-Control-Allow-Credentials', true);
//   next();
// });

// --- 使用 cors 中间件 (推荐) ---
const corsOptions = {
  origin: 'http://app.mydomain.com',
  methods: "GET,POST",
  allowedHeaders: "Content-Type,X-Custom-Header",
  credentials: true
};
app.use(cors(corsOptions));

// 路由处理
app.post('/data', (req, res) => {
  res.json({ message: 'CORS request successful!' });
});

app.listen(3000, () => console.log('API server listening on port 3000'));

2. JSONP (JSON with Padding)

JSONP是一种"hack"方法,利用 <script> 标签不受同源策略限制的特点。它只支持GET请求

前端代码:

js 复制代码
/**
 * JSONP 动态实现
 * @param {string} url - 请求的URL (不含callback参数)
 * @param {string} callbackParamName - 服务端接收的回调函数名参数 (通常是'callback')
 */
function jsonpRequest(url, callbackParamName = 'callback') {
  return new Promise((resolve, reject) => {
    const callbackName = `jsonp_callback_${Date.now()}_${Math.floor(Math.random() * 100000)}`;

    // 1. 在全局(window)上创建一个函数,用于接收服务端返回的数据
    window[callbackName] = function(data) {
      // 4. 清理工作:移除script标签和全局函数
      document.body.removeChild(script);
      delete window[callbackName];
      // 5. Promise 成功
      resolve(data);
    };

    // 2. 创建 script 标签
    const script = document.createElement('script');
    const separator = url.includes('?') ? '&' : '?';
    script.src = `${url}${separator}${callbackParamName}=${callbackName}`;
    script.onerror = () => {
      // 错误处理
      reject(new Error('JSONP request failed.'));
      document.body.removeChild(script);
      delete window[callbackName];
    };

    // 3. 将 script 标签插入DOM,浏览器会自动发起GET请求
    document.body.appendChild(script);
  });
}

// --- 使用场景 ---
jsonpRequest('http://api.example.com/jsonp-data')
  .then(data => {
    console.log('JSONP Response:', data);
  });

// 服务端(api.example.com)的响应内容应为:
// jsonp_callback_1678886400000_12345({"message": "This is a JSONP response"});

3. 代理服务器 (Proxy Server)

代理是目前大型项目中处理跨域最常用、最安全灵活的方案。

前端代码:

前端请求的是同源的后端代理接口,而不是直接请求第三方API。

javascript 复制代码
// 前端页面位于 http://app.mydomain.com
// 请求同源的后端代理接口 /api/weather

fetch('/api/weather?city=Shanghai')
  .then(response => response.json())
  .then(data => {
    console.log('Weather data from proxy:', data);
  })
  .catch(error => console.error('Proxy request failed:', error));

后端代理服务器代码 (Node.js/Express 示例):

js 复制代码
const express = require('express');
const axios = require('axios'); // 使用axios等库方便地在后端发起HTTP请求
const app = express();

// 定义代理接口 /api/weather
app.get('/api/weather', async (req, res) => {
  try {
    const { city } = req.query;
    const apiKey = 'YOUR_THIRD_PARTY_API_KEY';
    const externalApiUrl = `https://api.weatherapi.com/v1/current.json?key=${apiKey}&q=${city}`;

    // 关键:由后端服务器发起对第三方API的请求
    console.log(`Proxying request to: ${externalApiUrl}`);
    const apiResponse = await axios.get(externalApiUrl);

    // 将从第三方API获取的数据原样返回给前端
    res.status(apiResponse.status).json(apiResponse.data);
  } catch (error) {
    // 错误处理
    console.error('Proxy Error:', error.message);
    res.status(error.response?.status || 500).json({ message: 'Failed to fetch data from external API.' });
  }
});

app.listen(8080, () => console.log('App server with proxy running on port 8080'));
原理:
  • 浏览器(客户端) <=> 同源后端服务器 <=> 第三方API服务器。
  • 浏览器与后端服务器之间通信遵守同源策略,而后端服务器之间的HTTP请求不受浏览器同源策略的限制。

九、 浏览器兼容性

  • Babel: 将ES6+代码转换为向后兼容的ES5代码。

  • Polyfill : 为旧浏览器提供缺失的原生API的实现(例如 Promise, Array.from)。core-js 是最常用的Polyfill库。

  • CSS Autoprefixer: 自动为CSS规则添加浏览器厂商前缀。

  • caniuse.com: 查询特定Web技术在各浏览器版本中兼容性的权威网站。

十、 正则表达式

用于字符串的模式匹配,是处理文本的强大工具。

常用方法:

  • test() (检查是否匹配)
  • match() (获取匹配结果)
  • replace() (查找并替换)。

示例 (简单邮箱验证) :

js 复制代码
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/;
const email1 = 'test@example.com';
const email2 = 'invalid-email';

console.log(emailRegex.test(email1)); // true
console.log(emailRegex.test(email2)); // false

以上是JS中级篇面试考察点的内容,如有错误欢迎评论区指正。

相关推荐
Beginner x_u1 小时前
从接口文档到前端调用:Axios 封装与实战全流程详解
前端·javascript·ajax·接口·axios
HIT_Weston1 小时前
46、【Ubuntu】【Gitlab】拉出内网 Web 服务:http.server 分析(三)
前端·ubuntu·gitlab
测试-鹏哥1 小时前
ITP平台全新Mock服务上线 —— 助力高效API测试
前端·python·测试工具
BlackWolfSky1 小时前
ES6 教程学习笔记
前端·javascript·es6
IT_陈寒1 小时前
Redis性能翻倍的5个冷门技巧:从缓存穿透到集群优化实战指南
前端·人工智能·后端
木易 士心1 小时前
Node.js 性能诊断利器 Clinic.js:原理剖析与实战指南
开发语言·javascript·node.js
不会写DN1 小时前
如何实现UniApp登录拦截?
前端·javascript·vue.js·typescript·uni-app·vue
不老刘1 小时前
深入理解 React Native 的 Metro
javascript·react native·react.js·metro
哆啦A梦15881 小时前
67 token 过期时间
前端·javascript·vue.js·node.js