原型与原型链
JavaScript 的对象通过原型(prototype)互相关联,形成一条查找链。理解原型与原型链,是掌握 JavaScript 继承机制与 class 语法糖的基础。本文从概念出发,逐步拆解原型链的查找过程与继承实现。
一、原型概念
每个对象都有一个隐藏属性 [[Prototype]](浏览器中常以 __proto__ 暴露),指向另一个对象;每个函数都拥有 prototype 属性,指向一个专门存放共享属性和方法的对象。
// 对象有 __proto__,指向它的原型
const obj = {};
console.log(obj.__proto__ === Object.prototype); // true
// 函数有 prototype,指向构造出的实例的原型
function Person() {}
console.log(typeof Person.prototype); // object
console.log(Person.prototype.constructor === Person); // true// 普通对象没有 prototype,只有 __proto__
const obj = {};
console.log(obj.prototype); // undefined
console.log(obj.__proto__); // Object.prototype
// 只有函数才同时拥有两者
function fn() {}
console.log(fn.prototype); // { constructor: fn }
console.log(fn.__proto__ === Function.prototype); // true二、构造函数与 new 过程
使用 new 调用构造函数时,引擎内部依次执行四步:
| 步骤 | 操作 |
|---|---|
| 1 | 创建一个新对象 |
| 2 | 将新对象的 __proto__ 绑定到构造函数的 prototype |
| 3 | 以新对象为 this 执行构造函数体 |
| 4 | 若函数显式返回对象则返回该对象,否则返回新对象 |
function Person(name, age) {
// 第 3 步:this 指向新对象,可为其添加属性
this.name = name;
this.age = age;
// 第 4 步:没有 return 对象,隐式返回新对象
}
const p = new Person("张三", 25);
console.log(p.__proto__ === Person.prototype); // true
console.log(p instanceof Person); // true如果构造函数返回了一个对象,则会覆盖默认返回值:
function Bad() {
this.x = 1;
return { y: 2 }; // 显式返回对象,覆盖 this
}
console.log(new Bad()); // { y: 2 }
function Fine() {
this.x = 1;
return 42; // 返回原始值,被忽略
}
console.log(new Fine()); // { x: 1 }三、原型链查找机制
访问 obj.prop 时,JavaScript 会沿"对象 → 对象的原型 → 原型的原型 → ... → Object.prototype → null"逐级查找,找到第一个同名属性即停止;找不到则返回 undefined。
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function () {
console.log(`你好,我是${this.name}`);
};
const p = new Person("李四");
console.log(p.name); // 李四,来自自身属性
p.sayHello(); // 来自 Person.prototype
console.log(p.toString); // 来自 Object.prototype
console.log(p.nonexist); // undefined,全链找不到
// 完整的查找链
console.log(p.__proto__); // Person.prototype
console.log(p.__proto__.__proto__); // Object.prototype
console.log(p.__proto__.__proto__.__proto__); // null,链的终点// 赋值不会沿原型链覆盖:自有属性没有就在自己身上创建
function Person() {}
Person.prototype.name = "原型上的名字";
const p = new Person();
console.log(p.name); // 原型上的名字
p.name = "自己的名字"; // 创建自有属性,屏蔽原型上的同名属性
console.log(Person.prototype.name); // 原型上的名字,不受影响
console.log(p.hasOwnProperty("name")); // true四、proto 与 prototype 的关系
两者通过 new 建立联系:实例的 __proto__ 指向构造函数的 prototype。
function Person() {}
const p = new Person();
// 核心等式
console.log(p.__proto__ === Person.prototype); // true
console.log(p.__proto__ === Object.getPrototypeOf(p)); // true
// 更完整的关系网
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Person.__proto__ === Function.prototype); // true
console.log(Function.prototype.__proto__ === Object.prototype); // true| 表达式 | 结果 | 说明 |
|---|---|---|
p.__proto__ === Person.prototype | true | 实例与构造函数原型的关联 |
Person.prototype.constructor === Person | true | 原型指向构造函数 |
Person.prototype.__proto__ === Object.prototype | true | 构造函数原型也继承自 Object.prototype |
Person.__proto__ === Function.prototype | true | 构造函数本身是 Function 的实例 |
五、Object.getPrototypeOf / setPrototypeOf
规范推荐用这两个方法读写原型,而不是直接操作 __proto__:
const base = { greet() { return "hi"; } };
const child = {};
Object.setPrototypeOf(child, base);
console.log(Object.getPrototypeOf(child) === base); // true
console.log(child.greet()); // hi
// 设置原型对已创建对象生效
const bird = { fly() { console.log("飞"); } };
const duck = Object.create(bird); // 创建时就指定原型
duck.fly(); // 飞注意 setPrototypeOf 会破坏引擎对原型链的优化,性能较差;需要指定原型的对象应尽量用 Object.create。
六、instanceof 原理与局限
obj instanceof Fn 的检测方式是:沿 obj 的原型链查找是否存在 Fn.prototype。
function Person() {}
const p = new Person();
console.log(p instanceof Person); // true,原型链上有 Person.prototype
console.log(p instanceof Object); // true,Object.prototype 也在链上
console.log([] instanceof Array); // true
console.log([] instanceof Object); // true
console.log([] instanceof Function); // false
// 改动原型会改变 instanceof 结果
Object.setPrototypeOf(p, {});
console.log(p instanceof Person); // false局限:instanceof 依赖 prototype 属性,跨 iframe、跨 realm 的 Array.isArray 场景会误判,且无法判断原始值:
console.log(1 instanceof Number); // false,原始值没有原型链
console.log(Array.isArray([])); // true,推荐用 Array.isArray 判断数组七、constructor 属性
每个函数的 prototype 上默认带有一个 constructor 属性,指回函数自身,可用来追溯对象的构造者:
function Person() {}
const p = new Person();
console.log(p.constructor === Person); // true,沿原型链找到
console.log(Person.prototype.constructor); // Person
// 若整体替换 prototype,需要手动补回 constructor
function Dog() {}
Dog.prototype = { bark() { console.log("汪汪"); } };
console.log(Dog.prototype.constructor === Dog); // false
// 修复方式
Dog.prototype = {
bark() { console.log("汪汪"); },
constructor: Dog,
};八、原型方法共享
放在 prototype 上的方法被所有实例共享,内存中只有一份;而 this 上的属性属于每个实例自己:
function Person(name) {
this.name = name; // 实例自有属性,各自一份
}
Person.prototype.sayName = function () { // 原型方法,共享一份
console.log(this.name);
};
const a = new Person("张三");
const b = new Person("李四");
console.log(a.sayName === b.sayName); // true,同一个函数
console.log(a.hasOwnProperty("sayName")); // false,方法不在实例上
console.log(a.hasOwnProperty("name")); // true向原型上追加方法对已有实例立即生效:
Person.prototype.greet = function () {
console.log("你好");
};
a.greet(); // 你好,实例无需任何改动九、修改原型的影响
替换 prototype 与修改 prototype 上的属性,效果完全不同:
function Person() {}
const p1 = new Person();
// 在已有原型对象上追加方法:所有实例(含旧的)都生效
Person.prototype.sayHi = function () { console.log("hi"); };
p1.sayHi(); // hi
// 整体替换 prototype:旧实例仍指向旧原型,新实例指向新原型
Person.prototype = { sayBye() { console.log("bye"); } };
const p2 = new Person();
p2.sayBye(); // bye
p1.sayBye(); // TypeError,p1 指向被替换的旧原型十、原型链实现继承
10.1 原型链继承(原始方式)
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function () {
console.log(`${this.name}发出声音`);
};
function Dog(name, breed) {
this.name = name; // 重复初始化父类属性
this.breed = breed;
}
Dog.prototype = new Animal(); // 将子类原型指向父类实例
Dog.prototype.constructor = Dog;
const dog = new Dog("旺财", "柴犬");
dog.speak(); // 旺财发出声音问题:父类实例属性成为子类原型上的共享属性,多个实例会互相污染;且无法向父类构造函数传参。
10.2 借用构造函数继承
function Animal(name) {
this.name = name;
this.features = ["跑"];
}
function Dog(name) {
Animal.call(this, name); // 借调用父类构造函数
}
const d1 = new Dog("旺财");
const d2 = new Dog("来福");
d1.features.push("跳");
console.log(d1.features); // ['跑','跳'],各自独立
console.log(d2.features); // ['跑']解决了属性共享问题,但方法只能放在 this 上,无法复用原型方法。
10.3 寄生组合式继承(推荐)
组合两者的优点:属性通过 call 借用,方法通过原型链共享,且不执行父类构造函数初始化实例:
function inherit(Child, Parent) {
Child.prototype = Object.create(Parent.prototype); // 只继承原型,不执行构造
Child.prototype.constructor = Child; // 修正 constructor
}
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function () {
console.log(`${this.name}发出声音`);
};
function Dog(name, breed) {
Animal.call(this, name); // 借用构造:实例属性独立
this.breed = breed;
}
inherit(Dog, Animal);
Dog.prototype.bark = function () {
console.log("汪汪");
};
const dog = new Dog("旺财", "柴犬");
dog.speak(); // 旺财发出声音
dog.bark(); // 汪汪
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
console.log(dog.speak === new Dog("a").speak); // true,方法共享三种继承方式对比:
| 方式 | 实例属性独立 | 方法复用 | 可向父类传参 | 缺点 |
|---|---|---|---|---|
| 原型链继承 | 否 | 是 | 否 | 引用属性共享、无法传参 |
| 借用构造函数 | 是 | 否 | 是 | 方法无法复用 |
| 寄生组合式 | 是 | 是 | 是 | 代码稍复杂(class 已封装) |
十一、Object.create 的实现
Object.create(proto) 的本质是"以 proto 为原型创建一个新对象",可用一个临时构造函数模拟:
function create(proto) {
function F() {} // 临时构造函数
F.prototype = proto; // 让新对象的原型指向 proto
return new F(); // 实例的 __proto__ 即为 proto
}
const base = { hello() { return "world"; } };
const obj = create(base);
console.log(obj.hello()); // world
console.log(Object.getPrototypeOf(obj) === base); // true实现继承时常用 Object.create(Parent.prototype) 而非 new Parent(),因为前者不会执行父类构造函数、不会创建多余的实例属性,这正是寄生组合式继承的核心理念。
原型链是 JavaScript 面向对象的基础设施,class 语法(见《ES6+ 类》)正是建立在这一机制之上的语法糖。