原型与面向对象
JavaScript 以原型链实现对象共享与「继承」;ES6 class 是语法糖。见 语言基础、函数进阶。
一、原型链
1. [[Prototype]] 内部槽位
每个 JavaScript 对象都有一个内部属性 [[Prototype]],指向另一个对象(或 null)。当访问对象的一个属性时,如果对象自身没有,就会沿着 [[Prototype]] 链条向上查找,直到找到或到达 null(原型链的终点)。
2. prototype 属性
函数(尤其是构造函数)拥有 prototype 属性。当通过 new 调用该函数创建实例时,实例的 [[Prototype]] 会指向构造函数的 prototype 对象。
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function () {
console.log(`Hi, I'm ${this.name}`);
};
const p = new Person("Alice");
// p.[[Prototype]] === Person.prototype
3. __proto__ 访问器
__proto__ 是大多数 JavaScript 环境提供的非标准但广泛支持的访问器,用于读写对象的 [[Prototype]]。标准方法推荐使用 Object.getPrototypeOf() 和 Object.setPrototypeOf()。
const obj = {};
console.log(obj.__proto__ === Object.prototype); // true
4. Object.create(proto, [propertiesObject])
创建一个新对象,并将其 [[Prototype]] 显式指定为第一个参数。
const animal = {
eat() {
console.log("Eating");
},
};
const dog = Object.create(animal);
dog.bark = function () {
console.log("Woof");
};
dog.eat(); // 'Eating' (来自原型)
Object.create(null) 会创建一个没有原型的纯字典对象(适合存储数据,避免属性名冲突)。
原型链示例
function Parent() {}
Parent.prototype.method = function () {};
function Child() {}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
const c = new Child();
// 原型链: c -> Child.prototype -> Parent.prototype -> Object.prototype -> null
二、继承方式
1. 原型链继承
子构造函数的 prototype 指向父构造函数的实例。
缺点:所有子实例共享父实例的引用类型属性(如数组);无法向父构造函数传参。
function Parent() {
this.colors = ["red"];
}
function Child() {}
Child.prototype = new Parent();
const c1 = new Child();
c1.colors.push("blue"); // 影响所有实例
2. 构造函数盗用(经典盗用 / call/apply)
在子构造函数内部调用父构造函数,并绑定 this。
缺点:不能复用父原型上的方法,每个实例都会创建一份方法副本。
function Parent(name) {
this.name = name;
}
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
3. 组合继承(原型链 + 构造函数盗用)
优点:父构造函数执行两次(一次用于 Child.prototype = new Parent(),一次在 Child 内部调用),但解决了传参与属性共享问题。
缺点:效率稍低(父实例的无关属性存在于原型上)。
function Parent(name) {
this.name = name;
}
Parent.prototype.say = function () {};
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
4. 寄生组合继承(最优)
避免调用两次父构造函数,使用 Object.create 创建父原型的副本,然后增强该对象。
function inheritPrototype(child, parent) {
const prototype = Object.create(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}
function Parent(name) {
this.name = name;
}
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
inheritPrototype(Child, Parent);
优点:只调用一次父构造函数,原型链干净,是 ES5 环境下最理想的继承模式。
5. ES6 class 的本质(语法糖)
ES6 的 class 和 extends 底层仍是基于原型链的寄生组合继承。
class Parent {
constructor(name) {
this.name = name;
}
}
class Child extends Parent {
constructor(name, age) {
super(name);
this.age = age;
}
}
class 内部实现了:
- 构造函数的
prototype关联 - 自动调用
super()即Parent.call(this) - 静态方法挂载在类本身上
- class 方法定义在
prototype上且默认不可枚举(与手写Person.prototype.say = function(){}默认可枚举不同)
三、类的进阶特性
1. 静态方法(static)
使用 static 关键字定义的方法属于类本身,而非实例。它们通常用于工具函数。
class MathUtils {
static add(a, b) {
return a + b;
}
}
console.log(MathUtils.add(2, 3)); // 5
// 实例无法访问静态方法: new MathUtils().add -> TypeError
静态方法内部 this 指向类(而非实例),也可以被 extends 子类继承。
2. 私有字段(#)
ES2022 正式引入私有字段,只能在类内部访问,真正实现封装。
class Person {
#age;
constructor(name, age) {
this.name = name;
this.#age = age;
}
#privateMethod() {
return this.#age;
}
getAge() {
return this.#privateMethod();
}
}
const p = new Person("Bob", 30);
console.log(p.name); // 'Bob'
console.log(p.#age); // SyntaxError
console.log(p.getAge()); // 30
私有字段在编译时静态绑定,不会因为 Object.getOwnPropertyNames 等操作暴露。
3. instanceof 与 Symbol.hasInstance
instanceof 运算符检查对象的原型链中是否包含构造函数的 prototype 属性。
[] instanceof Array; // true
[] instanceof Object; // true (因为 Array.prototype 继承自 Object.prototype)
可以通过 Symbol.hasInstance 自定义 instanceof 行为:
class MyArray {
static [Symbol.hasInstance](instance) {
return Array.isArray(instance);
}
}
console.log([] instanceof MyArray); // true
四、new 操作符内部步骤
使用 new 调用一个构造函数时,引擎会执行以下 4 个步骤:
- 创建一个新的空对象:
const obj = {}; - 设置原型:将新对象的
[[Prototype]]指向构造函数的prototype属性:obj.__proto__ = Constructor.prototype;(内部操作是Object.setPrototypeOf(obj, Constructor.prototype)) - 绑定
this并执行构造函数:调用Constructor并将this绑定到新对象上,传递参数:const result = Constructor.apply(obj, args); - 返回对象:
- 如果构造函数返回一个对象(非
null),则返回该对象; - 否则返回第 1 步创建的新对象
obj。
- 如果构造函数返回一个对象(非
function Person(name) {
this.name = name;
return { custom: true }; // 如果返回对象,new 的结果就是这个对象
}
const p = new Person("Alice");
console.log(p); // { custom: true },不是 Person 实例
正常情况下,构造函数应避免显式返回对象,以确保 new 行为符合预期。
总结
- 原型链是 JavaScript 实现继承的底层机制,通过
[[Prototype]]串联对象。 - 各种继承模式从混乱到优雅,最终 ES6
class提供了清晰的语法,其本质仍然是原型链。 - 现代 JavaScript 支持静态方法、私有字段等,进一步增强了类的封装性和表达能力。
- 理解
new的执行步骤有助于掌握构造函数和对象创建的过程。
掌握这些内容,就能深入理解 JavaScript 的对象系统,写出更健壮、优雅的代码。
参考文献
以下链接在编写时均可正常访问:
| 资料 | 说明 |
|---|---|
| MDN:继承与原型链 | 原型链 |
| MDN:class | ES6 类语法 |
| MDN:new | new 运算符 |
| MDN:instanceof | 类型检测 |
| ECMA-262:对象 | 规范(英文) |
相关文章
错误处理与调试
结构化错误类型、try/catch 与全局未捕获处理是健壮应用的基础。
浏览器相关 API
DOM 事件、存储、fetch、Worker、Observer 等由浏览器提供的 Web API(非 ECMAScript 核心)。HTML 结构见 标签。
性能优化
防抖/节流、渲染与内存、资源预加载及引擎行为是前端性能的主要抓手。相关 CSS 见 视觉效果、Head 资源提示。
模块化与工程化
ESM、CommonJS 与打包工具(Tree Shaking、代码分割)是前端工程化的基础。入门见 JavaScript 基础 · 模块。
执行模型与作用域
执行上下文与词法作用域决定变量在何处可访问、闭包如何形成,以及 var / let / const 的差异。语言概览见 JavaScript 基础。
数据结构与集合增强
Map / Set、WeakMap / WeakSet、数组高阶方法、TypedArray 与 Symbol 等扩展了内置数据结构能力。见 语言基础。