JS 原型与原型链详解
JavaScript 原型与原型链详解
文章目录
- JavaScript 原型与原型链详解
- 一、基础概念类
- 1.1 什么是原型?JavaScript 中如何访问一个对象的原型?
- 1.2 构造函数、实例对象和原型对象之间的关系是什么?
- 1.3 prototype 和 **proto** 的区别是什么?
- 二、原型链机制类
- 2.1 什么是原型链?描述原型链的查找机制
- 2.2 代码示例分析
- 三、构造函数与实例类
- 3.1 new 操作符执行时发生了什么?
- 3.2 手动实现 new 操作符
- 3.2.1apply逻辑
- 3.2.2`result instanceof Object ? result : obj;`解决的问题
- 四、继承与原型链类
- 4.1 JS如何实现继承?
- 原型链继承
- 五、高级应用类
- 5.1 如何修改内置对象(如 Array)的原型?这样做有什么风险?
- 5.1.1修改内置原型
- 5.1.2创建子类继承内置对象(ES6 Class方式)
- 5.1.3使用Object.create创建原型链继承
- 5.2 如何判断一个属性是对象自身的还是继承自原型链的?
- 六、总结
一、基础概念类
1.1 什么是原型?JavaScript 中如何访问一个对象的原型?
**原型(Prototype)**是 JavaScript 实现继承的基础机制。每个 JavaScript 对象都有一个内部属性 [[Prototype]]
(可通过 __proto__
或 Object.getPrototypeOf()
访问),它指向该对象的原型对象。原型对象本身也是一个普通对象,同样拥有自己的原型,这样就形成了原型链。
访问对象原型的方式:
- 对于构造函数:通过
Constructor.prototype
访问 - 对于实例对象:
obj.__proto__
(非标准但广泛支持)Object.getPrototypeOf(obj)
(标准方法)
function Person() {}
const p = new Person();// 访问构造函数的原型
console.log(Person.prototype); // 访问实例的原型
console.log(p.__proto__);
console.log(Object.getPrototypeOf(p));
1.2 构造函数、实例对象和原型对象之间的关系是什么?
三者关系可概括为:
- 构造函数:用于创建对象的函数,拥有
prototype
属性 - 原型对象:通过
Constructor.prototype
访问,包含共享属性和方法 - 实例对象:通过
new Constructor()
创建,其__proto__
指向构造函数的原型
关系图示:
构造函数 (Person)├── prototype (原型对象)│ ├── constructor (指回构造函数)│ └── 共享属性和方法└── 实例化└── 实例对象 (p)└── __proto__ (指向原型对象)
关键点:
- 构造函数的
prototype
属性指向原型对象 - 原型对象的
constructor
属性指回构造函数 - 实例对象的
__proto__
指向构造函数的原型对象
1.3 prototype 和 proto 的区别是什么?
特性 | prototype | __proto__ |
---|---|---|
所属对象 | 函数 | 对象(包括函数) |
作用 | 为构造函数定义共享属性和方法 | 指向对象的原型,形成原型链 |
是否标准 | 是 | 否(非标准但广泛支持) |
访问方式 | Constructor.prototype | obj.__proto__ 或 Object.getPrototypeOf(obj) |
用途场景 | 实现继承和共享方法 | 属性查找机制 |
关键区别:
prototype
是函数特有的属性,用于实现基于原型的继承__proto__
是对象实例的属性,指向其构造函数的原型对象
二、原型链机制类
2.1 什么是原型链?描述原型链的查找机制
原型链是由对象的 __proto__
链接形成的链条结构,它允许对象访问其原型上的属性和方法,直到 Object.prototype.__proto__
(值为 null
)为止。
原型链查找机制:
- 访问对象属性时,首先在对象自身查找
- 如果自身不存在该属性,则通过
__proto__
查找其原型对象 - 继续沿原型链向上查找,直到找到属性或到达原型链顶端(
null
) - 如果最终未找到,则返回
undefined
这种机制实现了 JavaScript 的继承和属性共享。
2.2 代码示例分析
function Person() {}
Person.prototype.name = "Alice";let p = new Person();
console.log(p.name); // "Alice"
console.log(p.hasOwnProperty("name")); // false
执行过程解析:
p.name
访问时:- 首先在
p
对象自身查找name
属性 → 未找到 - 通过
p.__proto__
查找Person.prototype
→ 找到name: "Alice"
- 返回
"Alice"
- 首先在
p.hasOwnProperty("name")
:hasOwnProperty
是检查属性是否为对象自身的属性name
实际存在于Person.prototype
上,而非p
对象自身- 返回
false
三、构造函数与实例类
3.1 new 操作符执行时发生了什么?
new
操作符创建实例时,背后执行了以下步骤:
- 创建一个新的空对象
{}
- 将新对象的
__proto__
指向构造函数的prototype
属性 - 将构造函数的
this
绑定到新对象,并执行构造函数 - 如果构造函数返回一个对象,则返回该对象;否则返回新创建的对象
完整过程示例:
function Person(name) {this.name = name;
}const p = new Person("Alice");
等价于:
function myNew(Constructor, ...args) {// 1. 创建空对象let obj = {};// 2. 设置原型链obj.__proto__ = Constructor.prototype;// 3. 绑定this并执行构造函数let result = Constructor.apply(obj, args);// 4. 返回结果(优先返回对象,否则返回新对象)return result instanceof Object ? result : obj;
}const p = myNew(Person, "Alice");
3.2 手动实现 new 操作符
function myNew(Constructor, ...args) {// 1. 创建一个新对象,并将其原型指向构造函数的prototypelet obj = Object.create(Constructor.prototype);// 2. 调用构造函数,将this绑定到新对象,args是一个数组let result = Constructor.apply(obj, args);// 3. 如果构造函数返回了一个对象,则返回该对象;否则返回新对象return result instanceof Object ? result : obj;
}
3.2.1apply逻辑
apply(thisArg,argsArray)
接收 两个参数:
thisArg
(必需):函数运行时绑定的this
值。argsArray
(可选):一个数组或类数组对象,包含传递给函数的参数列表。如果省略或为null
/undefined
,则相当于传递空数组。
argsArray
的核心作用是解耦 myNew
和构造函数 Constructor
之间的参数关系:
myNew
不需要关心具体有多少参数,只需将所有额外参数打包到args
数组中。Constructor
可以自由定义自己需要的参数,通过this
接收并处理。
3.2.2result instanceof Object ? result : obj;
解决的问题
作用:处理构造函数 Constructor
的返回值,确保最终返回的对象符合预期
在 JavaScript 中,构造函数(通过 new
调用的函数)可以显式返回一个值。这个返回值可以是:
- 一个对象(包括数组、函数、普通对象等)。
- 一个原始值(如
number
、string
、boolean
、null
、undefined
)。
如果构造函数返回一个对象,new
操作符会忽略默认创建的实例对象,直接返回这个指定的对象;如果返回原始值,则忽略返回值,仍然返回默认创建的实例对象。
示例:构造函数返回对象 vs 原始值
// 情况1:构造函数返回对象
function Person1() {return { name: "Bob" }; // 返回一个新对象
}
const p1 = new Person1();
console.log(p1); // { name: "Bob" }(不是 Person1 的实例)// 情况2:构造函数返回原始值
function Person2() {return 123; // 返回原始值
}
const p2 = new Person2();
console.log(p2); // Person2 的实例(不是 123)
四、继承与原型链类
4.1 JS如何实现继承?
原型链继承
核心:将子类的原型指向父类的实例
JavaScript 中通过原型链继承实现继承的基本模式:
function Parent() {this.parentProperty = "Parent Value";
}Parent.prototype.parentMethod = function() {console.log("Parent Method");
};function Child() {this.childProperty = "Child Value";
}// 关键步骤:将Child的原型指向Parent的实例
Child.prototype = new Parent();const c = new Child();
console.log(c.parentProperty); // "Parent Value"
c.parentMethod(); // "Parent Method"
继承关系图示:
Child实例 (c)├── __proto__ → Child.prototype (Parent的实例)├── __proto__ → Parent.prototype├── constructor → Parent└── parentMethod└── childProperty (来自Child构造函数)
注意:这种继承方式存在一些问题(如引用类型共享、无法向父类构造函数传参等),现代开发更推荐使用 ES6 的 class
和 extends
语法。
五、高级应用类
5.1 如何修改内置对象(如 Array)的原型?这样做有什么风险?
5.1.1修改内置原型
修改方式示例:
// 添加自定义方法到Array原型
Array.prototype.customMethod = function() {console.log("This is a custom method");
};const arr = [1, 2, 3];
arr.customMethod(); // "This is a custom method"
潜在风险:
- 全局污染:修改内置原型会影响所有使用该内置对象的代码
- 命名冲突:可能与未来 JavaScript 版本新增的方法名冲突
- 兼容性问题:可能导致与其他库或框架的不可预期交互
- 维护困难:使代码行为变得不可预测,增加调试难度
最佳实践:
- 避免直接修改内置原型
- 如需扩展功能,考虑使用工具函数或创建子类
- 如果必须修改,添加前缀以减少冲突风险(如
myCustomMethod
)
5.1.2创建子类继承内置对象(ES6 Class方式)
这是最推荐的方式,既扩展了功能,又不会影响原生对象。
1. 基础继承示例
// 创建继承自Array的自定义类
class CustomArray extends Array {// 添加自定义方法customSum() {return this.reduce((acc, val) => acc + val, 0);}// 可以添加更多自定义方法customMax() {return Math.max(...this);}
}// 使用示例
const arr = new CustomArray(1, 2, 3, 4);
console.log(arr.customSum()); // 输出: 10
console.log(arr.customMax()); // 输出: 4
console.log(arr instanceof Array); // true (仍然属于Array类型)
5.1.3使用Object.create创建原型链继承
更底层的实现方式,不使用ES6 class语法。
// 创建基于Array的新对象
const EnhancedArray = function(...items) {const arr = [...items];return Object.setPrototypeOf(arr, EnhancedArray.prototype);
};// 设置原型链
EnhancedArray.prototype = Object.create(Array.prototype);
EnhancedArray.prototype.constructor = EnhancedArray;// 添加自定义方法
EnhancedArray.prototype.customSum = function() {return this.reduce((acc, val) => acc + val, 0);
};// 使用示例
const arr = EnhancedArray(1, 2, 3);
console.log(arr.customSum()); // 输出: 6
console.log(arr instanceof Array); // true
5.2 如何判断一个属性是对象自身的还是继承自原型链的?
判断方法:
-
hasOwnProperty()
方法:- 只检查对象自身的属性,不检查原型链
const obj = { a: 1 }; console.log(obj.hasOwnProperty('a')); // true console.log(obj.hasOwnProperty('toString')); // false
-
Object.getOwnPropertyNames()
:- 返回对象自身的所有属性名(不包括原型链)
const obj = { a: 1, b: 2 }; console.log(Object.getOwnPropertyNames(obj)); // ["a", "b"]
-
in
操作符:- 检查属性是否存在于对象或其原型链中
const obj = { a: 1 }; console.log('a' in obj); // true console.log('toString' in obj); // true
综合示例:
function checkProperty(obj, prop) {if (obj.hasOwnProperty(prop)) {console.log(`${prop} 是对象自身的属性`);} else if (prop in obj) {console.log(`${prop} 是继承自原型链的属性`);} else {console.log(`${prop} 不是对象的属性`);}
}const obj = { a: 1 };
checkProperty(obj, 'a'); // "a 是对象自身的属性"
checkProperty(obj, 'toString'); // "toString 是继承自原型链的属性"
checkProperty(obj, 'b'); // "b 不是对象的属性"
六、总结
JavaScript 的原型和原型链机制是其面向对象编程的核心,理解这些概念对于掌握 JavaScript 至关重要:
- 原型是对象间共享属性和方法的机制
- 原型链实现了属性的查找和继承
- 构造函数、实例和原型三者构成了 JavaScript 对象系统的基础
- new 操作符通过特定步骤创建实例并建立原型链
- 原型链继承是 JavaScript 实现继承的主要方式
- 谨慎操作内置原型,避免潜在风险
- 准确判断属性来源对调试和维护非常重要