Appearance
Day1 JS 核心:原型链、作用域、闭包、this 指向
📖 基础知识讲解【基础原理 + 高级深挖知识点】
基础原理
1. 作用域与作用域链
- JS 属于词法(静态)作用域:作用域关系在函数定义时确定,不是调用时决定。
- 作用域分类: 全局作用域、函数作用域、
let/const带来的块级作用域。 - 变量查找规则: 优先在当前作用域查找 → 一层层往外层作用域查找 → 直到全局作用域;未找到的标识符直接读取时抛出
ReferenceError。
| 声明方式 | 变量提升与访问 | 块级作用域 | 是否挂载到 window |
|---|---|---|---|
var | 声明提升,赋值前读取通常为 undefined | 没有 | 浏览器经典脚本顶层声明通常成为 window 属性;函数内、ES Module 中不会 |
let / const | 存在暂时性死区(TDZ),声明初始化前不能访问 | 有 | 不会 |
提升优先级: 同一作用域内,函数声明整体提升且优先于 var 声明;重名时函数声明先生效,后续赋值再覆盖。
js
console.log(typeof fn); // 'function' 函数声明整体提升
console.log(a); // undefined var 只提升声明
// console.log(b); // ReferenceError:b 处于 TDZ
var a = 1;
let b = 2;
function fn() {}2. 闭包
定义: 函数与其捕获的外层词法环境的组合。外层函数执行完毕后,只要内层函数仍可达,它需要访问的外层变量就可以继续保留。
常用场景:
- 私有化变量。
- 函数柯里化。
- 防抖、节流。
- 保存回调上下文。
3. JS 原型 & 原型链
一句话先记住:JS 里每个对象都有一个隐藏的
__proto__(原型),指向它的构造函数的prototype;原型本身也是对象,也有自己的__proto__,一层一层往上找,这条链式查找路径就是原型链。
3.1 什么是原型 prototype
prototype:只有函数才有,是函数身上的一个属性,是一个对象。- 作用:用来存放实例公用的方法/属性,所有 new 出来的实例对象可以共享这份 prototype 上的东西,不用每个实例都复制一份,省内存。
js
function Person(name) {
this.name = name;
}
// 写在构造函数 prototype 上,所有实例共用
Person.prototype.sayHi = function () {
console.log("hi " + this.name);
};
const p1 = new Person("张三");
p1.sayHi(); // 实例自己没有 sayHi,去 Person.prototype 拿到3.2 __proto__(隐式原型,实例身上才有)
每一个对象(实例)都有 __proto__
✅ 实例.__proto__ === 它的构造函数.prototype
js
p1.__proto__ === Person.prototype; // true⚠️ 区分:
- 函数:有
prototype(显式原型,给 new 出来的实例用)- 对象实例:有
__proto__(隐式原型,指向构造函数的 prototype)
3.3 什么是原型链
当你访问一个对象的属性/方法:
- 先在对象自身身上找,找到了直接返回
- 找不到 → 就去
obj.__proto__(也就是构造函数 prototype)上面找 - 如果原型上还找不到 → 继续拿这个原型对象的
__proto__继续往上找 - 一直找到
Object.prototype.__proto__ = null,到头了,返回undefined
这条一层一层向上查找的链条,就是原型链。
完整链条示例:
js
p1.__proto__; // Person.prototype
p1.__proto__.__proto__; // Object.prototype
p1.__proto__.__proto__.__proto__; // null 原型链终点3.4 原型链核心特点(面试高频)
- 属性查找是向上查找,读取会顺着链找;赋值只会写在当前对象自身,不会修改原型上的属性
js
Person.prototype.age = 18;
p1.age = 20; // 直接给 p1 新增 age 属性,不会改原型上面的 age
console.log(p1.age); // 20 自身优先
console.log(Person.prototype.age); // 18 原型没变instanceof的原理:沿着原型链,看右边构造函数的 prototype 是否出现在左边实例的整条原型链上
js
p1 instanceof Person; // true
p1 instanceof Object; // true- 所有普通对象最终都继承自
Object.prototype,像toString()、hasOwnProperty()这些方法都来自这里
3.5 手写极简版答案(面试口述直接说)
原型:JS 中每个构造函数都有 prototype 原型对象,用来存放实例共享的属性方法;每一个实例对象都有 __proto__ 隐式原型,指向构造函数的 prototype。
原型链:对象访问属性时,先在自身查找,找不到就顺着 __proto__ 一层一层向上查找,直到 Object.prototype.__proto__ 也就是 null 为止,这条查找链路就是原型链。
3.6 ❓ 高频追问顺带准备
prototype和__proto__的区别?hasOwnProperty作用:只判断属性是不是对象自身的,不会去原型链上查找- 原型链继承怎么实现?优缺点?
4. this 绑定四大基础形式
- 普通函数调用:默认绑定。
- 对象方法调用:隐式绑定。
new调用:构造绑定。call/apply/bind:显式指定绑定,bind返回绑定后的函数。
js
"use strict";
function show() {
return this;
}
// 1. 默认绑定:严格模式为 undefined,非严格模式为全局对象
console.log(show()); // undefined
// 2. 隐式绑定:this 指向调用它的那个对象(点号左边)
const user = {
name: "Tom",
getName() {
return this.name;
},
child: {
name: "Jerry",
getName() {
return this.name;
},
},
};
console.log(user.getName()); // 'Tom'
console.log(user.child.getName()); // 'Jerry',只看最近一层调用者
// 3. 构造绑定:this 指向新创建的实例
function Person(name) {
this.name = name;
}
console.log(new Person("Jack").name); // 'Jack'
// 4. 显式绑定:手动指定 this
function greet(greeting, mark) {
return `${greeting}, ${this.name}${mark}`;
}
console.log(greet.call(user, "hi", "!")); // 'hi, Tom!' 参数逐个传
console.log(greet.apply(user, ["hello", "?"])); // 'hello, Tom?' 参数用数组传
const boundGreet = greet.bind(user, "hey"); // bind 返回新函数,可预传参数
console.log(boundGreet("~")); // 'hey, Tom~'典型 this 丢失场景: 方法被赋值给变量单独调用、作为 setTimeout / 数组方法的回调传入、解构出对象方法后调用——这些都会退回默认绑定。
js
const obj = {
name: "obj",
getName() {
return this.name;
},
};
const fn = obj.getName;
console.log(obj.getName()); // 'obj' 隐式绑定
console.log(fn()); // 严格模式下 TypeError;非严格模式读到全局
setTimeout(obj.getName, 0); // 同样丢失,需 bind 或箭头函数包一层✨ 高级深挖知识点
1. this 绑定优先级
面试中通常按以下顺序理解普通函数的绑定规则:
text
new 绑定 > bind 硬绑定 > call / apply 显式绑定
> 对象隐式绑定 > 普通函数默认绑定关键点:
bind硬绑定后,call/apply无法修改已绑定的this。- 如果原函数可构造,使用
new调用bind返回的函数时,会忽略绑定的this,但仍保留预传参数。 - 普通函数默认调用时,严格模式下
this是undefined;非严格模式下一般是全局对象。 - 箭头函数不参与上述动态绑定规则。
js
function Tag(label) {
this.label = label;
this.who = this.name; // 构造绑定时 this.name 为 undefined
}
const a = { name: "A", Tag };
const b = { name: "B" };
// 隐式 > 默认:点号左边的 a 胜出
console.log(a.Tag("t1"), a.label, a.who); // undefined 't1' 'A'
// 显式 > 隐式:call 指定的 b 覆盖隐式的 a
a.Tag.call(b, "t2");
console.log(b.label, b.who); // 't2' 'B'
// 硬绑定 > 显式:bind 之后 call / apply 改不动
const bound = Tag.bind(a, "fixed");
bound.call(b);
console.log(a.label, a.who); // 'fixed' 'A',b 未被写入
// new > 硬绑定:忽略绑定的 a,但保留预传参数 'fixed'
const ins = new bound();
console.log(ins.label, ins.who, ins instanceof Tag); // 'fixed' undefined true
// 箭头函数不参与:call 无效,this 来自定义时的外层词法环境
const arrow = () => this;
console.log(arrow.call(a) === arrow()); // true2. 寄生组合继承
- 组合继承的缺陷: 典型实现中,设置子类原型时执行一次父构造函数,创建子类实例时又执行一次。
- 寄生组合继承: 使用
Object.create(父类.prototype)建立原型关系,仅在创建实例时调用父构造函数。 - 与 ES6 class 的关系: 实例原型链结构类似,但不能把
class extends完全等同于寄生组合继承;它还建立构造函数之间的继承,并有super、类构造器等专门语义。
3. 闭包与内存泄漏
闭包本身不是泄漏。当不再需要的闭包仍被定时器、事件监听或全局容器引用时,它捕获的对象可能无法释放。
典型场景: 组件定时器、事件监听没有清除,闭包一直持有 DOM 引用,已移除的 DOM 仍然可达。
排查工具与流程:
- 使用 Chrome DevTools 的 Memory 面板拍摄堆快照。
- 反复执行组件挂载、卸载,再比较快照。
- 查找
Detached DOM(游离 DOM)节点。 - 追溯引用链,定位定时器、监听器或闭包持有者。
- 清理后再次验证;游离节点不一定都是泄漏,需要结合生命周期判断。
4. 箭头函数特性
- 没有自己的
this和arguments;使用时从外层词法环境获取。 - 没有用于构造实例的
prototype,不能作为new的构造函数。 this在创建函数时捕获外层this,call/apply/bind无法重新指定它。- 对象字面量不产生新的
this绑定,因此箭头方法的this不会自动指向该对象。
❓ 高频面试题【基础简答 + 连环追问链 + 精简口述答案 + 项目关联话术】
题 1:什么是闭包?使用场景和缺点是什么?
精简口述答案:
闭包是函数与其捕获的外层词法环境的组合。外层函数执行完,内层函数依旧可以读取外层变量。场景包括私有变量、柯里化、防抖和节流。它会延长被捕获变量的生命周期,不合理保留引用可能导致内存泄漏,应及时清理不再需要的订阅和引用。
连环追问链:
- 追问:闭包内存泄漏如何排查定位?
- 答:使用 Chrome Memory 堆快照,查找游离 DOM 节点并追溯引用来源;页面销毁时清除定时器、解绑事件,必要时置空长期持有的引用。
- 追问:写一段柯里化示例,柯里化的好处是什么?
- 答:把多参函数转换为逐次接收参数的函数,例如
const add = a => b => a + b,调用add(1)(2)得到3;可以复用参数,并在参数收集完成后执行计算。
- 答:把多参函数转换为逐次接收参数的函数,例如
✅ 项目关联话术(按真实经历替换):
我在后台管理项目封装防抖函数,依靠闭包保存定时器 ID,搜索输入高频触发时减少接口请求;初期踩坑页面切换没有清除定时器,造成内存持续上涨,在组件销毁钩子清除定时器解决。
题 2:prototype 和 __proto__ 的区别是什么?原型链如何查找?
精简口述答案:
普通构造函数的
prototype存放实例共享的属性和方法;对象的内部原型串联原型链,通常可以通过__proto__访问,更推荐使用Object.getPrototypeOf()。访问属性时优先读取自身,找不到就沿原型链向上查找,直到null。
连环追问链:
- 追问:Object.create(null) 生成的对象有原型吗?
- 答:它的内部原型为
null,不会继承Object.prototype上的toString等方法,适合纯净字典对象。注意其obj.__proto__默认是undefined,不是null,应通过Object.getPrototypeOf(obj)验证。
- 答:它的内部原型为
- 追问:ES6 class 继承的原型关系是什么?
- 答:对普通的
class Child extends Parent,Object.getPrototypeOf(Child) === Parent,且Object.getPrototypeOf(Child.prototype) === Parent.prototype,分别建立静态成员和实例方法的继承链。
- 答:对普通的
✅ 项目关联话术(按真实经历替换):
项目封装工具类,公共方法挂载到构造函数
prototype,所有实例共用同一份函数,减少内存占用,避免每个实例重复创建函数。
题 3:this 绑定有哪几种?优先级是什么?箭头函数有什么特点?
精简口述答案:
常见绑定方式包括
new、bind、call/apply、对象方法调用和默认调用。可以按构造绑定、硬绑定、显式绑定、隐式绑定、默认绑定理解优先关系。箭头函数没有独立的this,捕获外层this,call/bind无法修改,也不能通过new实例化。
连环追问链:
- 追问:bind 绑定之后,call 还能修改 this 吗?
- 答:不能,
call/apply无法覆盖硬绑定;但原函数可构造时,new调用绑定函数会使用新实例,而非绑定对象。
- 答:不能,
- 追问:对象字面量里使用箭头函数作为方法有什么坑?
- 答:箭头函数捕获外层
this,不会指向对象自身。浏览器经典脚本顶层通常为window,ES Module 顶层则是undefined;也可能捕获包围它的函数的this,不能一概认定是window。
- 答:箭头函数捕获外层
✅ 项目关联话术(按真实经历替换):
业务开发回调经常丢失
this,早期用const that = this保存实例,现在直接使用箭头函数捕获外层组件上下文,简化代码。
✍️ 手撕代码【核心实现 + 边界 Case 自测清单 + 变种拓展题】
题 1:基础闭包计数器
核心实现:
js
function counter() {
let num = 0;
return function () {
return ++num;
};
}
const add = counter();
console.log(add()); // 1
console.log(add()); // 2✅ 边界 Case 自测: 多个 counter 实例之间计数隔离,互不干扰。
🔁 变种拓展题: 用闭包实现只能调用一次的 once,第二次起直接返回首次结果。
js
function once(fn) {
let called = false;
let result;
return function (...args) {
if (!called) {
called = true;
result = fn.apply(this, args);
}
return result;
};
}题 2:寄生组合继承(面试必背)
核心实现:
js
// 父类
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function () {
console.log(`hello ${this.name}`);
};
// 子类
function Student(name, grade) {
Person.call(this, name);
this.grade = grade;
}
// 核心:不要 new Person()
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
const s = new Student("张三", 2);
s.sayHello(); // hello 张三✅ 边界 Case 自测: 创建每个子类实例时父构造函数只执行一次;子类实例正常调用父原型方法。
🔁 变种拓展题: 手写 myNew,还原 new 的四步逻辑(创建对象 → 关联原型 → 执行构造 → 处理返回值)。
js
function myNew(Ctor, ...args) {
const obj = Object.create(Ctor.prototype);
const ret = Ctor.apply(obj, args);
// 构造函数显式返回对象时以该对象为准
return ret !== null && (typeof ret === "object" || typeof ret === "function")
? ret
: obj;
}题 3:手写 bind,支持 new 场景
核心实现(面试可默写版):
js
Function.prototype.myBind = function (context, ...args1) {
if (typeof this !== "function") {
throw new TypeError("myBind must be called on a function");
}
const fn = this;
const boundFn = function (...args2) {
// new.target 判断是否为 new 调用
const ctx = new.target ? this : context;
return fn.apply(ctx, [...args1, ...args2]);
};
// 箭头函数、class 方法等没有 prototype,需先判空
if (fn.prototype) {
boundFn.prototype = Object.create(fn.prototype);
}
return boundFn;
};✅ 边界 Case 自测: 普通调用时绑定 this 生效;new 调用时忽略绑定的 this;支持预传参数;传入箭头函数时不抛错。
此代码只演示普通可构造函数,不是原生
bind的完整实现:class 构造器不能通过apply调用;原生绑定函数不暴露自己的prototype,name、length也与此实现不同。仅在练习环境扩展Function.prototype。
🔁 变种拓展题: 顺手手写 myCall,用临时 Symbol 属性避免污染目标对象。
js
Function.prototype.myCall = function (context, ...args) {
// const ctx = context === undefined || context === null ? window : context;
const ctx =
context === null || context === undefined ? globalThis : Object(context);
const key = Symbol("fn");
ctx[key] = this;
try {
return ctx[key](...args);
} finally {
delete ctx[key];
}
};⚠️ 当日踩坑实录
| 常见坑 | 解决方案 |
|---|---|
❌ 对象字面量方法使用箭头函数,this 不指向对象自身,读取不到对象属性 | ✅ 需要动态接收者的方法使用普通函数或方法简写 |
| ❌ 闭包定时器忘记清除,组件反复挂载后定时器持续执行,可能持续持有对象 | ✅ 组件销毁时执行 clearTimeout;周期任务用 clearInterval,并解绑相关事件监听 |
❌ 在业务运行过程中直接修改 __proto__,造成原型变化并可能影响性能 | ✅ 优先在创建时使用 Object.create;确有必要才使用 Object.setPrototypeOf,它同样可能影响性能 |
❌ 把对象方法当回调直接传给 setTimeout / 数组方法,this 退回默认绑定 | ✅ 用 bind 或箭头函数包一层,保留原始接收者 |
❌ 替换子类 prototype 后忘记修正 constructor,实例 constructor 指向父类 | ✅ 紧跟一行 Child.prototype.constructor = Child |
📌 当日复盘任务
- [ ] 口述完整的
this绑定优先关系。 - [ ] 手写
bind代码,自测普通调用和new调用两种场景。 - [ ] 默写
myNew与myCall,说明返回值与this的处理差异。 - [ ] 用纯文本图画出寄生组合继承的原型链关系。
- [ ] 自己举一个闭包内存泄漏案例,口述完整排查思路。
- [ ] 将三道核心面试题与项目话术完整口述,控制在 2 分钟内。