在JavaScript中,每个对象都与一个被称为原型的内部引用相关联,通常用[[Prototype]]表示。对象自身未定义的属性会沿着这条原型链逐级向上查找,因此修改对象的原型会直接影响它能够访问的继承成员。根据修改时机、影响范围以及目标对象类型的不同,JavaScript提供了多种修改原型的方案,下面从实际应用角度对它们进行梳理。

通过构造函数prototype属性批量调整原型
构造函数是创建同一类对象的模板。每个函数在被定义时都会自动获得一个prototype属性,这个属性本身是一个对象,当使用new关键字调用构造函数时,新创建的实例对象内部的原型引用会指向构造函数的prototype对象。因此,只要在构造函数的prototype上添加或修改成员,所有已经创建以及后续创建的实例都可以通过原型链访问到这些成员。
这种方式最大的优势是集中式的批量调整。例如,一个应用中已经存在大量由同一个构造函数创建的实例,如果希望为这些实例统一追加一个新的方法,不需要逐个修改实例,只需修改构造函数的prototype即可。需要注意的是,实例自身如果定义了与原型同名的属性,属性查找会优先返回实例自身的值,原型上的同名成员不会覆盖实例自身成员。
// 定义构造函数
function Animal(name) {
this.name = name;
}
// 创建两个实例
const dog = new Animal('旺财');
const cat = new Animal('咪咪');
// 在构造函数的prototype上添加方法
Animal.prototype.speak = function() {
console.log('我是' + this.name);
};
// 已经存在的实例和后续创建的实例都能访问该方法
dog.speak(); // 输出:我是旺财
cat.speak(); // 输出:我是咪咪
上面的示例演示了通过修改Animal.prototype为所有实例补充speak方法的过程。修改前创建的实例没有发生变化,但修改后再次调用方法时,由于原型链是动态查找的,实例仍然可以找到新增的方法。这正是原型继承中非常有用的特性。
使用Object.setPrototypeOf修改单个对象的原型
如果需求只是针对某一个已经存在的对象,而不是某一类对象,可以使用Object.setPrototypeOf。该方法接收两个参数,第一个参数是目标对象,第二个参数是新的原型对象。调用后,目标对象原来的原型链接会被新的原型对象替换。
这种方式的控制粒度更细,适合动态调整单个对象的继承关系。例如,某个对象在不同的运行阶段可能需要从不同的原型上获取能力,就可以通过这个方法来切换。但需要特别留意,现代JavaScript引擎对对象的原型结构有大量优化假设,频繁调用Object.setPrototypeOf会破坏这些优化,导致属性访问速度下降。因此在性能敏感的代码路径中应谨慎使用。
// 定义两个原型对象
const protoA = {
greetA: function() {
console.log('来自protoA的问候');
}
};
const protoB = {
greetB: function() {
console.log('来自protoB的问候');
}
};
// 创建初始原型为protoA的对象
const item = Object.create(protoA);
item.greetA(); // 输出:来自protoA的问候
// 把item的原型修改为protoB
Object.setPrototypeOf(item, protoB);
item.greetB(); // 输出:来自protoB的问候
// item.greetA(); // 若取消注释会抛出TypeError,因为原型已经改变
在上面代码中,item对象最初通过Object.create(protoA)创建,因此能够使用greetA方法。执行Object.setPrototypeOf(item, protoB)后,item的原型被替换为protoB,此时item不再拥有protoA的继承关系,调用greetA会失败,而greetB则可以正常访问。
通过Object.create在创建时指定原型
如果并不需要修改一个已有对象的原型,而是在创建新对象时就直接指定它的原型,那么Object.create是更合适的选择。它会把传入的第一个参数作为新对象的原型,并返回这个新对象。由于新对象从一开始就基于指定原型创建,不需要进行后续的原型变更操作。
与Object.setPrototypeOf相比,Object.create对引擎更加友好,因为它可以在对象创建阶段就确定原型结构,不需要打乱已有的优化。实际开发中,当需要基于当需要基于某个原型创建对象时,使用 `Object.create` 可以让代码的意图更加清晰,也更容易被引擎优化。比如一个简单的场景:创建一个记录不同等级日志的对象,每个等级对象都继承自同一个基础日志对象。
// 基础日志对象
const baseLogger = {
write: function(level, message) {
console.log(`[${level}] ${message}`);
}
};
// 基于 baseLogger 创建 infoLogger
const infoLogger = Object.create(baseLogger);
infoLogger.info = function(message) {
this.write('INFO', message);
};
// 基于 baseLogger 创建 errorLogger
const errorLogger = Object.create(baseLogger);
errorLogger.error = function(message) {
this.write('ERROR', message);
};
infoLogger.info('应用启动完成'); // 输出:[INFO] 应用启动完成
errorLogger.error('连接数据库失败'); // 输出:[ERROR] 连接数据库失败
这里 `infoLogger` 和 `errorLogger` 都通过 `Object.create(baseLogger)` 获得了 `write` 方法,而它们自身只定义了自己关心的 `info` 或 `error` 方法。这样既避免了重复编写 `write` 逻辑,又让对象之间的继承关系在创建时就确定下来。`this` 的指向会沿着调用链正确传递,因为在 `infoLogger.info` 内部,`this` 指向的是 `infoLogger` 本身,而 `infoLogger` 通过原型链可以找到 `write` 方法。
关于 `Object.create` 还有一点值得注意:它可以传入 `null` 来创建一个没有原型的对象。这在需要将对象当作纯字典使用时特别有用,因为普通对象会从 `Object.prototype` 继承 `toString`、`constructor` 等属性,可能造成键名冲突或误判。
const pureDict = Object.create(null);
pureDict.toString = '这是一个普通值';
console.log(pureDict.toString); // 输出:这是一个普通值
// 普通对象的情况
const normalDict = {};
normalDict.toString = '这也是一个普通值';
console.log(normalDict.toString); // 输出:这也是一个普通值
// 但如果遍历 normalDict,会发现一些额外的情况
for (const key in normalDict) {
console.log(key); // 只会输出'toString',因为继承的属性默认不可枚举
}
// 不过检测属性存在时,普通对象会误判
console.log('constructor' in normalDict); // 输出:true
console.log('constructor' in pureDict); // 输出:false
在这个例子中,`pureDict` 没有原型,因此 `constructor` 这样的继承属性完全不存在。而 `normalDict` 虽然自身没有 `constructor` 属性,但因为它继承自 `Object.prototype`,`in` 操作符会沿着原型链找到它。当你需要判断一个属性是否直接属于某个对象、而不是来自原型链时,应该使用 `hasOwnProperty` 方法,或者更现代的 `Object.hasOwn` 静态方法。
const animal = {
species: '哺乳动物',
describe: function() {
console.log(`物种:${this.species}`);
}
};
const dog = Object.create(animal);
dog.name = '旺财';
console.log(dog.hasOwnProperty('name')); // 输出:true
console.log(dog.hasOwnProperty('species')); // 输出:false
console.log(dog.hasOwnProperty('describe')); // 输出:false
// 使用 Object.hasOwn 可避免 hasOwnProperty 被覆盖的问题
console.log(Object.hasOwn(dog, 'name')); // 输出:true
console.log(Object.hasOwn(dog, 'species')); // 输出:false
这里 `dog` 对象自身只有 `name` 属性,`species` 和 `describe` 都来自原型 `animal`。因此 `hasOwnProperty('name')` 返回 `true`,而另外两个返回 `false`。使用 `Object.hasOwn` 是更推荐的做法,因为有些对象可能没有继承自 `Object.prototype`,或者 `hasOwnProperty` 方法被自身的同名属性覆盖。
说到遍历属性,`for...in` 循环的行为也受原型链影响。`for...in` 会遍历对象自身以及原型链上所有可枚举的属性,这有时会带来意料之外的结果。如果需要只处理对象自身的属性,通常要在循环内加上 `hasOwnProperty` 判断。
const parentObj = { inherited: '来自原型的属性' };
const childObj = Object.create(parentObj);
childObj.own = '自身属性';
for (const key in childObj) {
if (childObj.hasOwnProperty(key)) {
console.log(`自身属性:${key}`);
} else {
console.log(`继承属性:${key}`);
}
}
// 输出:
// 自身属性:own
// 继承属性:inherited
上面的循环演示了如何区分自身属性与继承属性。在实际项目中,如果你只关心对象自己的数据,使用 `Object.keys` 或 `Object.entries` 会更安全,因为它们只会返回对象自身的可枚举属性,不会沿原型链查找。
console.log(Object.keys(childObj)); // 输出:['own'] console.log(Object.entries(childObj)); // 输出:[['own', '自身属性']]除了查看原型链上的属性,有时也需要在运行时获取对象的原型。`Object.getPrototypeOf` 可以返回指定对象的原型,它与 `Object.setPrototypeOf` 形成对应关系。
const proto = { version: '1.0' };
const instance = Object.create(proto);
console.log(Object.getPrototypeOf(instance) === proto); // 输出:true
console.log(Object.getPrototypeOf(proto) === Object.prototype); // 输出:true
console.log(Object.getPrototypeOf(Object.prototype)); // 输出:null
这段代码展示了原型链的终点:`Object.prototype` 的原型是 `null`。当 JavaScript 引擎在原型链上找不到某个属性、最终到达 `null` 时,就会返回 `undefined`。整个原型链的查找过程可以概括为:从对象自身开始,沿着 `[[Prototype]]` 链接逐级向上查找,直到找到属性或到达 `null`。
理解这条链的完整结构对于掌握 JavaScript 的对象模型非常重要。在构造函数与类的场景中,原型链会变得更加复杂,因为涉及 `prototype` 属性与实例的 `[[Prototype]]` 之间的关系。构造函数通过 `prototype` 属性为实例提供共享的方法和属性,而实例通过内部的 `[[Prototype]]` 引用指向构造函数的 `prototype` 对象。
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log(`你好,我是${this.name}`);
};
const alice = new Person('Alice');
const bob = new Person('Bob');
alice.sayHello(); // 输出:你好,我是Alice
bob.sayHello(); // 输出:你好,我是Bob
console.log(Object.getPrototypeOf(alice) === Person.prototype); // 输出:true
console.log(Object.getPrototypeOf(Person.prototype) === Object.prototype); // 输出:true
console.log(Object.getPrototypeOf(Object.prototype)); // 输出:null
这里 `alice` 和 `bob` 的原型都指向 `Person.prototype`,而 `Person.prototype` 本身也是一个对象,它的原型指向 `Object.prototype`。于是形成了 `alice -> Person.prototype -> Object.prototype -> null` 这样一条完整的原型链。`alice.sayHello()` 能够执行,正是因为引擎在 `alice` 自身找不到 `sayHello` 时,沿着原型链在 `Person.prototype` 上找到了它。
构造函数与 `Object.create` 并不冲突,它们可以结合起来使用。例如,你可以在构造函数内部返回一个通过 `Object.create` 创建的对象,从而更精细地控制原型链的形态。
function createPerson(name) {
const person = Object.create(createPerson.prototype);
person.name = name;
return person;
}
createPerson.prototype.sayHello = function() {
console.log(`你好,我是${this.name}`);
};
const carol = createPerson('Carol');
carol.sayHello(); // 输出:你好,我是Carol
console.log(Object.getPrototypeOf(carol) === createPerson.prototype); // 输出:true
这种方式与使用 `new` 关键字的构造函数在效果上基本一致,但显式地展示了内部机制。在现代 JavaScript 中,类语法提供了更简洁的写法,但它底层仍然是基于原型实现的。
class Animal {
constructor(type) {
this.type = type;
}
sound() {
return `${this.type}发出声音`;
}
}
class Dog extends Animal {
constructor(name) {
super('犬类');
this.name = name;
}
bark() {
return `${this.name}汪汪叫`;
}
}
const myDog = new Dog('旺财');
console.log(myDog.sound()); // 输出:犬类发出声音
console.log(myDog.bark()); // 输出:旺财汪汪叫
// 验证原型链关系
console.log(Object.getPrototypeOf(myDog) === Dog.prototype); // 输出:true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // 输出:true
console.log(Object.getPrototypeOf(Animal.prototype) === Object.prototype); // 输出:true
从输出来看,类语法并没有改变原型链的本质。`myDog` 的原型是 `Dog.prototype`,`Dog.prototype` 的原型是 `Animal.prototype`,最终连接到 `Object.prototype`,再以 `null` 收尾。类的继承实际上是通过设置 `Dog.prototype` 的 `[[Prototype]]` 为 `Animal.prototype` 来实现的。理解这一点后,再回头看不使用类的原型写法,会发现它们只是同一套机制的不同表达方式。
在实际开发中,操作原型链需要保持谨慎。频繁使用 `Object.setPrototypeOf` 动态修改已有对象的原型,会破坏引擎的优化假设,导致性能下降。因此更应该优先在对象创建阶段就确定好原型结构,选择 `Object.create` 或者构造函数、类来组织继承关系。同时,访问原型链上的属性时,要注意继承属性和自身属性的区别,避免在遍历或判断时产生意外的行为。当需要判断某个属性是否存在,且不区分自身还是继承时,可以用 `in` 操作符;如果必须确认属性直接属于对象自身,则使用 `Object.hasOwn` 更可靠。
掌握了这些原型链的基本操作和注意事项之后,就能更从容地理解 JavaScript 中一些常见的模式,比如混入、工厂函数、委托设计模式等。它们大多都建立在对 `[[Prototype]]` 链接的深刻理解之上。原型机制是 JavaScript 对象系统的核心,也是深入语言内部的一个关键入口。
JavaScript对象原型prototypeObject_setPrototypeOf修改时间:2026-07-14 00:15:28