JS 回顾 L
约 1773 字大约 6 分钟
2026-01-25
构造函数 & 原型体系
构造函数:用来“批量造对象”的函数(配合 new)
原型(prototype):构造函数给实例共享方法/属性的地方
隐式原型([[Prototype]] / __proto__):实例指向其原型的链接
原型链:属性查找一路沿着 [[Prototype]] 往上找,直到 null
继承:让一个类型“复用另一个类型”的属性/方法(本质:链接原型链 + 复用构造逻辑)
构造函数(Constructor)
function Person(name) {
this.name = name;
}
const obj = new Person("A");new Person("A") 背后大概做了四步:
- 创建一个新对象
obj - 把
obj.[[Prototype]]指向Person.prototype - 执行构造函数:
Person.call(obj, "A"),返回obj(除非构造函数显式返回一个对象)
构造函数里 this 指向 新创建的实例对象,不用 new 调用会出问题(严格模式下 this 是 undefined)
原型(prototype)
每个函数都有 prototype(用于 new)
Person.prototype.sayHi = function () {
console.log("hi");
};Person.prototype 是一个对象
所有由 new Person() 创建的实例 都会共享 Person.prototype 上的方法,好处:省内存(方法不用每个实例都拷贝一份)
每个对象都有 [[Prototype]](隐式原型),实例对象上有一条“指向原型”的链接:标准访问:Object.getPrototypeOf(obj),非标准(但常见):obj.__proto__
Object.getPrototypeOf(p) === Person.prototype // true原型链(Prototype Chain)
属性查找规则
访问 p.xxx 时:
- 先看
p自己有没有xxx - 没有就去
p.[[Prototype]](也就是Person.prototype)找 - 还没有继续往上找(
Object.prototype) - 直到
null为止
链条通常长这样:
p
-> Person.prototype
-> Object.prototype
-> nullp instanceof Person // true本质:判断 Person.prototype 是否出现在 p 的原型链上。
constructor
Person.prototype.constructor === Person // true(默认)constructor 是原型对象上默认带的属性,指回“构造函数本身”
继承
目标:让 Child 继承 Parent:
- 继承 实例属性(构造函数里
this.xxx) - 继承 原型方法(
Parent.prototype.xxx)
经典写法:组合继承
function Parent(name) {
this.name = name;
}
Parent.prototype.say = function () {
console.log(this.name);
};
function Child(name, age) {
Parent.call(this, name); // 继承实例属性
this.age = age;
}
// 继承原型方法(关键)
Child.prototype = Object.create(Parent.prototype);
// 修正 constructor
Child.prototype.constructor = Child;
Child.prototype.play = function () {
console.log("play");
};Object.create 会创建一个新对象,并让这个新对象的 [[Prototype]] 指向 Parent.prototype,也就是“搭起原型链”。
ES6 class 其实是语法糖
class Parent {
constructor(name) { this.name = name; }
say() { console.log(this.name); }
}
class Child extends Parent {
constructor(name, age) {
super(name); // Parent.call(this, name)
this.age = age;
}
play() { console.log("play"); }
}extends 帮你处理原型链,super() 帮你调用父构造函数,底层仍然是原型那套机制
数组
String.prototype.replaceAll() 用正则表达式或搜索字符串替换字符串中所有子字符串的实例。
for-of 用于遍历数组或伪数组(document.querySelectorAll)
Array.isArray() 判断该变量是不是一个真数组
Array.from() 用于将伪数组对象或可迭代对象转换为数组
IIFE
IIFE(Immediately Invoked Function Expression,立即调用函数表达式),它主要是为了解决 作用域隔离 和 全局变量污染 这些老问题的。
(function () {
// 这里是一个“独立的小世界”
const x = 1;
console.log(x);
})(); // 立刻执行或者箭头版:
(() => {
// ...
})();IIFE 执行时会创建一个函数作用域(现在也可以理解为一个“封闭块”),里面定义的变量:外面访问不到,不会跑到全局去
(function () {
var a = 123;
})();
console.log(a); // ReferenceError: a is not defined这就是它的核心用途:把变量关起来。
防止全局变量被污染:以前(尤其在 ES6 之前)var 没有块级作用域,写多了就很容易:不小心在全局挂一堆东西(或覆盖别人),多个脚本文件变量撞名
IIFE 的经典用途就是:
- 库/插件内部用 IIFE 把内部变量封装起来
- 只往外暴露一个必要的全局入口(比如
window.$)
const MyLib = (function () {
const secret = "hidden";
function hi() { return "hello"; }
return { hi }; // 只暴露 hi
})();
console.log(MyLib.hi()); // hello
// console.log(secret); // 访问不到var:函数作用域,IIFE 能隔离 var
let/const:块级作用域,即使不用 IIFE,也能用 {} 隔离
requestAnimationFrame
requestAnimationFrame(fn):把 fn 放进“下一次浏览器准备渲染这一帧之前”执行的队列里。
不是立刻执行,不是定时器,不是宏任务 / 微任务之一
它是 浏览器渲染流程专用的一类回调。
浏览器主线程在不停做一个循环(事件循环 + 渲染管线),一次循环 ≈ 一次“机会”去画一帧。
一个完整的大循环,按顺序是这样的(这是重点):
1️⃣ 执行一个【宏任务(Task)】
来源包括:
script(最初的 JS)setTimeout / setInterval- DOM 事件(click、scroll…)
- 网络回调(fetch 完成)
setTimeout(() => {
console.log('timeout');
}, 0);⚠️ 一次循环只执行一个宏任务
2️⃣ 清空【微任务队列(Microtask)】
包括:
Promise.then / catch / finallyqueueMicrotaskMutationObserver
规则:微任务会一直执行,直到队列清空
Promise.resolve().then(() => {
console.log('microtask');
});⚠️ 如果微任务不停加自己 → 页面会卡死(渲染永远等不到)
3️⃣ 现在,JS 暂时“安静”了
此时浏览器会问一句:👉 “我现在要不要渲染一帧?”
- 页面有变化(DOM / style 改了)
- 到了屏幕刷新时间(比如 16.7ms)
如果 不需要 → 直接回到下一轮宏任务
如果 需要 → 进入渲染阶段
requestAnimationFrame 就在“真正渲染之前”插一脚。
3️⃣ 渲染前检查
执行所有 requestAnimationFrame 回调
requestAnimationFrame(() => {
// 此时:
// - 微任务已经执行完
// - 这一帧还没画
});这是 rAF 的唯一执行点。
渲染流水线开始,按顺序:
- style(计算 CSS)
- layout(回流)
- paint(绘制)
- composite(合成)
然后:屏幕显示这一帧
rAF 不是宏任务(不会进入 task queue),也不是微任务(不会插队 Promise),是 渲染前回调队列(render callback queue)
可以把它理解成:浏览器在“准备画这一帧”之前,专门开的一个小窗口
rAF 受宏任务影响,如果宏任务太长:
setTimeout(() => {
const start = Date.now();
while (Date.now() - start < 50) {} // 卡 50ms
}, 0);结果:
- 主线程被占用
- 下一帧渲染被推迟
- rAF 也只能等
rAF 永远等 JS 空闲
rAF 受微任务影响,而且微任务优先级更高。
requestAnimationFrame(() => {
console.log('rAF');
});
Promise.resolve().then(() => {
console.log('microtask');
});
// 执行顺序:
// microtask
// rAF原因:微任务 必须清空,rAF 必须等微任务结束
rAF 不会在同一轮事件循环里执行,rAF 一定在 下一帧渲染前
console.log('start');
requestAnimationFrame(() => {
console.log('rAF');
});
console.log('end');
// 输出
// start
// end
// rAFrequestAnimationFrame
- 只在 需要渲染时执行
- 和屏幕刷新率对齐(60Hz / 120Hz)
- 标签页不可见时会 暂停
- 天然适合动画 / 视觉更新
经典动画写法(递归 rAF)
function animate(time) {
// time 是高精度时间戳
// 用来算位移、速度
update(time);
render();
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);注意:
- rAF 不会自动循环
- 每一帧你都得“再申请一次”
总结
requestAnimationFrame = 渲染前回调
执行时机:宏任务 → 微任务 → rAF → 渲染
不属于事件循环队列,只在浏览器准备绘制新一帧时执行,是动画 / DOM 可视更新的最优选择