🌀 小Dora 的 JS 修炼日记 · Day 7
"写 polyfill,不只是为了面试,而是走进 JS 引擎脑子里的最近通道。"
------dora · 准高级前端工程师
🌟 开篇:四大函数机制是怎么被 JS 引擎执行的?
new
:你以为只是创建对象?其实背后隐藏了 V8 的 Hidden Class 分配 + 构造绑定策略call
/apply
:你以为只是换个this
?其实是 JS 上下文切换 + Inline Cache 的血泪史bind
:你以为是懒执行?其实 V8 想优化都优化不了,还会禁用内联!
我们要掌握的,不只是 API 行为,而是 👇
函数机制 | 执行过程 | V8 处理重点 | 性能影响 |
---|---|---|---|
new |
创建对象 + 构造函数调用 | HiddenClass 状态迁移 | 构造对象不一致会触发 deopt |
call/apply |
上下文切换,立即调用 | Inline Cache 路径匹配 | 多变 this 会失去优化 |
bind |
延迟绑定 this 返回闭包 | 函数不可预测性高 | V8 无法内联,优化死角 |
🔧 一、手写 new
操作符 + 底层流程
javascript
function myNew(Ctor, ...args) {
const obj = Object.create(Ctor.prototype); // 模拟原型链挂载
const result = Ctor.apply(obj, args); // 执行构造函数
return result instanceof Object ? result : obj;
}
🧠 底层发生了什么?
- 申请内存空间(堆)
- 创建隐藏类(Hidden Class)
- this 绑定到新对象
- 构造函数执行
- 返回对象
🐛 示例陷阱题:
ini
function A() {
this.name = '小吴';
return { age: 26 };
}
const res = myNew(A);
console.log(res.name); // ❓ undefined or 小吴?
✅ 解析:构造函数返回对象,会覆盖 new 创建的 this,所以 name 是 undefined。
🔧 二、手写 call
/ apply
ini
Function.prototype.myCall = function(ctx, ...args) {
ctx = ctx || globalThis;
const fn = Symbol();
ctx[fn] = this;
const result = ctx[fn](...args);
delete ctx[fn];
return result;
};
👇 题目验证理解:
javascript
function say(a, b) {
console.log(this.name, a, b);
}
const obj = { name: '小吴' };
say.call(obj, 'Hello', 'World'); // 小吴 Hello World
say.myCall(obj, 'Hi', 'V8'); // 小吴 Hi V8
✅ call/apply 的实质:临时把函数挂在 obj 上执行,然后删除
🔧 三、手写 bind
javascript
Function.prototype.myBind = function(ctx, ...args) {
const originFn = this;
function bound(...restArgs) {
const finalCtx = this instanceof bound ? this : ctx;
return originFn.apply(finalCtx, [...args, ...restArgs]);
}
bound.prototype = Object.create(originFn.prototype);
return bound;
};
🧪 测试继承 + 构造:
ini
function Person(name) {
this.name = name;
}
const BoundPerson = Person.myBind({});
const p = new BoundPerson('小吴');
console.log(p.name); // ✅ 小吴
📛 误区题目:
ini
const obj = { name: 'V8' };
function foo() {
console.log(this.name);
}
const bound = foo.bind(obj);
const newFoo = new bound();
🔍 注意:当用 new
调用 bind 结果时,this
会忽略绑定的 obj,绑定到新创建对象。
🔬 四、V8 背后的执行模型(执行栈 + Hidden Class)
call/apply
会触发函数上下文切换:push stack → bind this → run → pop
bind
返回闭包,闭包结构复杂,V8 无法内联展开,性能差new
会判断构造函数是否符合 inline 构造路径(不能随意返回对象!)
Hidden Class 的影响:
ini
function A() {
this.x = 1;
}
const a1 = new A();
const a2 = new A();
a1.y = 2; // ⚠️ 改变 Hidden Class,性能损
🔍 五、典型面试题 + 实战题自测
题 1:手写一个组合继承函数
ini
function Parent(name) {
this.name = name;
}
Parent.prototype.say = function() {
return this.name;
};
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
👉 这段代码能否被 V8 优化?为何?
题 2:输出结果分析
scss
function Foo() {}
Foo.prototype.test = function() {
console.log(this);
};
const f = new Foo();
const test = f.test;
test(); // ❓
f.test(); // ❓
test.call(f); // ❓
题 3:bind 后还能再 call 吗?
ini
function test() {
console.log(this.name);
}
const obj = { name: '小吴' };
const bindFn = test.bind(obj);
bindFn.call({ name: 'V8' }); // 输出?
✅ 输出是 "小吴",因为 bind 优先级更高。
📋 📌 函数调用机制专项自查 Checklist
自查点 | 是否掌握 |
---|---|
能否手写 new 实现并解释 proto 绑定 | ✅ |
知道 call/apply 原理及内存释放机制 | ✅ |
bind 的延迟执行及构造 this 替换规则 | ✅ |
V8 中 call 的 Inline Cache 原理 | ✅ |
bind 无法内联优化的底层限制 | ✅ |
函数上下文与 this 的绑定顺序 | ✅ |
构造函数返回对象 VS 返回原始值 | ✅ |
new + bind 的优先级理解 | ✅ |
💡 总结一句话
call
是函数扮演别人,apply
是换衣服一起上,
bind
是懒汉型"认主",而new
是新生儿找爹。
而 V8 背后,会对每个调用路径打上优化或惩罚标签,你写的每一行代码,都在引擎眼皮底下"评优评级" 。