【京东前端面试真题解析】第一篇:从基础到进阶,吃透 JS 核心考点
前言
京东作为国内头部电商平台,其前端面试对基础知识的扎实度、工程化思维和业务落地能力要求极高。本系列博客将围绕京东前端面试高频真题,从「题目拆解→核心考点→解题思路→拓展延伸」四个维度,深度剖析面试题背后的考察逻辑,帮助大家不仅 “会做”,更能 “懂原理、知延伸”。
本篇作为系列第一篇,聚焦京东前端面试中JavaScript 核心基础考点(高频且必考),精选 3 道典型真题,覆盖变量作用域、原型链、异步编程三大核心模块,都是京东一面 / 二面的必问考点。
真题 1:变量作用域与提升(基础必问)
面试原题
javascript
运行
// 请说出以下代码的输出结果,并解释原因
console.log(a);
var a = 10;
function fn() {
console.log(a);
var a = 20;
}
fn();
console.log(a);
核心考点
- 变量提升(var/let/const 的区别)
- 函数作用域与全局作用域的隔离性
- 执行上下文的创建与执行阶段
解题思路与解析
第一步:输出结果
plaintext
undefined
undefined
10
第二步:逐行拆解执行过程
-
全局执行上下文阶段:
- JS 引擎在执行代码前,会先进行「变量提升」和「函数提升」:
var a被提升到全局作用域顶部,默认值为undefined;- 函数
fn整体提升到全局作用域。
- 执行
console.log(a):此时a已提升但未赋值,输出undefined; - 执行
var a = 10:全局变量a赋值为 10。
- JS 引擎在执行代码前,会先进行「变量提升」和「函数提升」:
-
执行 fn 函数(函数执行上下文):
- 进入
fn函数,先创建函数执行上下文,同样进行变量提升:- 函数内的
var a被提升到函数作用域顶部,默认值undefined;
- 函数内的
- 执行
console.log(a):此时访问的是函数内的a(未赋值),输出undefined; - 执行
var a = 20:函数内的a赋值为 20(不影响全局a)。
- 进入
-
执行全局最后一行
console.log(a):- 访问全局作用域的
a,此时值为 10,输出10。
- 访问全局作用域的
第三步:拓展延伸(京东面试追问)
- 追问 1:如果把
var a = 20改成a = 20(去掉 var),输出结果会变吗?→ 会!函数内无var声明时,a会成为全局变量,执行fn()时console.log(a)会输出 10(全局已赋值的 a),最后console.log(a)输出 20。 - 追问 2:如果把所有
var换成let,代码会报错吗?→ 会!let不存在变量提升,且有「暂时性死区」,第一行console.log(a)会直接报错ReferenceError: Cannot access 'a' before initialization。
真题 2:原型链与继承(进阶核心)
面试原题
javascript
运行
// 请完善以下代码,实现Student继承Person,要求:
// 1. Student能继承Person的name属性和sayName方法
// 2. Student新增score属性和sayScore方法
// 3. 保证原型链不紊乱,避免原型污染
function Person(name) {
this.name = name;
}
Person.prototype.sayName = function() {
console.log(`My name is ${this.name}`);
};
// 请编写Student的实现代码
// ...
// 测试代码
const stu = new Student('张三', 98);
stu.sayName(); // 输出:My name is 张三
stu.sayScore(); // 输出:My score is 98
console.log(stu instanceof Person); // 输出:true
核心考点
- 原型链的基本原理
- 构造函数继承(借用父类构造函数)
- 原型继承的正确写法
instanceof的判断逻辑
解题思路与解析
第一步:正确实现代码
javascript
运行
function Person(name) {
this.name = name;
}
Person.prototype.sayName = function() {
console.log(`My name is ${this.name}`);
};
// Student实现代码
function Student(name, score) {
// 1. 借用父类构造函数,继承实例属性
Person.call(this, name);
// 2. 新增子类实例属性
this.score = score;
}
// 3. 继承父类原型方法(避免直接赋值导致原型污染)
Student.prototype = Object.create(Person.prototype);
// 4. 修复构造函数指向(否则stu.constructor会指向Person)
Student.prototype.constructor = Student;
// 5. 新增子类原型方法
Student.prototype.sayScore = function() {
console.log(`My score is ${this.score}`);
};
// 测试代码
const stu = new Student('张三', 98);
stu.sayName(); // My name is 张三
stu.sayScore(); // My score is 98
console.log(stu instanceof Person); // true
第二步:关键知识点解析
-
Person.call(this, name):- 核心作用:调用父类构造函数,将父类的实例属性(name)挂载到子类实例上,实现「实例属性继承」;
- 若不写这行,Student 实例会丢失 name 属性。
-
Object.create(Person.prototype):- 核心作用:创建一个以 Person.prototype 为原型的新对象,赋值给 Student.prototype,实现「原型方法继承」;
- 为什么不用
Student.prototype = Person.prototype?→ 直接赋值会导致子类原型和父类原型指向同一个对象,修改子类原型方法会污染父类原型(比如给 Student.prototype 加方法,Person.prototype 也会有)。
-
Student.prototype.constructor = Student:- 因为
Object.create会覆盖 Student.prototype 的 constructor 指向,修复后才能保证stu.constructor === Student(符合原型链规范)。
- 因为
第三步:拓展延伸(京东面试追问)
- 追问 1:ES6 的
class如何实现上述继承?javascript
运行
class Person { constructor(name) { this.name = name; } sayName() { console.log(`My name is ${this.name}`); } } class Student extends Person { constructor(name, score) { super(name); // 等价于Person.call(this, name) this.score = score; } sayScore() { console.log(`My score is ${this.score}`); } } - 追问 2:
Object.create和new的区别?→Object.create(proto)仅创建一个原型指向 proto 的空对象;new 构造函数会执行构造函数,给实例添加属性,同时实例的原型指向构造函数的 prototype。
真题 3:异步编程(高频难点)
面试原题
javascript
运行
// 请说出以下代码的输出顺序,并解释原因
console.log('start');
setTimeout(() => {
console.log('setTimeout1');
Promise.resolve().then(() => {
console.log('promise1');
});
}, 0);
Promise.resolve().then(() => {
console.log('promise2');
setTimeout(() => {
console.log('setTimeout2');
}, 0);
});
console.log('end');
核心考点
- 宏任务(setTimeout/setInterval/script 整体)与微任务(Promise.then/async/await/queueMicrotask)的执行顺序
- 事件循环(Event Loop)的执行机制
解题思路与解析
第一步:输出顺序
plaintext
start
end
promise2
setTimeout1
promise1
setTimeout2
第二步:事件循环拆解
-
第一轮事件循环(执行同步代码):
- 执行
console.log('start')→ 输出 start; - 遇到
setTimeout1:属于宏任务,加入「宏任务队列」; - 遇到
Promise.resolve().then(...):then 回调属于微任务,加入「微任务队列」; - 执行
console.log('end')→ 输出 end; - 同步代码执行完毕,开始执行「微任务队列」中的任务。
- 执行
-
执行微任务队列:
- 执行
promise2的回调 → 输出 promise2; - 回调内遇到
setTimeout2:加入「宏任务队列」; - 微任务队列清空,第一轮事件循环结束。
- 执行
-
第二轮事件循环(执行宏任务队列):
- 取出第一个宏任务
setTimeout1执行 → 输出 setTimeout1; - 执行过程中遇到
Promise.resolve().then(...):加入「微任务队列」; - 宏任务执行完毕,执行「微任务队列」→ 输出 promise1;
- 微任务队列清空,第二轮事件循环结束。
- 取出第一个宏任务
-
第三轮事件循环(执行宏任务队列):
- 取出第二个宏任务
setTimeout2执行 → 输出 setTimeout2; - 无微任务,事件循环结束。
- 取出第二个宏任务
第三步:核心规则总结(必记)
- 先执行同步代码,再执行微任务,最后执行宏任务;
- 每执行完一个宏任务,必须清空当前微任务队列,再执行下一个宏任务;
- 常见微任务:Promise.then/catch/finally、async/await(本质是 Promise)、queueMicrotask;
- 常见宏任务:setTimeout/setInterval、DOM 事件、AJAX 请求、script(整体代码)。
第四步:拓展延伸(京东面试追问)
- 追问 1:如果加入
async/await,执行顺序会变吗?javascript
运行
async function fn() { console.log('async1'); await Promise.resolve(); console.log('async2'); } fn(); // 执行顺序:async1 → 同步代码 → async2(await后的代码属于微任务) - 追问 2:为什么
setTimeout(fn, 0)不是立即执行?→ 即使延迟 0ms,setTimeout 的回调仍会被加入宏任务队列,需等待同步代码和微任务执行完毕后才会执行,这是 JS 单线程和事件循环的特性。
总结
本篇聚焦京东前端面试中 JS 核心考点,核心要点如下:
- 变量提升仅针对
var和函数声明,let/const存在暂时性死区,函数作用域会隔离内部变量; - 正确的原型继承需结合「构造函数借用」和「原型链挂载」,避免直接赋值原型导致污染;
- 事件循环遵循 “同步代码→微任务→宏任务” 的执行顺序,每轮宏任务执行后需清空微任务队列。
下一篇将聚焦京东前端面试中的「DOM/BOM + 浏览器原理」考点,敬请关注!如果有疑问或想补充的考点,欢迎在评论区交流~
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)