JavaScript 原型
什么是原型?
在JavaScript中,原型是一个核心概念,它是JavaScript实现对象继承的基础机制。与传统的基于类的继承不同,JavaScript使用原型链的方式来实现对象之间的继承关系。
每个JavaScript对象都有一个与之关联的原型对象。当我们试图访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript引擎会自动去对象的原型上查找。这种机制称为"原型继承"。
原型链基础
每个JavaScript对象都有一个特殊的内部属性[[Prototype]]
(在一些浏览器中可以通过__proto__
访问),它指向该对象的原型。原型本身也是一个对象,因此也有自己的原型,这样就形成了一个原型链。
原型链的终点通常是Object.prototype
,而Object.prototype
的原型是null
。
访问和设置原型
使用Object.getPrototypeOf()
和Object.setPrototypeOf()
const person = {
name: '张三',
sayHello() {
return `你好,我是${this.name}`;
}
};
const student = {
grade: '一年级'
};
// 设置student的原型为person
Object.setPrototypeOf(student, person);
// 获取student的原型
console.log(Object.getPrototypeOf(student) === person); // 输出:true
// 通过原型链调用方法
console.log(student.sayHello()); // 输出:"你好,我是张三"
使用__proto__
(不推荐在生产环境使用)
const person = {
name: '张三',
greet() {
return `你好,我是${this.name}`;
}
};
const student = {
grade: '一年级',
__proto__: person
};
console.log(student.greet()); // 输出:"你好,我是张三"
__proto__
是一个已废弃的属性,不应在生产代码中使用。请使用Object.getPrototypeOf()
和Object.setPrototypeOf()
或构造函数来代替。
构造函数和原型
在JavaScript中,构造函数是创建对象的模板。每个构造函数都有一个prototype
属性,指向一个对象,这个对象就是通过该构造函数创建的实例的原型。
// 定义一个构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
// 在原型上添加方法
Person.prototype.sayHello = function() {
return `你好,我是${this.name},我今年${this.age}岁`;
};
// 创建实例
const person1 = new Person('李四', 25);
const person2 = new Person('王五', 30);
console.log(person1.sayHello()); // 输出:"你好,我是李四,我今年25岁"
console.log(person2.sayHello()); // 输出:"你好,我是王五,我今年30岁"
// 验证原型关系
console.log(person1.__proto__ === Person.prototype); // 输出:true
console.log(person2.__proto__ === Person.prototype); // 输出:true
构造函数、原型和实例的关系
原型继承
JavaScript中实现继承的一种方式是通过原型链。
使用Object.create()
// 父对象
const animal = {
type: '动物',
eat() {
return `${this.name}正在吃东西`;
}
};
// 使用Object.create()创建一个继承自animal的对象
const dog = Object.create(animal);
dog.name = '旺财';
dog.bark = function() {
return '汪汪!';
};
console.log(dog.eat()); // 输出:"旺财正在吃东西"
console.log(dog.bark()); // 输出:"汪汪!"
构造函数继承
// 父构造函数
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
return `${this.name}正在吃东西`;
};
// 子构造函数
function Dog(name, breed) {
// 调用父构造函数
Animal.call(this, name);
this.breed = breed;
}
// 设置原型链
Dog.prototype = Object.create(Animal.prototype);
// 修复constructor指向
Dog.prototype.constructor = Dog;
// 添加子类特有方法
Dog.prototype.bark = function() {
return `${this.breed}${this.name}:汪汪!`;
};
const myDog = new Dog('小黑', '拉布拉多');
console.log(myDog.eat()); // 输出:"小黑正在吃东西"
console.log(myDog.bark()); // 输出:"拉布拉多小黑:汪汪!"
属性查找机制
当我们访问一个对象的属性时,JavaScript引擎会按照以下步骤进行查找:
- 首先在对象自身查找该属性
- 如果没有找到,则在对象的原型上查找
- 如果还没有找到,则继续在原型的原型上查找
- 重复以上步骤,直到找到该属性或到达原型链的末尾(
null
)
const grandparent = {
lastName: '王',
hobby: '下棋'
};
const parent = Object.create(grandparent);
parent.job = '教师';
parent.hobby = '读书'; // 覆盖祖父的爱好
const child = Object.create(parent);
child.name = '小明';
console.log(child.name); // 输出:"小明"(自身属性)
console.log(child.job); // 输出:"教师"(来自父对象)
console.log(child.lastName); // 输出:"王"(来自祖父对象)
console.log(child.hobby); // 输出:"读书"(来自父对象,覆盖了祖父对象的属性)
console.log(child.age); // 输出:undefined(整个原型链上都没有这个属性)
实际应用案例
案例1:创建可复用的UI组件
// 基础组件原型
const UIComponent = {
render() {
return `<div class="${this.className}">${this.content}</div>`;
},
setContent(content) {
this.content = content;
return this;
}
};
// 创建按钮组件
const Button = Object.create(UIComponent);
Button.className = 'btn';
Button.content = '点击我';
Button.click = function(callback) {
console.log('按钮被点击');
if (typeof callback === 'function') {
callback();
}
};
// 使用组件
console.log(Button.render()); // 输出:<div class="btn">点击我</div>
Button.setContent('提交').click(() => console.log('表单已提交'));
console.log(Button.render()); // 输出:<div class="btn">提交</div>
案例2:创建一个简单的插件系统
// 基础插件系统
const PluginSystem = {
plugins: {},
register(name, plugin) {
this.plugins[name] = plugin;
return this;
},
use(name, ...args) {
if (this.plugins[name]) {
return this.plugins[name].apply(this, args);
}
return null;
}
};
// 创建应用实例
const app = Object.create(PluginSystem);
// 注册一些插件
app.register('logger', function(message) {
console.log(`[LOG]: ${message}`);
return true;
});
app.register('formatter', function(text) {
return text.toUpperCase();
});
// 使用插件
app.use('logger', 'Hello World'); // 输出:[LOG]: Hello World
const formatted = app.use('formatter', 'hello javascript');
console.log(formatted); // 输出:HELLO JAVASCRIPT
原型方法
JavaScript提供了一些原型相关的方法,帮助我们更好地操作原型:
Object.create()
创建一个新对象,使用现有的对象作为新创建对象的原型。
const person = {
isHuman: true,
introduce() {
return `我的名字是 ${this.name}. 我是一个${this.isHuman ? '人类' : '机器人'}`;
}
};
const me = Object.create(person);
me.name = 'Jack';
console.log(me.introduce()); // 输出:"我的名字是 Jack. 我是一个人类"
hasOwnProperty()
用于检查对象是否具有指定的自有属性(不是继承来的)。
const parent = { lastName: '张' };
const child = Object.create(parent);
child.firstName = '小明';
console.log(child.hasOwnProperty('firstName')); // 输出:true
console.log(child.hasOwnProperty('lastName')); // 输出:false
isPrototypeOf()
用于检查一个对象是否存在于另一个对象的原型链上。
const animal = { eats: true };
const rabbit = Object.create(animal);
const longEar = Object.create(rabbit);
console.log(animal.isPrototypeOf(rabbit)); // 输出:true
console.log(animal.isPrototypeOf(longEar)); // 输出:true
console.log(rabbit.isPrototypeOf(animal)); // 输出:false
ES6 中的类与原型
ES6引入了class
语法,使JavaScript的面向对象编程更接近传统的基于类的语言。但在底层,JavaScript的class
仍然是基于原型的。
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name}发出声音`;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
speak() {
return `${this.name}汪汪叫`;
}
fetch() {
return `${this.name}在捡东西`;
}
}
const dog = new Dog('小花', '柯基');
console.log(dog.speak()); // 输出:"小花汪汪叫"
console.log(dog.fetch()); // 输出:"小花在捡东西"
实际上,上面的代码在底层仍然使用原型链实现继承。
总结
在JavaScript中,原型是一种非常强大的机制,它使对象能够从其他对象继承属性和方法。理解原型对于掌握JavaScript面向对象编程至关重要。通过原型,我们可以:
- 实现对象之间的继承关系
- 共享方法,减少内存占用
- 动态扩展对象功能
- 创建可复用的对象模板
尽管ES6引入了更易于理解的类语法,但JavaScript的对象系统仍然基于原型。深入理解原型机制可以帮助我们更有效地使用JavaScript,编写更高效、更优雅的代码。
学习原型时,建议先理解基本概念,再通过实际编码加深理解。可以尝试改造本文中的示例,或者自己创建一些继承关系来巩固所学知识。
练习
-
创建一个
Vehicle
原型对象,包含type
和move()
方法,然后创建Car
和Bicycle
对象继承自Vehicle
,并添加各自特有的方法。 -
实现一个简单的继承函数
extend(Child, Parent)
,使Child
构造函数能够继承Parent
构造函数的原型方法。 -
使用原型模式设计一个简单的表单验证系统,包括一个基础验证器和几个特定类型的验证器(如邮箱验证器、密码验证器等)。
参考资源
继续深入学习JavaScript原型将帮助你更好地理解JavaScript这门语言的核心机制,为掌握更复杂的JavaScript概念和设计模式打下坚实基础。