Skip to content

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 什么是原型链 ​

当你访问一个对象的属性/方法:

  1. 先在对象自身身上找,找到了直接返回
  2. 找不到 → 就去 obj.__proto__(也就是构造函数 prototype)上面找
  3. 如果原型上还找不到 → 继续拿这个原型对象的 __proto__ 继续往上找
  4. 一直找到 Object.prototype.__proto__ = null,到头了,返回 undefined

这条一层一层向上查找的链条,就是原型链。

完整链条示例:

js
p1.__proto__; // Person.prototype
p1.__proto__.__proto__; // Object.prototype
p1.__proto__.__proto__.__proto__; // null 原型链终点
3.4 原型链核心特点(面试高频) ​
  1. 属性查找是向上查找,读取会顺着链找;赋值只会写在当前对象自身,不会修改原型上的属性
js
Person.prototype.age = 18;
p1.age = 20; // 直接给 p1 新增 age 属性,不会改原型上面的 age
console.log(p1.age); // 20 自身优先
console.log(Person.prototype.age); // 18 原型没变
  1. instanceof 的原理:沿着原型链,看右边构造函数的 prototype 是否出现在左边实例的整条原型链上
js
p1 instanceof Person; // true
p1 instanceof Object; // true
  1. 所有普通对象最终都继承自 Object.prototype,像 toString()、hasOwnProperty() 这些方法都来自这里
3.5 手写极简版答案(面试口述直接说) ​

原型:JS 中每个构造函数都有 prototype 原型对象,用来存放实例共享的属性方法;每一个实例对象都有 __proto__ 隐式原型,指向构造函数的 prototype。

原型链:对象访问属性时,先在自身查找,找不到就顺着 __proto__ 一层一层向上查找,直到 Object.prototype.__proto__ 也就是 null 为止,这条查找链路就是原型链。

3.6 ❓ 高频追问顺带准备 ​
  • prototype 和 __proto__ 的区别?
  • hasOwnProperty 作用:只判断属性是不是对象自身的,不会去原型链上查找
  • 原型链继承怎么实现?优缺点?

4. this 绑定四大基础形式 ​

  1. 普通函数调用:默认绑定。
  2. 对象方法调用:隐式绑定。
  3. new 调用:构造绑定。
  4. 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()); // true

2. 寄生组合继承 ​

  • 组合继承的缺陷: 典型实现中,设置子类原型时执行一次父构造函数,创建子类实例时又执行一次。
  • 寄生组合继承: 使用 Object.create(父类.prototype) 建立原型关系,仅在创建实例时调用父构造函数。
  • 与 ES6 class 的关系: 实例原型链结构类似,但不能把 class extends 完全等同于寄生组合继承;它还建立构造函数之间的继承,并有 super、类构造器等专门语义。

3. 闭包与内存泄漏 ​

闭包本身不是泄漏。当不再需要的闭包仍被定时器、事件监听或全局容器引用时,它捕获的对象可能无法释放。

典型场景: 组件定时器、事件监听没有清除,闭包一直持有 DOM 引用,已移除的 DOM 仍然可达。

排查工具与流程:

  1. 使用 Chrome DevTools 的 Memory 面板拍摄堆快照。
  2. 反复执行组件挂载、卸载,再比较快照。
  3. 查找 Detached DOM(游离 DOM)节点。
  4. 追溯引用链,定位定时器、监听器或闭包持有者。
  5. 清理后再次验证;游离节点不一定都是泄漏,需要结合生命周期判断。

4. 箭头函数特性 ​

  • 没有自己的 this 和 arguments;使用时从外层词法环境获取。
  • 没有用于构造实例的 prototype,不能作为 new 的构造函数。
  • this 在创建函数时捕获外层 this,call / apply / bind 无法重新指定它。
  • 对象字面量不产生新的 this 绑定,因此箭头方法的 this 不会自动指向该对象。

❓ 高频面试题【基础简答 + 连环追问链 + 精简口述答案 + 项目关联话术】 ​

题 1:什么是闭包?使用场景和缺点是什么? ​

精简口述答案:

闭包是函数与其捕获的外层词法环境的组合。外层函数执行完,内层函数依旧可以读取外层变量。场景包括私有变量、柯里化、防抖和节流。它会延长被捕获变量的生命周期,不合理保留引用可能导致内存泄漏,应及时清理不再需要的订阅和引用。

连环追问链:

  1. 追问:闭包内存泄漏如何排查定位?
    • 答:使用 Chrome Memory 堆快照,查找游离 DOM 节点并追溯引用来源;页面销毁时清除定时器、解绑事件,必要时置空长期持有的引用。
  2. 追问:写一段柯里化示例,柯里化的好处是什么?
    • 答:把多参函数转换为逐次接收参数的函数,例如 const add = a => b => a + b,调用 add(1)(2) 得到 3;可以复用参数,并在参数收集完成后执行计算。

✅ 项目关联话术(按真实经历替换):

我在后台管理项目封装防抖函数,依靠闭包保存定时器 ID,搜索输入高频触发时减少接口请求;初期踩坑页面切换没有清除定时器,造成内存持续上涨,在组件销毁钩子清除定时器解决。

题 2:prototype 和 __proto__ 的区别是什么?原型链如何查找? ​

精简口述答案:

普通构造函数的 prototype 存放实例共享的属性和方法;对象的内部原型串联原型链,通常可以通过 __proto__ 访问,更推荐使用 Object.getPrototypeOf()。访问属性时优先读取自身,找不到就沿原型链向上查找,直到 null。

连环追问链:

  1. 追问:Object.create(null) 生成的对象有原型吗?
    • 答:它的内部原型为 null,不会继承 Object.prototype 上的 toString 等方法,适合纯净字典对象。注意其 obj.__proto__ 默认是 undefined,不是 null,应通过 Object.getPrototypeOf(obj) 验证。
  2. 追问: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 实例化。

连环追问链:

  1. 追问:bind 绑定之后,call 还能修改 this 吗?
    • 答:不能,call / apply 无法覆盖硬绑定;但原函数可构造时,new 调用绑定函数会使用新实例,而非绑定对象。
  2. 追问:对象字面量里使用箭头函数作为方法有什么坑?
    • 答:箭头函数捕获外层 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 分钟内。