综合型前端面试一周通 - DAY1 JS基础

11 阅读9分钟

DAY 1

1. 什么是原型链?JS 的继承方式有哪些?ES6 class 和 ES5 原型继承的区别?

原型链: JS中的每个对象实例都有一个__proto__属性,其指向对应构造函数的prototype属性(原型对象),原型对象也有自己的__proto__属性,继续指向其构造函数的原型,形成链式结构,即原型链。

创建一个对象要经过哪些过程?
  1. 创建一个空对象;
  2. 将对象的__proto__属性,指向对应构造函数的prototype
  3. 将构造函数中的this指向所创建的对象,并执行构造函数;
  4. 返回对象的引用。
const myNew = (constructor, ...args) => {
    // 创建对象
    const result = {};
    // 指定prototype
    result.__proto__ = constructor.prototype;
    // 指定this指向并执行构造函数
    constructor.apply(result, args);

    return result;
}

function A(name, word) {
    this.name = name;
    this.word = word;
}

A.prototype.say = function () {
    console.log(this.name + ': ' + this.word);
}

const p1 = myNew(A, 'Name', 'Word!');
const p2 = myNew(A, 'Omg', 'World!');
p1.say();
p2.say();
JS的继承方式
  1. 原型链继承:私有属性会不小心挂到子类的prototype上,导致所有子类实例共享属性引用地址。
function Parent() {
  this.name = "parent";
}
function Child() {}
Child.prototype = new Parent(); // 核心
  1. 借用构造函数继承(call/apply):也称为传统继承,即在子类上执行父类的构造函数,但无法继承原型的属性。
function Parent() {
  this.arr = [1, 2, 3];
}
function Child() {
  Parent.call(this); // 核心
}
  1. 组合继承:既执行父类的构造函数创建私有属性,又继承父类prototype属性,但是私有属性在prototype中也会存在一份,内存浪费 + 两次构造函数执行。
function Parent() {
  this.a = [1, 2, 3];
}
function Child() {
  Parent.call(this);
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
  1. 原型式继承:使一个对象的__proto__直接指向一个对象,使其属性在原型链上,但是引用属性仍然会共享。
function createObj(proto) {
    function f() {}
    f.prototype = proto;
    return new f();
}

const proto = {'name': 'Name', 'age': 'Age'};
const obj = createObj(proto);
  1. 寄生式继承:在原型式继承外面再封装一层,强加一个属性,无法共享该属性,会导致内存浪费。
function createEnhanceObj(proto) {
    const obj = Object.create(proto);
    obj.func = function(){};
    
    return obj;
}
  1. 寄生组合式继承:为子类原型创建替身,不直接使用父类实例,既能防止引用共享,还能继承原型方法。
function Parent() {
    // this.xxx = ...
}

Parent.prototype.xxx = ...

function Child(...args) {
    Parent.call(this, ...args);
    // this.xxx = ...
}

const proto = Object.create(Parent.prototype);
proto.constructor = Child;
Child.prototype = proto;

ES6 的 class语法糖,底层仍基于原型链,但写法更清晰、更接近传统面向对象语言:

class Parent {}
class Child extends Parent {}

2.什么是闭包?核心原理是什么?在项目中哪里可以使用闭包?

闭包: 闭包 = 函数 + 该函数声明时所在的词法作用域

核心原理: 在JS函数被创建时,就确定了词法作用域,并记录下作用域链;当函数执行结束时,内部变量本该被销毁,但如果函数内部仍持有对外部作用域的引用,则会被垃圾回收机制认为可达,不被回收;这部分变量便存储在内存中,可被持续访问,实现状态的持久化暂存。

可使用的范围: 例如防抖节流用于缓存定时器;柯里化用于存储前面参数的执行结果等。

3.闭包会导致内存泄漏吗?为什么?如何避免?

闭包本身不会直接导致内存泄漏错误 / 不当使用闭包才会造成内存泄漏。

闭包长期持有不再需要的变量 / DOM 元素 / 定时器引用,且没有手动释放时:

  • 垃圾回收机制判定这些对象仍然可达
  • 无法回收内存
  • 内存持续占用、累积 → 最终内存泄漏

「如何避免?」

  1. 及时清理定时器不用时调用 clearTimeout / clearInterval
  2. DOM 事件解绑元素销毁前,移除通过闭包绑定的事件监听
  3. 手动解除无用引用将闭包内不再使用的变量赋值为 null
  4. 控制闭包生命周期不要让闭包意外长期存活(如挂载到全局变量)
  5. 最小化闭包持有作用域只在必要场景使用闭包,不滥用

4.this 的绑定规则有哪些?分别举一个例子。

new 绑定 > 显式绑定(call/apply/bind)> 隐式绑定 > 默认绑定

1. 隐式绑定 vs 默认绑定

结果:隐式 > 默认

const obj = {
  name: 'obj',
  fn: function() {
    console.log(this.name);
  }
};

// 隐式调用
obj.fn(); // obj

// 拿出来独立调用(默认绑定)
const f = obj.fn;
f(); // undefined(默认绑定到 window/undefined)

谁赢:隐式

2. 显式绑定 vs 隐式绑定

结果:显式 > 隐式

const obj = { name: 'obj' };
const other = { name: 'other' };

function fn() {
  console.log(this.name);
}

// 隐式绑定
obj.fn = fn;
obj.fn(); // obj

// 显式绑定 call,覆盖隐式
obj.fn.call(other); // other

谁赢:显式

3. new 绑定 vs 显式绑定(bind)

结果:new > 显式

function Person(name) {
  this.name = name;
}

const obj = {};
// 强行把 this 绑定到 obj
const BoundPerson = Person.bind(obj);

// 1. 普通调用:this 是 obj(显式生效)
BoundPerson('aaa');
console.log(obj.name); // aaa

// 2. new 调用:显式被无视,this 指向新实例
const p = new BoundPerson('bbb');
console.log(p.name); // bbb
console.log(obj.name); // 还是 aaa,没被修改

谁赢:new

一句话总结优先级
  • 函数自己跑:默认
  • 对象。方法跑:隐式(盖过默认)
  • call/apply/bind 跑:显式(盖过隐式)
  • new 函数:new(盖过一切)

5. 箭头函数的 this 和普通函数有什么区别?为什么箭头函数不能当构造函数?

  • 普通函数

    • this动态绑定的,由调用方式决定(默认、隐式、显式、new 绑定);
    • 谁调用,this 就指向谁,运行时才能确定
  • 箭头函数

    • 箭头函数没有自己的 this,它的 this词法作用域继承来的;
    • 继承自定义时所在外层作用域的 this定义时就已确定
    • 无法通过 call/apply/bindnew 去修改它的 this

6. 什么是事件循环?浏览器的事件循环执行流程是什么?

JavaScript 是单线程语言,同一时间只能做一件事。为了处理异步任务、避免代码阻塞,JS 设计了事件循环(Event Loop)机制,用来调度同步任务、异步任务的执行顺序,是 JS 实现非阻塞异步编程的核心。

执行流程:

  1. 执行全局同步代码,推入调用栈依次执行,同步代码执行完毕后调用栈清空。

  2. 立即清空微任务队列:取出所有微任务,依次执行,直到微任务队列为空。

  3. 执行浏览器渲染更新(重排、重绘)。

  4. 宏任务队列中取出第一个宏任务执行,执行完毕。

  5. 重复步骤 2 → 3 → 4,不断循环。

7. 宏任务和微任务分别包含哪些?优先级如何?

同步代码 > 微任务 > 宏任务。

宏任务:

  • 整个 <script> 代码块

  • setTimeout / setInterval

  • 网络请求回调:fetch / ajax / 动态加载 script

  • 用户交互事件:clickmousemove 等回调

注意: requestAnimationFrame在微任务队列清空后、GUI渲染之前,不属于常规意义上的宏任务。

微任务: Promise.then|catch|finally、queueMicrotask(() => {})、MutarionObserver(监听DOM变化)

8. async/await 的执行顺序是什么?结合 Promise 分析。

async函数中的内容同步执行,await后紧跟的表达式也是同步执行的,函数剩余代码等价于Promise.then

async function test() {
  // 1. 这行:async 函数内同步执行
  console.log(1)

  // 2. await 后面的表达式:同步执行
  await console.log(2)

  // 3. await 之后的代码:微任务,异步执行
  console.log(3)
}

console.log('start')
test()
console.log('end')

输出: start、1、2、end、3。

9. Promise 有哪几种状态?状态可以改变吗?

Promise有三种状态pending、resolved、rejected,状态在pending时可以改变,当变为resolved、rejected后,就不可改变了。

const p = new Promise((resolve, reject) => {
  resolve(1) // 状态变为 fulfilled
  reject(2)  // 无效,状态已凝固
  resolve(3) // 同样无效
})

10. Promise 的 then/catch/finally 有什么特点?

then、catch、finally 的共同特点
  1. 都会返回一个新的 Promise,因此可以链式调用
  2. 回调函数都是微任务,在同步代码执行完后执行
  3. 都不会改变原 Promise 的状态,只影响返回的新 Promise
then 的特点
  • 处理 Promise 成功(fulfilled) 的结果
  • 接收两个参数:onFulfilledonRejected
  • 回调内部正常 return → 新 Promise 变为成功
  • 回调内部抛错 /return 失败 Promise → 新 Promise 变为失败
  • 没有传对应回调时,会透传状态和值到下一个链式方法
catch 的特点
  • 本质是 then(null, onRejected)语法糖

  • 专门捕获失败(rejected) 和整条链上的错误

  • 默认情况下,catch 捕获错误后会消化错误

    • 内部不抛错 → 返回的新 Promise 变为成功
    • 内部再次抛错 → 新 Promise 才是失败
finally 的特点
  • 无论成功还是失败,回调一定会执行
  • 不接收参数,拿不到结果或错误
  • 默认透传原来的状态和值,不改变 Promise 链走向
  • 只有在 finally 内部抛错时,才会让新 Promise 变为失败
  • 常用于:关闭 loading、释放资源、清理操作

11. 什么是回调地狱?Promise 是如何解决回调地狱的?

回调地狱(Callback Hell) ,就是多层异步回调函数层层嵌套,代码形成向右缩进的 “金字塔” 结构,导致:

  • 代码可读性极差、难以维护
  • 错误处理混乱,每个回调都要单独捕获错误
  • 逻辑耦合严重,改一处牵一发而动全身

本质就是:异步逻辑用嵌套回调写,代码横向膨胀,完全违背线性阅读习惯

Promise 靠扁平化的链式调用,把横向嵌套改成纵向线性执行,彻底干掉嵌套结构,同时统一错误处理。

12. 什么是浅拷贝 / 深拷贝?区别是什么?

浅拷贝只复制对象 / 数组的第一层,深拷贝递归复制对象 / 数组的所有层级, 拷贝的本质区别就是:是否递归复制嵌套的引用类型,是否共用同一块内存

13. 数组的常用方法有哪些?哪些是纯函数?

“头尾增删,剪接排序反转”

  • 头:unshift(头加)、shift(头删)
  • 尾:push(尾加)、pop(尾删)
  • 剪接:splice(万能删插)
  • 排序:sort
  • 反转:reverse

只要出现这 7 个 → 不纯除此之外的数组方法 → 全是纯函数

比如:map / filter / reduce / slice / concat``find / some / every / includes / join / flat...

14. 什么是柯里化?函数柯里化的核心作用是什么?

柯里化是将具有多个入参的函数,转换为一系列单个入参函数,当参数收集完整后,再执行逻辑的技术。

核心作用是:复用 + 延迟

通用柯里化方法:

function curry(fn) {
    return function curried(...args) {
        if(args.length >= fn.length) {
            return fn.call(this, ...args);
        }
        else {
            return (...restArgs) => curried(...args, ...restArgs);
        }
    }
}