DAY 1
1. 什么是原型链?JS 的继承方式有哪些?ES6 class 和 ES5 原型继承的区别?
原型链: JS中的每个对象实例都有一个__proto__属性,其指向对应构造函数的prototype属性(原型对象),原型对象也有自己的__proto__属性,继续指向其构造函数的原型,形成链式结构,即原型链。
创建一个对象要经过哪些过程?
- 创建一个空对象;
- 将对象的
__proto__属性,指向对应构造函数的prototype; - 将构造函数中的
this指向所创建的对象,并执行构造函数; - 返回对象的引用。
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的继承方式
- 原型链继承:私有属性会不小心挂到子类的prototype上,导致所有子类实例共享属性引用地址。
function Parent() {
this.name = "parent";
}
function Child() {}
Child.prototype = new Parent(); // 核心
- 借用构造函数继承(call/apply):也称为传统继承,即在子类上执行父类的构造函数,但无法继承原型的属性。
function Parent() {
this.arr = [1, 2, 3];
}
function Child() {
Parent.call(this); // 核心
}
- 组合继承:既执行父类的构造函数创建私有属性,又继承父类prototype属性,但是私有属性在prototype中也会存在一份,内存浪费 + 两次构造函数执行。
function Parent() {
this.a = [1, 2, 3];
}
function Child() {
Parent.call(this);
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
- 原型式继承:使一个对象的__proto__直接指向一个对象,使其属性在原型链上,但是引用属性仍然会共享。
function createObj(proto) {
function f() {}
f.prototype = proto;
return new f();
}
const proto = {'name': 'Name', 'age': 'Age'};
const obj = createObj(proto);
- 寄生式继承:在原型式继承外面再封装一层,强加一个属性,无法共享该属性,会导致内存浪费。
function createEnhanceObj(proto) {
const obj = Object.create(proto);
obj.func = function(){};
return obj;
}
- 寄生组合式继承:为子类原型创建替身,不直接使用父类实例,既能防止引用共享,还能继承原型方法。
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 元素 / 定时器引用,且没有手动释放时:
- 垃圾回收机制判定这些对象仍然可达
- 无法回收内存
- 内存持续占用、累积 → 最终内存泄漏
「如何避免?」
- 及时清理定时器不用时调用
clearTimeout/clearInterval - DOM 事件解绑元素销毁前,移除通过闭包绑定的事件监听
- 手动解除无用引用将闭包内不再使用的变量赋值为
null - 控制闭包生命周期不要让闭包意外长期存活(如挂载到全局变量)
- 最小化闭包持有作用域只在必要场景使用闭包,不滥用
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/bind或new去修改它的this。
- 箭头函数没有自己的 this,它的
6. 什么是事件循环?浏览器的事件循环执行流程是什么?
JavaScript 是单线程语言,同一时间只能做一件事。为了处理异步任务、避免代码阻塞,JS 设计了事件循环(Event Loop)机制,用来调度同步任务、异步任务的执行顺序,是 JS 实现非阻塞异步编程的核心。
执行流程:
执行全局同步代码,推入调用栈依次执行,同步代码执行完毕后调用栈清空。
立即清空微任务队列:取出所有微任务,依次执行,直到微任务队列为空。
执行浏览器渲染更新(重排、重绘)。
从宏任务队列中取出第一个宏任务执行,执行完毕。
重复步骤 2 → 3 → 4,不断循环。
7. 宏任务和微任务分别包含哪些?优先级如何?
同步代码 > 微任务 > 宏任务。
宏任务:
-
整个
<script>代码块 -
setTimeout/setInterval -
网络请求回调:
fetch/ajax/ 动态加载 script -
用户交互事件:
click、mousemove等回调
注意: 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 的共同特点
- 都会返回一个新的 Promise,因此可以链式调用
- 回调函数都是微任务,在同步代码执行完后执行
- 都不会改变原 Promise 的状态,只影响返回的新 Promise
then 的特点
- 处理 Promise 成功(fulfilled) 的结果
- 接收两个参数:
onFulfilled、onRejected - 回调内部正常 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);
}
}
}