CodeL
以前端为翼,以 AI 为脑,向全栈而行
2026-03-31

JavaScript 闭包与原型链

JavaScript 闭包与原型链:从零基础到完全理解 一篇文章彻底搞懂 JavaScript 最核心的两个概念:闭包与原型链,附完整代码示例和实战场景 一、前置知识:JavaScript 的核心特性 1.1 为什么闭包...

JavaScript 闭包与原型链:从零基础到完全理解 #

一篇文章彻底搞懂 JavaScript 最核心的两个概念:闭包与原型链,附完整代码示例和实战场景


一、前置知识:JavaScript 的核心特性 #

1.1 为什么闭包和原型链这么重要? #

JavaScript 有两个独特的特性,是其他语言少有的:

特性 作用 为什么重要
闭包 函数可以"记住"它创建时的变量 实现私有变量、模块化、回调函数
原型链 对象可以继承其他对象的属性 实现继承、节省内存、面向对象编程

这两个概念是 JavaScript 的"灵魂",不理解它们就无法真正理解 JS。

1.2 先理解几个基础概念 #

在讲闭包和原型链之前,先复习几个基础概念:

(1)变量作用域

// 全局变量 —— 在任何地方都能访问
var globalVar = '我是全局变量';
 
function test() {
  // 局部变量 —— 只在函数内部能访问
  var localVar = '我是局部变量';
  
  console.log(globalVar);  // ✅ 能访问
  console.log(localVar);   // ✅ 能访问
}
 
test();
console.log(globalVar);  // ✅ 能访问
console.log(localVar);   // ❌ 报错:localVar is not defined

(2)函数嵌套

// 函数里面可以定义函数
function outer() {
  var outerVar = '外层变量';
  
  function inner() {
    var innerVar = '内层变量';
    console.log(outerVar);  // ✅ 内层函数能访问外层变量
    console.log(innerVar);  // ✅ 能访问自己的变量
  }
  
  inner();
  // console.log(innerVar);  // ❌ 外层不能访问内层变量
}
 
outer();

(3)作用域链

// 变量的查找规则:从内往外找
var a = '全局a';
 
function outer() {
  var a = '外层a';  // 同名变量,会"覆盖"全局的
  
  function inner() {
    var a = '内层a';
    console.log(a);  // 输出: 内层a(先在当前找)
  }
  
  inner();
  console.log(a);   // 输出: 外层a
}
 
outer();
console.log(a);     // 输出: 全局a
 
// 查找顺序:当前作用域 → 外层作用域 → ... → 全局作用域

二、闭包(Closure)详解 #

2.1 闭包是什么?(大白话解释) #

闭包 = 函数 + 函数能访问的外部变量

打个比方:

你有一个"背包"(函数),背包里装着你出发时带的东西(变量)。无论你走到哪里,背包里的东西都还在。即使你已经离开了出发的地方(函数执行结束),背包里的东西依然可以使用。

// 最简单的闭包示例
function createCounter() {
  let count = 0;  // 这个变量"被困"在闭包里
  
  return function() {  // 返回的函数就是一个闭包
    count++;           // 它能访问外部的 count
    return count;
  };
}
 
const counter = createCounter();  // createCounter 执行完毕
// 此时 count 变量应该消失了...但是!
 
console.log(counter());  // 输出: 1  ✅ count 还活着!
console.log(counter());  // 输出: 2  ✅ count 继续增加
console.log(counter());  // 输出: 3  ✅ count 还在!
 
// 这就是闭包:函数"记住"了它创建时的变量

核心定义:

闭包是指函数与其词法环境(创建时能访问的变量)的组合。即使函数在其原始作用域之外执行,它仍然能访问那些变量。

2.2 闭包的形成条件 #

闭包在以下情况自动形成:

// 条件1:函数嵌套
function outer() {
  var outerVar = '外部变量';
  
  // 条件2:内部函数引用了外部变量
  function inner() {
    console.log(outerVar);  // 引用了 outerVar → 形成闭包
  }
  
  // 条件3:内部函数被返回或传递到外部
  return inner;  // 返回到外部
}
 
const closureFunc = outer();  // outer 执行完毕
closureFunc();  // 输出: 外部变量 —— 闭包生效!

三个必要条件:

条件 说明 必要吗
函数嵌套 一个函数在另一个函数内部定义 ✅ 必要
引用外部变量 内部函数使用了外部函数的变量 ✅ 必要
返回/传递 内部函数被返回或传递到外部执行 ✅ 必要

2.3 闭包的内存原理 #

为什么函数执行完,变量还能存在?

function createClosure() {
  let privateVar = '私有数据';
  
  return function() {
    console.log(privateVar);
  };
}
 
const myClosure = createClosure();
 
// 正常情况:createClosure 执行完,privateVar 应该被销毁
// 但因为闭包存在,privateVar 被保留在内存中!
 
myClosure();  // ✅ 还能访问 privateVar
 
// 内存原理:
// ┌─────────────────────────────────────┐
// │  Closure 对象(存储在堆内存)         │
// │  ┌───────────────────────────────┐  │
// │  │  函数代码                      │  │
// │  │  词法环境引用                  │  │
// │  └───────────────────────────────┘  │
// │  ┌───────────────────────────────┐  │
// │  │  词法环境                      │  │
// │  │  privateVar: '私有数据'       │  │
// │  └───────────────────────────────┘  │
// └─────────────────────────────────────┐

关键理解:

  • 普通函数执行完,局部变量从栈内存弹出,被销毁
  • 闭包的变量存储在堆内存,不会被自动销毁
  • 只有当闭包函数不再被引用时,垃圾回收才会清理

2.4 闭包的典型应用场景 #

场景一:实现私有变量 #

JavaScript 没有 class 的私有属性(ES2022 之前),用闭包模拟:

// 创建一个"银行账户",余额是私有的
function createAccount(initialBalance) {
  let balance = initialBalance;  // 私有变量,外部无法直接访问
  
  return {
    // 公开方法
    deposit(amount) {
      if (amount > 0) {
        balance += amount;
        console.log(`存入 ${amount},余额 ${balance}`);
      }
    },
    
    withdraw(amount) {
      if (amount > 0 && amount <= balance) {
        balance -= amount;
        console.log(`取出 ${amount},余额 ${balance}`);
        return amount;
      } else {
        console.log('余额不足!');
        return 0;
      }
    },
    
    getBalance() {
      return balance;  // 通过方法访问,不是直接访问
    }
  };
}
 
const account = createAccount(1000);
 
account.deposit(500);   // 存入 500,余额 1500
account.withdraw(200);  // 取出 200,余额 1300
console.log(account.getBalance());  // 1300
 
// ❌ 无法直接访问 balance
console.log(account.balance);  // undefined!
 
// balance 是私有的,只能通过 deposit/withdraw/getBalance 操作

场景二:函数工厂(参数预设) #

// 创建"乘法函数工厂"
function createMultiplier(factor) {
  // factor 被"固化"在闭包中
  return function(number) {
    return number * factor;
  };
}
 
const double = createMultiplier(2);    // 创建"乘2"函数
const triple = createMultiplier(3);    // 创建"乘3"函数
const tenTimes = createMultiplier(10); // 创建"乘10"函数
 
console.log(double(5));     // 10
console.log(triple(5));     // 15
console.log(tenTimes(5));   // 50
 
// 每个函数都"记住"了自己的 factor

场景三:事件处理与回调 #

// 常见错误:循环中的闭包陷阱
for (var i = 1; i <= 3; i++) {
  setTimeout(function() {
    console.log(i);  // 输出: 4, 4, 4(都是同一个 i!)
  }, 100);
}
 
// 解释:
// var i 是函数级作用域,整个循环共用一个 i
// setTimeout 的回调在循环结束后执行
// 此时 i = 4,所以三个回调都打印 4
 
// 正确写法1:使用 let(块级作用域)
for (let i = 1; i <= 3; i++) {
  setTimeout(function() {
    console.log(i);  // 输出: 1, 2, 3 ✅
  }, 100);
}
// let i 每次循环都是新的,闭包捕获的是当前的 i
 
// 正确写法2:使用闭包保存当前值
for (var i = 1; i <= 3; i++) {
  (function(j) {  // 立即执行函数,创建闭包
    setTimeout(function() {
      console.log(j);  // 输出: 1, 2, 3 ✅
    }, 100);
  })(i);  // 把 i 的当前值传进去
}

场景四:模块模式 #

// 创建一个模块,暴露公共方法,隐藏私有实现
const myModule = (function() {
  // 私有变量
  let privateData = [];
  let privateConfig = { debug: false };
  
  // 私有函数
  function privateHelper(item) {
    return item * 2;
  }
  
  // 公开的 API
  return {
    addItem(item) {
      privateData.push(privateHelper(item));
    },
    
    getItems() {
      return privateData;
    },
    
    setDebug(value) {
      privateConfig.debug = value;
    }
  };
})();  // 立即执行,返回模块对象
 
myModule.addItem(5);
myModule.addItem(10);
console.log(myModule.getItems());  // [10, 20]
 
// ❌ 无法访问私有成员
console.log(myModule.privateData);    // undefined
console.log(myModule.privateHelper);  // undefined

场景五:防抖和节流函数 #

// 防抖函数 —— 使用闭包保存 timer
function debounce(func, delay) {
  let timer = null;  // 闭包变量,保存定时器
  
  return function(...args) {
    // 每次调用都清除之前的定时器
    if (timer) {
      clearTimeout(timer);
    }
    
    // 设置新的定时器
    timer = setTimeout(() => {
      func.apply(this, args);
      timer = null;
    }, delay);
  };
}
 
// 使用:搜索输入防抖
const searchInput = document.getElementById('search');
const handleSearch = debounce((value) => {
  console.log('搜索:', value);
  // 发送搜索请求...
}, 300);
 
searchInput.addEventListener('input', (e) => {
  handleSearch(e.target.value);
});
 
// 用户连续输入 "hello"
// h...he...hel...hell...hello
// 只有停下来 300ms 后才执行一次搜索
// timer 变量被闭包保存,每次调用都能访问到
// 节流函数 —— 使用闭包保存上次执行时间
function throttle(func, interval) {
  let lastTime = 0;  // 闭包变量
  
  return function(...args) {
    const now = Date.now();
    
    if (now - lastTime >= interval) {
      func.apply(this, args);
      lastTime = now;
    }
  };
}
 
// 使用:滚动事件节流
const handleScroll = throttle((scrollTop) => {
  console.log('滚动位置:', scrollTop);
}, 200);
 
window.addEventListener('scroll', () => {
  handleScroll(window.scrollY);
});
 
// 每 200ms 最多执行一次,避免频繁触发

场景六:柯里化函数 #

// 柯里化 —— 把多参数函数转成单参数函数链
function curry(func) {
  const expectedArgs = func.length;  // 函数需要的参数数量
  
  return function curried(...args) {
    if (args.length >= expectedArgs) {
      // 参数够了,执行原函数
      return func.apply(this, args);
    } else {
      // 参数不够,返回新函数继续收集参数
      return function(...moreArgs) {
        return curried.apply(this, [...args, ...moreArgs]);
      };
    }
  };
}
 
// 使用
function sum(a, b, c) {
  return a + b + c;
}
 
const curriedSum = curry(sum);
 
console.log(curriedSum(1)(2)(3));     // 6
console.log(curriedSum(1, 2)(3));     // 6
console.log(curriedSum(1)(2, 3));     // 6
console.log(curriedSum(1, 2, 3));     // 6
 
// 每一步都是闭包,保存了已收集的参数

2.5 闭包的注意事项 #

注意一:内存消耗 #

// 闭包会占用更多内存
function createHeavyClosure() {
  const largeData = new Array(10000).fill('data');  // 大数组
  
  return function() {
    return largeData.length;  // 引用了大数组
  };
}
 
const closure = createHeavyClosure();
// largeData 会一直存在于内存中,直到 closure 被销毁
 
// 解决:如果不需要,手动释放
closure = null;  // 让垃圾回收清理

注意二:变量共享问题 #

// 同一个闭包环境中的变量是共享的
function createCounter() {
  let count = 0;
  
  return {
    increment() {
      count++;
      return count;
    },
    decrement() {
      count--;
      return count;
    }
  };
}
 
const counter = createCounter();
console.log(counter.increment());  // 1
console.log(counter.increment());  // 2
console.log(counter.decrement());  // 1 —— 两个方法共享同一个 count

注意三:this 指向问题 #

// 闭包中的 this 可能不是你期望的
const obj = {
  name: 'Object',
  getName() {
    return function() {  // 普通函数,this 丢失
      console.log(this.name);
    };
  }
};
 
const fn = obj.getName();
fn();  // undefined(this 指向 window 或 undefined)
 
// 解决方法1:保存 this
const obj2 = {
  name: 'Object2',
  getName() {
    const self = this;  // 保存 this
    return function() {
      console.log(self.name);  // 使用保存的值
    };
  }
};
 
// 解决方法2:使用箭头函数
const obj3 = {
  name: 'Object3',
  getName() {
    return () => {  // 箭头函数继承 this
      console.log(this.name);
    };
  }
};

三、原型链(Prototype Chain)详解 #

3.1 原型是什么?(大白话解释) #

原型 = 对象的"父对象",继承属性的地方

打个比方:

每个人都有父母,你的某些特征(长相、姓氏)继承自父母。父母也有父母,形成一条"家族链"。你找不到某个特征时,就去父母那里找,父母找不到就去祖父母那里找...

// 最简单的原型示例
const person = {
  name: '默认姓名',
  sayHello() {
    console.log(`你好,我是 ${this.name}`);
  }
};
 
const student = {
  name: '张三',
  grade: '三年级'
};
 
// 让 student "继承" person
Object.setPrototypeOf(student, person);
 
student.sayHello();  // 输出: 你好,我是 张三
// student 没有 sayHello 方法,但能从原型 person 找到!
 
console.log(student.grade);  // 三年级(自己的属性)
console.log(student.name);   // 张三(自己的属性覆盖了原型的)

3.2 原型的核心概念 #

(1)prototype 属性 #

每个函数都有一个 prototype 属性:

// 函数的 prototype
function Person(name) {
  this.name = name;
}
 
console.log(Person.prototype);  // { constructor: Person }
 
// prototype 是一个对象,用于被继承
Person.prototype.sayHello = function() {
  console.log(`你好,我是 ${this.name}`);
};
 
Person.prototype.age = 18;
 
// 创建实例
const p1 = new Person('张三');
const p2 = new Person('李四');
 
p1.sayHello();  // 你好,我是 张三
p2.sayHello();  // 你好,我是 李四
 
console.log(p1.age);  // 18(从原型继承)
console.log(p2.age);  // 18(从原型继承)

(2)proto 属性 #

每个对象都有一个 __proto__ 属性(内部属性,指向原型):

function Person(name) {
  this.name = name;
}
 
const p1 = new Person('张三');
 
// __proto__ 指向构造函数的 prototype
console.log(p1.__proto__ === Person.prototype);  // true ✅
 
// 原型链关系图:
// p1 ──→ p1.__proto__ ──→ Person.prototype
// Person.prototype ──→ Person.prototype.__proto__ ──→ Object.prototype
// Object.prototype ──→ Object.prototype.__proto__ ──→ null

规范写法:

__proto__ 是非标准属性(已废弃),推荐使用:

// 获取原型
Object.getPrototypeOf(obj);
 
// 设置原型
Object.setPrototypeOf(obj, prototype);
 
// 示例
const obj = {};
console.log(Object.getPrototypeOf(obj) === Object.prototype);  // true

(3)constructor 属性 #

原型对象有一个 constructor 属性,指向构造函数:

function Person(name) {
  this.name = name;
}
 
const p1 = new Person('张三');
 
console.log(Person.prototype.constructor === Person);  // true
console.log(p1.__proto__.constructor === Person);      // true
console.log(p1.constructor === Person);                // true
 
// constructor 可以用来判断对象的构造函数
console.log(p1.constructor.name);  // "Person"

3.3 原型链的查找机制 #

function Animal(name) {
  this.name = name;
}
 
Animal.prototype.eat = function() {
  console.log(`${this.name} 在吃东西`);
};
 
function Dog(name, breed) {
  Animal.call(this, name);  // 继承属性
  this.breed = breed;
}
 
// 继承方法
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
 
Dog.prototype.bark = function() {
  console.log(`${this.name} 在叫`);
};
 
const dog = new Dog('旺财', '金毛');
 
// 属性查找顺序
console.log(dog.name);   // '旺财' —— 先找自己的属性 ✅
console.log(dog.breed);  // '金毛' —— 自己的属性 ✅
dog.bark();              // '旺财 在叫' —— 自己的原型方法 ✅
dog.eat();               // '旺财 在吃东西' —— 原型链上找到 ✅
 
// 原型链查找图:
// dog ──→ Dog.prototype ──→ Animal.prototype ──→ Object.prototype ──→ null
//  ↓         ↓                  ↓                   ↓
// name    bark               eat                 toString
// breed   constructor       constructor         hasOwnProperty

查找规则:

  1. 先在对象自身的属性中找
  2. 找不到 → 去 __proto__(原型)找
  3. 还找不到 → 原型的 __proto__(原型的原型)找
  4. 一直往上找,直到 Object.prototype
  5. Object.prototype.__proto__null,查找结束
  6. 找不到返回 undefined

3.4 原型链的完整图解 #

// JavaScript 原型链全图
//
//                    null
//                     ↑
//            Object.prototype
//           (toString, hasOwnProperty...)
//                     ↑
//          Function.prototype
//           (call, bind, apply...)
//                     ↑
//       ┌─────────────┴─────────────┐
//       ↑                           ↑
//   Person.prototype             Array.prototype
//   (sayHello, eat)              (push, pop, map...)
//       ↑                           ↑
//     p1实例                      arr实例
//   (name属性)                   (元素)
//
// 规则:
// 1. 所有对象最终都继承自 Object.prototype
// 2. Object.prototype.__proto__ === null(终点)
// 3. 函数的原型是 Function.prototype
// 4. 数组的原型是 Array.prototype

验证原型链:

function Person(name) {
  this.name = name;
}
const p1 = new Person('张三');
 
// 原型链验证
console.log(p1.__proto__ === Person.prototype);           // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null);         // true
 
// 是不是 Person 的实例
console.log(p1 instanceof Person);  // true
console.log(p1 instanceof Object);  // true
 
// 所有对象都是 Object 的实例
console.log({} instanceof Object);  // true
console.log([] instanceof Object);  // true
console.log(function(){} instanceof Object); // true

3.5 原型链的实际应用 #

应用一:实现继承 #

// ES5 继承模式
function Animal(name) {
  this.name = name;
}
 
Animal.prototype.eat = function() {
  console.log(`${this.name} 在吃东西`);
};
 
function Dog(name, breed) {
  // 第一步:继承父类属性
  Animal.call(this, name);
  
  // 第二步:自己的属性
  this.breed = breed;
}
 
// 第三步:继承父类方法
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;  // 修复 constructor
 
// 第四步:自己的方法
Dog.prototype.bark = function() {
  console.log(`${this.name}: 汪汪汪!`);
};
 
const dog = new Dog('旺财', '金毛');
dog.eat();   // 旺财 在吃东西(继承的)
dog.bark();  // 旺财: 汪汪汪!(自己的)

应用二:扩展内置对象 #

// 扩展数组方法(不推荐直接修改原生 prototype)
// 更好的做法:创建子类
 
class MyArray extends Array {
  // 自定义方法
  first() {
    return this[0];
  }
  
  last() {
    return this[this.length - 1];
  }
  
  sum() {
    return this.reduce((a, b) => a + b, 0);
  }
}
 
const arr = new MyArray(1, 2, 3, 4, 5);
console.log(arr.first());  // 1
console.log(arr.last());   // 5
console.log(arr.sum());    // 15
 
// arr 仍然有 Array 的所有方法
console.log(arr.map(x => x * 2));  // [2, 4, 6, 8, 10]
console.log(arr.filter(x => x > 2));  // [3, 4, 5]

应用三:检测对象类型 #

// 使用 instanceof 检测原型链上的类型
const arr = [1, 2, 3];
 
console.log(arr instanceof Array);  // true
console.log(arr instanceof Object); // true
 
// 更精确的检测
console.log(Array.isArray(arr));    // true(推荐)
 
// 检测是否是某构造函数的实例
function Person(name) {
  this.name = name;
}
 
const p1 = new Person('张三');
console.log(p1 instanceof Person);  // true
 
// 注意:instanceof 检查原型链,可能不准确
const obj = Object.create(Person.prototype);
console.log(obj instanceof Person);  // true(但不是真正的实例)

应用四:属性判断 #

const obj = {
  ownProp: '自己的属性'
};
 
Object.setPrototypeOf(obj, {
  inheritedProp: '继承的属性'
});
 
console.log(obj.ownProp);       // 自己的属性
console.log(obj.inheritedProp); // 继承的属性
 
// 判断属性是否是自己的
console.log(obj.hasOwnProperty('ownProp'));       // true ✅
console.log(obj.hasOwnProperty('inheritedProp')); // false ✅
 
// 推荐:使用 Object.hasOwn()(ES2022)
console.log(Object.hasOwn(obj, 'ownProp'));       // true ✅
console.log(Object.hasOwn(obj, 'inheritedProp')); // false ✅

3.6 ES6 Class 语法 #

ES6 引入了 class,但本质仍是原型继承:

// ES6 class 写法
class Animal {
  constructor(name) {
    this.name = name;
  }
  
  eat() {
    console.log(`${this.name} 在吃东西`);
  }
}
 
class Dog extends Animal {
  constructor(name, breed) {
    super(name);  // 调用父类构造函数
    this.breed = breed;
  }
  
  bark() {
    console.log(`${this.name}: 汪汪汪!`);
  }
}
 
const dog = new Dog('旺财', '金毛');
dog.eat();   // 旺财 在吃东西
dog.bark();  // 旺财: 汪汪汪!
 
// class 本质是函数,prototype 继承仍然有效
console.log(typeof Animal);  // "function"
console.log(Dog.prototype.__proto__ === Animal.prototype);  // true

四、闭包与原型链的关系 #

4.1 闭包在原型方法中的应用 #

// 原型方法中使用闭包
function Timer() {
  this.startTime = Date.now();
}
 
Timer.prototype.start = function() {
  const self = this;  // 闭包保存 this
  
  return function() {
    const elapsed = Date.now() - self.startTime;
    console.log(`已运行 ${elapsed}ms`);
  };
};
 
const timer = new Timer();
const checkTime = timer.start();
 
setTimeout(checkTime, 1000);  // 1秒后:已运行 1000ms
setTimeout(checkTime, 2000);  // 2秒后:已运行 2000ms
 
// startTime 通过闭包保存在 checkTime 函数中

4.2 原型链 vs 闭包存储 #

// 方式1:原型链存储共享数据
function Counter() {}
 
Counter.prototype.count = 0;  // 原型上存储(所有实例共享)
 
Counter.prototype.increment = function() {
  this.count++;  // 会创建自己的 count 属性!
  return this.count;
};
 
const c1 = new Counter();
const c2 = new Counter();
 
console.log(c1.increment());  // 1
console.log(c2.increment());  // 1 —— 不是共享的!
 
// 原型上的属性是共享的,但修改会创建自己的属性
 
// 方式2:闭包存储私有数据
function createCounter() {
  let count = 0;  // 私有
  
  return {
    increment() {
      return ++count;
    }
  };
}
 
const counter1 = createCounter();
const counter2 = createCounter();
 
console.log(counter1.increment());  // 1
console.log(counter2.increment());  // 1 —— 独立的计数器

4.3 选择对比 #

方式 特点 适用场景
原型链 多实例共享、节省内存 共享方法、常量
闭包 数据私有、实例独立 私有状态、计数器

五、常见问题与陷阱 #

Q1: 闭包会导致内存泄漏吗? #

// 旧浏览器可能有问题,现代浏览器已优化
function createClosure() {
  const largeData = new Array(10000).fill('x');
  
  return function() {
    return largeData[0];
  };
}
 
const closure = createClosure();
// largeData 会保留,但这是正常的,不是"泄漏"
 
// "泄漏"是指:你以为数据应该释放,但它没有
// 正确的做法:不需要时释放引用
closure = null;  // largeData 可以被垃圾回收

Q2: 为什么原型上的属性修改会"失效"? #

function Person() {}
Person.prototype.skills = ['JavaScript'];
 
const p1 = new Person();
const p2 = new Person();
 
p1.skills.push('Python');  // 修改原型数组
 
console.log(p2.skills);  // ['JavaScript', 'Python'] —— 共享!
 
// 原因:p1.skills 指向原型上的数组,修改会影响所有实例
 
// 解决:在构造函数中创建自己的属性
function Person2() {
  this.skills = ['JavaScript'];  // 每个实例都有自己的数组
}

Q3: proto 和 prototype 有什么区别? #

属性 属于谁 指向什么 作用
prototype 函数 原型对象 作为实例的原型
__proto__ 对象 它的原型 用于属性查找
function Person() {}
const p = new Person();
 
console.log(Person.prototype);  // Person 的原型对象
console.log(p.__proto__);       // 指向 Person.prototype
console.log(p.__proto__ === Person.prototype);  // true

Q4: 如何判断属性是在原型上还是自己身上? #

const obj = { own: '自己的' };
Object.setPrototypeOf(obj, { inherited: '继承的' });
 
console.log(obj.own);       // 自己的
console.log(obj.inherited); // 继承的
 
// 方法1:hasOwnProperty
console.log(obj.hasOwnProperty('own'));       // true
console.log(obj.hasOwnProperty('inherited')); // false
 
// 方法2:Object.hasOwn(ES2022,推荐)
console.log(Object.hasOwn(obj, 'own'));       // true
console.log(Object.hasOwn(obj, 'inherited')); // false
 
// 方法3:in 操作符(检查原型链)
console.log('own' in obj);       // true
console.log('inherited' in obj); // true —— 原型上的也算!

Q5: new 操作符做了什么? #

function Person(name) {
  this.name = name;
}
 
const p = new Person('张三');
 
// new 做了 4 件事:
// 1. 创建一个空对象
const obj = {};
 
// 2. 设置原型链
Object.setPrototypeOf(obj, Person.prototype);
// obj.__proto__ = Person.prototype;
 
// 3. 执行构造函数,this 指向新对象
Person.call(obj, '张三');
// obj.name = '张三'
 
// 4. 返回新对象(如果构造函数返回对象,则返回那个对象)
return obj;
 
// 手写 new
function myNew(constructor, ...args) {
  const obj = Object.create(constructor.prototype);
  const result = constructor.call(obj, ...args);
  return result instanceof Object ? result : obj;
}
 
const p2 = myNew(Person, '李四');
console.log(p2.name);  // 李四

六、手写实现 #

6.1 手写 Object.create() #

// Object.create(proto) 创建一个以 proto 为原型的对象
function myCreate(proto) {
  if (proto === null) {
    // 创建无原型对象
    return { __proto__: null };
  }
  
  if (typeof proto !== 'object' && typeof proto !== 'function') {
    throw new TypeError('Object prototype may only be an Object or null');
  }
  
  function F() {}  // 临时构造函数
  F.prototype = proto;
  return new F();
}
 
// 测试
const parent = { name: '父对象', sayHi() { console.log('hi'); } };
const child = myCreate(parent);
 
console.log(child.__proto__ === parent);  // true
child.sayHi();  // hi
console.log(child.name);  // 父对象

6.2 手写 instanceof #

// instanceof 检查左边对象的原型链上是否有右边构造函数的 prototype
function myInstanceof(obj, constructor) {
  // obj 必须是对象
  if (typeof obj !== 'object' || obj === null) {
    return false;
  }
  
  // 获取原型链
  let proto = Object.getPrototypeOf(obj);
  
  // 沿原型链查找
  while (proto !== null) {
    if (proto === constructor.prototype) {
      return true;
    }
    proto = Object.getPrototypeOf(proto);
  }
  
  return false;
}
 
// 测试
function Person() {}
const p = new Person();
 
console.log(myInstanceof(p, Person));  // true
console.log(myInstanceof(p, Object));  // true
console.log(myInstanceof(p, Array));   // false

6.3 手写 new #

// 手写 new 操作符
function myNew(constructor, ...args) {
  // 1. 创建空对象,原型指向构造函数的 prototype
  const obj = Object.create(constructor.prototype);
  
  // 2. 执行构造函数,this 绑定到新对象
  const result = constructor.call(obj, ...args);
  
  // 3. 如果构造函数返回对象,则使用返回的对象
  //    否则返回新创建的对象
  return (result !== null && typeof result === 'object') ? result : obj;
}
 
// 测试
function Person(name, age) {
  this.name = name;
  this.age = age;
}
 
Person.prototype.sayHello = function() {
  console.log(`我是 ${this.name}, ${this.age}岁`);
};
 
const p = myNew(Person, '张三', 25);
p.sayHello();  // 我是 张三, 25岁
console.log(p instanceof Person);  // true

6.4 手写继承 #

// 手写 ES5 继承(组合继承)
function inherit(Child, Parent) {
  // 1. 继承原型
  Child.prototype = Object.create(Parent.prototype);
  
  // 2. 修复 constructor
  Child.prototype.constructor = Child;
  
  // 3. 保存父类引用(可选,用于 super)
  Child._super = Parent;
  
  // 4. 添加方法
  Child.prototype.method = function(name, fn) {
    Child.prototype[name] = fn;
  };
}
 
// 使用
function Animal(name) {
  this.name = name;
}
 
Animal.prototype.eat = function() {
  console.log(`${this.name} 在吃东西`);
};
 
function Dog(name, breed) {
  // 继承属性
  Dog._super.call(this, name);
  this.breed = breed;
}
 
// 继承原型
inherit(Dog, Animal);
 
// 添加方法
Dog.prototype.bark = function() {
  console.log(`${this.name}: 汪汪!`);
};
 
const dog = new Dog('旺财', '金毛');
dog.eat();   // 旺财 在吃东西
dog.bark();  // 旺财: 汪汪!

七、总结速记 #

闭包核心要点 #

要点 说明
定义 函数 + 词法环境
形成 函数嵌套 + 引用外部变量 + 返回/传递
内存 存储在堆,不会自动销毁
应用 私有变量、模块、防抖节流、柯里化

原型链核心要点 #

要点 说明
prototype 函数的原型对象(用于被继承)
proto 对象的原型引用(用于查找属性)
constructor 原型指向构造函数的引用
链式查找 自己 → proto → ... → Object.prototype → null

对比速查 #

方式 数据存储 共享性 私有性 内存效率
闭包 堆内存 不共享 ✅ 私有 较低
原型链 原型对象 ✅ 共享 ❌ 公开 较高

最佳实践 #

场景 推荐方式
私有数据、状态隔离 闭包
共享方法、常量 原型
需要继承 ES6 class + extends
模块化 闭包 + IIFE

附录:调试技巧 #

// 查看对象的原型链
function getPrototypeChain(obj) {
  const chain = [];
  let proto = Object.getPrototypeOf(obj);
  
  while (proto !== null) {
    chain.push(proto.constructor?.name || 'Object');
    proto = Object.getPrototypeOf(proto);
  }
  
  return chain;
}
 
console.log(getPrototypeChain([]));
// ['Array', 'Object']
 
console.log(getPrototypeChain(function(){}));
// ['Function', 'Object']
 
// 查看对象的属性来源
function inspectObject(obj) {
  for (let key in obj) {
    const isOwn = Object.hasOwn(obj, key);
    const from = isOwn ? '自身' : '原型';
    console.log(`${key}: ${from}`);
  }
}

最后更新:2026-04-01