前言

京东作为国内头部电商平台,其前端面试对基础知识的扎实度、工程化思维和业务落地能力要求极高。本系列博客将围绕京东前端面试高频真题,从「题目拆解→核心考点→解题思路→拓展延伸」四个维度,深度剖析面试题背后的考察逻辑,帮助大家不仅 “会做”,更能 “懂原理、知延伸”。

本篇作为系列第一篇,聚焦京东前端面试中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
第二步:逐行拆解执行过程
  1. 全局执行上下文阶段

    • JS 引擎在执行代码前,会先进行「变量提升」和「函数提升」:
      • var a 被提升到全局作用域顶部,默认值为undefined
      • 函数fn整体提升到全局作用域。
    • 执行console.log(a):此时a已提升但未赋值,输出undefined
    • 执行var a = 10:全局变量a赋值为 10。
  2. 执行 fn 函数(函数执行上下文)

    • 进入fn函数,先创建函数执行上下文,同样进行变量提升:
      • 函数内的var a被提升到函数作用域顶部,默认值undefined
    • 执行console.log(a):此时访问的是函数内的a(未赋值),输出undefined
    • 执行var a = 20:函数内的a赋值为 20(不影响全局a)。
  3. 执行全局最后一行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
第二步:关键知识点解析
  1. Person.call(this, name)

    • 核心作用:调用父类构造函数,将父类的实例属性(name)挂载到子类实例上,实现「实例属性继承」;
    • 若不写这行,Student 实例会丢失 name 属性。
  2. Object.create(Person.prototype)

    • 核心作用:创建一个以 Person.prototype 为原型的新对象,赋值给 Student.prototype,实现「原型方法继承」;
    • 为什么不用Student.prototype = Person.prototype?→ 直接赋值会导致子类原型和父类原型指向同一个对象,修改子类原型方法会污染父类原型(比如给 Student.prototype 加方法,Person.prototype 也会有)。
  3. 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.createnew的区别?→ 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
第二步:事件循环拆解
  1. 第一轮事件循环(执行同步代码)

    • 执行console.log('start') → 输出 start;
    • 遇到setTimeout1:属于宏任务,加入「宏任务队列」;
    • 遇到Promise.resolve().then(...):then 回调属于微任务,加入「微任务队列」;
    • 执行console.log('end') → 输出 end;
    • 同步代码执行完毕,开始执行「微任务队列」中的任务。
  2. 执行微任务队列

    • 执行promise2的回调 → 输出 promise2;
    • 回调内遇到setTimeout2:加入「宏任务队列」;
    • 微任务队列清空,第一轮事件循环结束。
  3. 第二轮事件循环(执行宏任务队列)

    • 取出第一个宏任务setTimeout1执行 → 输出 setTimeout1;
    • 执行过程中遇到Promise.resolve().then(...):加入「微任务队列」;
    • 宏任务执行完毕,执行「微任务队列」→ 输出 promise1;
    • 微任务队列清空,第二轮事件循环结束。
  4. 第三轮事件循环(执行宏任务队列)

    • 取出第二个宏任务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 核心考点,核心要点如下:

  1. 变量提升仅针对var和函数声明,let/const存在暂时性死区,函数作用域会隔离内部变量;
  2. 正确的原型继承需结合「构造函数借用」和「原型链挂载」,避免直接赋值原型导致污染;
  3. 事件循环遵循 “同步代码→微任务→宏任务” 的执行顺序,每轮宏任务执行后需清空微任务队列。

下一篇将聚焦京东前端面试中的「DOM/BOM + 浏览器原理」考点,敬请关注!如果有疑问或想补充的考点,欢迎在评论区交流~

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐