文章目录
前言:为什么要学习原型?
原型(Prototype)是JavaScript中最重要的核心概念之一,它是JavaScript实现继承的基础机制。理解原型和原型链,对于以下几个方面至关重要:
- 深入理解JavaScript对象模型
- 掌握JavaScript中继承的实现方式
- 理解内置对象(如Array、String等)的工作原理
- 能够更好地使用和扩展JavaScript内置对象
- 理解现代JavaScript框架和库的源码
// 在学习原型前,我们可能写出这样的代码来共享方法
function createPerson(name, age) {
return {
name: name,
age: age,
greet: function() {
return `你好,我是${this.name},今年${this.age}岁`;
}
};
}
const person1 = createPerson('张三', 25);
const person2 = createPerson('李四', 30);
// 问题:每个person对象都有自己的greet方法副本,浪费内存
// 通过原型,我们可以让所有实例共享一个方法
JavaScript对象基础回顾
在深入原型之前,我们需要回顾JavaScript对象的基础知识:
对象的创建方式
// 1. 对象字面量
const person = {
name: '张三',
age: 25,
sayHello() {
console.log(`你好,我是${this.name}`);
}
};
// 2. 构造函数
function Person(name, age) {
this.name = name;
this.age = age;
this.sayHello = function() {
console.log(`你好,我是${this.name}`);
};
}
const person1 = new Person('张三', 25);
// 3. Object.create()
const personProto = {
sayHello() {
console.log(`你好,我是${this.name}`);
}
};
const person2 = Object.create(personProto);
person2.name = '李四';
构造函数的问题
function Person(name, age) {
this.name = name;
this.age = age;
// 每次创建实例时,都会创建一个新的函数对象
this.sayHello = function() {
console.log(`你好,我是${this.name}`);
};
}
const person1 = new Person('张三', 25);
const person2 = new Person('李四', 30);
console.log(person1.sayHello === person2.sayHello); // false,两个不同的函数
// 这导致内存浪费,而原型可以解决这个问题
原型对象基础概念
什么是原型?
在JavaScript中,每个函数都有一个特殊的属性叫做prototype
(原型),这个属性是一个对象。而每个通过这个函数创建的对象都有一个指向这个原型对象的内部链接。
function Person(name) {
this.name = name;
}
// Person.prototype 是一个对象
console.log(typeof Person.prototype); // "object"
// 向原型添加方法
Person.prototype.sayHello = function() {
console.log(`你好,我是${this.name}`);
};
// 创建实例
const person1 = new Person('张三');
const person2 = new Person('李四');
// 两个实例共享原型上的方法
person1.sayHello(); // "你好,我是张三"
person2.sayHello(); // "你好,我是李四"
// 证明方法是共享的
console.log(person1.sayHello === person2.sayHello); // true
访问原型的方式
// 1. 通过构造函数的prototype属性
console.log(Person.prototype);
// 2. 通过对象的__proto__属性(不推荐直接使用,但有助于理解)
console.log(person1.__proto__);
console.log(person1.__proto__ === Person.prototype); // true
// 3. 通过Object.getPrototypeOf()(推荐方式)
console.log(Object.getPrototypeOf(person1));
console.log(Object.getPrototypeOf(person1) === Person.prototype); // true
原型的可视化表示
+-------------+ +------------------+
| Constructor | | Prototype Object |
| (Person) | | |
| |----------->| constructor -----+----+
| | prototype | | |
+-------------+ | sayHello() | |
^ | | |
| +------------------+ |
| |
| |
+--------------------------------------------+
+-------------+
| Instance |
| (person1) |
| |
| name: '张三' |
| |
+-------------+
|
| [[Prototype]] (__proto__)
|
v
+------------------+
| Prototype Object |
| (Person.prototype)|
| |
| constructor |
| sayHello() |
| |
+------------------+
构造函数与原型
构造函数、原型对象和实例之间有一个三角关系:
constructor属性
原型对象默认有一个constructor
属性,指回构造函数:
function Person(name) {
this.name = name;
}
// 原型的constructor属性指向构造函数
console.log(Person.prototype.constructor === Person); // true
const person1 = new Person('张三');
// 实例可以通过原型访问constructor属性
console.log(person1.constructor === Person); // true
// 可以通过constructor创建新实例
const person2 = new person1.constructor('李四');
console.log(person2.name); // "李四"
重写原型对象时的注意事项
function Person(name) {
this.name = name;
}
// 完全重写原型对象
Person.prototype = {
sayHello() {
console.log(`你好,我是${this.name}`);
},
introduce() {
console.log(`我的名字是${this.name}`);
}
};
// 此时constructor属性丢失了!
console.log(Person.prototype.constructor === Person); // false
console.log(Person.prototype.constructor === Object); // true
// 修正constructor
Person.prototype = {
constructor: Person, // 手动恢复constructor指向
sayHello() {
console.log(`你好,我是${this.name}`);
}
};
// 或者这样修正
Object.defineProperty(Person.prototype, 'constructor', {
value: Person,
enumerable: false, // 不可枚举
writable: true,
configurable: true
});
原型链详解
原型链是JavaScript实现继承的核心机制,它的基本思想是:对象有一个指向原型的链接,如果在对象上找不到属性,就会沿着这个链接到原型上查找,原型本身也是对象,也有自己的原型,这样就形成了一个链条。
原型链查找过程
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log(`你好,我是${this.name}`);
};
const person1 = new Person('张三');
// 查找属性或方法的过程:
// 1. 首先在person1对象自身查找
// 2. 如果找不到,到person1.__proto__(即Person.prototype)查找
// 3. 如果还找不到,继续到Person.prototype.__proto__(即Object.prototype)查找
// 4. 如果仍找不到,则返回undefined
console.log(person1.name); // "张三"(在对象自身找到)
person1.sayHello(); // "你好,我是张三"(在原型上找到)
console.log(person1.toString()); // "[object Object]"(在Object.prototype上找到)
console.log(person1.someMethod); // undefined(整个原型链上都没找到)
原型链的终点
// 原型链最终会指向null
console.log(Object.prototype.__proto__); // null
// 验证原型链
console.log(person1.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true
原型链可视化
person1 ---> Person.prototype ---> Object.prototype ---> null
| | |
| | |
name sayHello() toString()
constructor hasOwnProperty()
等其他内置方法...
实例属性与原型属性
理解实例属性与原型属性之间的区别和关系非常重要:
属性遮蔽(Property Shadowing)
function Person() {}
Person.prototype.name = '原型上的名字';
const person1 = new Person();
console.log(person1.name); // "原型上的名字"(从原型获取)
// 添加同名实例属性
person1.name = '实例上的名字';
console.log(person1.name); // "实例上的名字"(实例属性遮蔽了原型属性)
// 删除实例属性后,原型属性"重新显露"
delete person1.name;
console.log(person1.name); // "原型上的名字"
检测属性的位置
function Person() {}
Person.prototype.name = '张三';
const person = new Person();
person.age = 25;
// 检查属性是否在对象自身(而非原型)上
console.log(person.hasOwnProperty('age')); // true
console.log(person.hasOwnProperty('name')); // false
// 检查属性是否存在(无论是自身还是原型)
console.log('age' in person); // true
console.log('name' in person); // true
console.log('toString' in person); // true(来自Object.prototype)
// 判断一个属性是否来自原型
function isPrototypeProperty(object, property) {
return property in object && !object.hasOwnProperty(property);
}
console.log(isPrototypeProperty(person, 'name')); // true
console.log(isPrototypeProperty(person, 'age')); // false
遍历实例属性与原型属性
function Person() {}
Person.prototype.sayHello = function() {};
const person = new Person();
person.name = '张三';
person.age = 25;
// for...in循环会遍历所有可枚举属性,包括原型上的
for (let prop in person) {
console.log(prop); // 输出: "name", "age", "sayHello"
}
// 只遍历实例自身的属性
for (let prop in person) {
if (person.hasOwnProperty(prop)) {
console.log(prop); // 输出: "name", "age"
}
}
// 直接获取对象自身的所有属性
const ownProps = Object.getOwnPropertyNames(person);
console.log(ownProps); // ["name", "age"]
// 获取对象的原型属性
const protoProps = Object.getOwnPropertyNames(Person.prototype);
console.log(protoProps); // ["constructor", "sayHello"]
原型的动态性
原型对象的变化会实时反映到所有实例上,这是原型的动态特性:
动态添加原型方法
function Person(name) {
this.name = name;
}
const person1 = new Person('张三');
const person2 = new Person('李四');
// 动态地在原型上添加方法
Person.prototype.sayHello = function() {
console.log(`你好,我是${this.name}`);
};
// 即使是在创建实例之后添加的方法,所有实例也能访问
person1.sayHello(); // "你好,我是张三"
person2.sayHello(); // "你好,我是李四"
原型的完全替换
function Person(name) {
this.name = name;
}
const person1 = new Person('张三');
// 完全替换原型
Person.prototype = {
constructor: Person,
sayHi() {
console.log(`Hi, I'm ${this.name}`);
}
};
// 已创建的实例仍然连接到旧原型
// person1.sayHi(); // Error: person1.sayHi is not a function
// 新创建的实例会连接到新原型
const person2 = new Person('李四');
person2.sayHi(); // "Hi, I'm 李四"
// 验证原型连接
console.log(person1.__proto__ !== Person.prototype); // true
console.log(person2.__proto__ === Person.prototype); // true
原型链判定
function Person() {}
const person = new Person();
// instanceof 操作符检查原型链
console.log(person instanceof Person); // true
console.log(person instanceof Object); // true
// isPrototypeOf方法检查原型链
console.log(Person.prototype.isPrototypeOf(person)); // true
console.log(Object.prototype.isPrototypeOf(person)); // true
// Object.getPrototypeOf获取原型
console.log(Object.getPrototypeOf(person) === Person.prototype); // true
使用原型实现继承
JavaScript通过原型链实现继承,这是它区别于其他语言的独特特性:
原型继承的基本实现
// 父类
function Animal(name) {
this.name = name;
this.species = '动物';
}
Animal.prototype.makeSound = function() {
console.log('一些声音...');
};
// 子类
function Dog(name, breed) {
// 调用父类构造函数
Animal.call(this, name);
this.breed = breed;
this.species = '狗';
}
// 设置原型链,让Dog继承Animal
Dog.prototype = Object.create(Animal.prototype);
// 修复constructor
Dog.prototype.constructor = Dog;
// 给Dog添加自己的方法
Dog.prototype.makeSound = function() {
console.log('汪汪汪!');
};
Dog.prototype.fetch = function() {
console.log(`${this.name}在捡球`);
};
// 使用
const dog = new Dog('小黑', '拉布拉多');
console.log(dog.name); // "小黑"
console.log(dog.species); // "狗"
dog.makeSound(); // "汪汪汪!"
dog.fetch(); // "小黑在捡球"
// 验证继承关系
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
console.log(dog instanceof Object); // true
继承的可视化表示
原型链结构:
Dog.prototype ---> Animal.prototype ---> Object.prototype ---> null
| | |
| | |
makeSound() makeSound() toString()
fetch() (被Dog覆盖) hasOwnProperty()
constructor 等其他内置方法...
dog实例:
|
| [[Prototype]]
V
Dog.prototype
常见的继承模式
1. 原型链继承
function Parent() {
this.colors = ['red', 'blue', 'green'];
}
function Child() {}
// 子类原型指向父类实例
Child.prototype = new Parent();
Child.prototype.constructor = Child;
// 问题: 所有Child实例共享引用类型属性
const child1 = new Child();
const child2 = new Child();
child1.colors.push('black');
console.log(child2.colors); // ["red", "blue", "green", "black"]
2. 借用构造函数(经典继承)
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
function Child(name, age) {
// 调用父类构造函数
Parent.call(this, name);
this.age = age;
}
// 优点: 避免共享引用类型属性
const child1 = new Child('张三', 18);
const child2 = new Child('李四', 20);
child1.colors.push('black');
console.log(child1.colors); // ["red", "blue", "green", "black"]
console.log(child2.colors); // ["red", "blue", "green"]
// 缺点: 无法继承原型上的方法
3. 组合继承(最常用)
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
// 继承属性
Parent.call(this, name);
this.age = age;
}
// 继承方法
Child.prototype = new Parent();
Child.prototype.constructor = Child;
Child.prototype.sayAge = function() {
console.log(this.age);
};
const child1 = new Child('张三', 18);
child1.colors.push('black');
child1.sayName(); // "张三"
child1.sayAge(); // 18
const child2 = new Child('李四', 20);
console.log(child2.colors); // ["red", "blue", "green"]
4. 寄生组合继承(最优解)
function inheritPrototype(Child, Parent) {
// 创建父类原型的副本
const prototype = Object.create(Parent.prototype);
// 增强对象
prototype.constructor = Child;
// 指定对象
Child.prototype = prototype;
}
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
// 继承
inheritPrototype(Child, Parent);
Child.prototype.sayAge = function() {
console.log(this.age);
};
// 使用
const child = new Child('张三', 18);
child.sayName(); // "张三"
child.sayAge(); // 18
ES6 class与原型
ES6引入了类语法,但这只是语法糖,底层仍然使用原型:
类与构造函数的对比
// ES5构造函数
function PersonES5(name, age) {
this.name = name;
this.age = age;
}
PersonES5.prototype.sayHello = function() {
console.log(`你好,我是${this.name}`);
};
// ES6类
class PersonES6 {
constructor(name, age) {
this.name = name;
this.age = age;
}
// 方法自动添加到原型上
sayHello() {
console.log(`你好,我是${this.name}`);
}
}
// 验证两者等价
const person1 = new PersonES5('张三', 25);
const person2 = new PersonES6('李四', 30);
console.log(typeof PersonES6); // "function"
console.log(PersonES6.prototype.sayHello); // [Function: sayHello]
console.log(person2.__proto__ === PersonES6.prototype); // true
ES6类继承
class Animal {
constructor(name) {
this.name = name;
}
makeSound() {
console.log('一些声音...');
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // 调用父类构造函数
this.breed = breed;
}
makeSound() {
console.log('汪汪汪!');
}
fetch() {
console.log(`${this.name}在捡球`);
}
}
const dog = new Dog('小黑', '拉布拉多');
dog.makeSound(); // "汪汪汪!"
dog.fetch(); // "小黑在捡球"
// 底层仍然是原型继承
console.log(dog.__proto__ === Dog.prototype); // true
console.log(Dog.prototype.__proto__ === Animal.prototype); // true
静态方法与静态属性
class MathHelper {
// 静态方法
static add(x, y) {
return x + y;
}
// 静态属性 (ES2022+)
static PI = 3.14159;
}
// 静态方法直接通过类调用
console.log(MathHelper.add(5, 3)); // 8
console.log(MathHelper.PI); // 3.14159
// 在原型模型中,静态方法/属性直接添加到构造函数上
function MathHelperES5() {}
MathHelperES5.add = function(x, y) {
return x + y;
};
MathHelperES5.PI = 3.14159;
常见问题与实用技巧
检测属性存在的各种方法
const obj = { name: 'test', age: 0, empty: null };
// 1. in 操作符(检查自身和原型链)
console.log('name' in obj); // true
console.log('toString' in obj); // true (来自原型链)
// 2. hasOwnProperty(只检查自身)
console.log(obj.hasOwnProperty('name')); // true
console.log(obj.hasOwnProperty('toString')); // false
// 3. undefined比较(容易出错)
console.log(obj.name !== undefined); // true
console.log(obj.age !== undefined); // true
console.log(obj.empty !== undefined); // true
console.log(obj.notExist !== undefined); // false
// 4. Object.hasOwn() (ES2022+, 推荐使用)
console.log(Object.hasOwn(obj, 'name')); // true
console.log(Object.hasOwn(obj, 'toString')); // false
扩展内置对象原型
// 扩展String原型 (谨慎使用!)
String.prototype.reverse = function() {
return this.split('').reverse().join('');
};
console.log('hello'.reverse()); // "olleh"
// 安全地扩展 - 检查是否已存在
if (!String.prototype.startsWith) {
String.prototype.startsWith = function(search) {
return this.indexOf(search) === 0;
};
}
理解对象属性的描述符
// 定义不可枚举的属性
Object.defineProperty(Object.prototype, 'hiddenMethod', {
value: function() { return '找到我了!'; },
enumerable: false, // 不会在for...in中出现
writable: true, // 可以被重写
configurable: true // 可以被删除或修改特性
});
const obj = {};
console.log(obj.hiddenMethod()); // "找到我了!"
// 不会在循环中出现
for (let prop in obj) {
console.log(prop); // 不会输出"hiddenMethod"
}
// 获取属性描述符
const descriptor = Object.getOwnPropertyDescriptor(
Object.prototype, 'hiddenMethod'
);
console.log(descriptor.enumerable); // false
封装私有属性
function Counter() {
// 私有变量
let count = 0;
// 特权方法可以访问私有变量
this.increment = function() {
return ++count;
};
this.decrement = function() {
return --count;
};
this.getCount = function() {
return count;
};
}
const counter = new Counter();
console.log(counter.getCount()); // 0
counter.increment();
console.log(counter.getCount()); // 1
console.log(counter.count); // undefined (无法直接访问)
总结与最佳实践
原型使用原则
- 合理使用原型:将共享的方法和属性放在原型上,将实例特有的属性放在构造函数中
function Person(name, age) {
// 实例特有的属性
this.name = name;
this.age = age;
// 每个实例可能不同的引用类型数据
this.friends = [];
}
// 共享的方法放到原型上
Person.prototype.sayHello = function() {
console.log(`你好,我是${this.name}`);
};
Person.prototype.getAge = function() {
return this.age;
};
- 避免在原型上放置引用类型:
// 不好的做法
function Person() {}
Person.prototype.friends = ['Alice', 'Bob']; // 所有实例共享同一个数组
const person1 = new Person();
const person2 = new Person();
person1.friends.push('Carol');
console.log(person2.friends); // ['Alice', 'Bob', 'Carol'] - 意外修改!
// 更好的做法
function Person() {
this.friends = ['Alice', 'Bob']; // 每个实例有自己的数组
}
- 使用Object.create()代替对象字面量:
// 不好的做法
ChildType.prototype = ParentType.prototype; // 直接引用,修改子类原型会影响父类原型
// 好的做法
ChildType.prototype = Object.create(ParentType.prototype);
ChildType.prototype.constructor = ChildType;
- 使用现代继承模式:
// 首选使用ES6类
class Parent {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}
class Child extends Parent {
constructor(name, age) {
super(name);
this.age = age;
}
sayAge() {
console.log(this.age);
}
}
// 或使用寄生组合继承
function inheritPrototype(Child, Parent) {
const prototype = Object.create(Parent.prototype);
prototype.constructor = Child;
Child.prototype = prototype;
}
- 谨慎扩展内置原型:
// 避免这样做,可能导致命名冲突
Array.prototype.contains = function(item) {
return this.indexOf(item) !== -1;
};
// 如果必须扩展,使用Symbol属性可减少冲突
const contains = Symbol('contains');
Array.prototype[contains] = function(item) {
return this.indexOf(item) !== -1;
};
// 使用
const arr = [1, 2, 3];
console.log(arr[contains](2)); // true
关键概念总结
-
原型链查找顺序:实例 → 实例原型 → 父类原型 → … → Object.prototype → null
-
本质: JavaScript使用原型链实现继承,通过引用而非复制来共享代码
-
实例与原型关系: 实例通过内部[[Prototype]]链接到原型,可以通过
__proto__
或Object.getPrototypeOf()
访问 -
构造函数与原型关系: 每个函数都有一个
prototype
属性指向其原型对象,原型有一个constructor
属性指回构造函数 -
ES6类:只是原型继承的语法糖,底层实现仍然基于原型链
理解JavaScript的原型系统对于成为一名优秀的JavaScript开发者至关重要。尽管它最初可能看起来复杂,但一旦掌握,你就能更深入地理解JavaScript的工作原理,编写更优雅、更高效的代码。