文章
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查找规则:
- 先在对象自身的属性中找
- 找不到 → 去
__proto__(原型)找 - 还找不到 → 原型的
__proto__(原型的原型)找 - 一直往上找,直到
Object.prototype Object.prototype.__proto__是null,查找结束- 找不到返回
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); // true3.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); // trueQ4: 如何判断属性是在原型上还是自己身上? #
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)); // false6.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); // true6.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
继续阅读
返回文章列表