JavaScript原型链:继承的本质
我们先整理一下,通过构造函数可以创建一个实例对象,那么实例对象如何使用构造函数里的属性和方法呢?
- 构造函数里有:一个属性
protoType,属性值是一个对象,里边放着可以共享的属性和方法 - 实例对象里有: 一个指针
__proto__,指向构造函数的protoType属性的属性值,所以通过指针就可以访问构造函数的属性和方法。但是最好不要使用这个属性,因为它不是规范中规定的。ES5 中新增了一个Object.getPrototypeOf()方法,可以通过这个方法来获取对象的原型;一个constructor属性,指回构造函数本身
当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是 Object.prototype 所以这就是新建的对象为什么能够使用 toString() 等方法的原因。
一切从对象开始
在JavaScript中,几乎一切都是对象。当你创建一个数组、函数或对象字面量时,它们都和一个隐藏的链接相关联——这个链接指向另一个对象,我们称之为“原型”。
const person = {
name: '小明',
greet() {
console.log(`你好,我是${this.name}`);
}
};
console.log(person.toString()); // [object Object]
这里有个有趣的现象:person对象并没有定义toString方法,但它却可以调用。这是因为它沿着原型链找到了这个方法。
原型链的核心机制
每个JavaScript对象都有一个内部属性[[Prototype]](在浏览器中通常通过__proto__访问),它指向另一个对象。当访问一个对象的属性时,JavaScript引擎会执行以下步骤:
- 在对象自身的属性中查找
- 如果没找到,沿着
[[Prototype]]链向上查找 - 如果找到原型链末端(
null)还没找到,返回undefined
const animal = {
eat: true
};
const rabbit = {
jump: true,
__proto__: animal // 设置原型
};
console.log(rabbit.jump); // true(自身属性)
console.log(rabbit.eat); // true(原型上的属性)
console.log(rabbit.toString); // function(原型链更深处的属性)
函数与构造函数的特殊地位
函数在JavaScript中扮演着特殊角色。每个函数都有一个prototype属性,但这个属性并不是函数的原型,而是当这个函数作为构造函数时,创建的新对象的原型。
function Dog(name) {
this.name = name;
}
Dog.prototype.bark = function() {
console.log(`${this.name}汪汪叫`);
};
const dog1 = new Dog('旺财');
const dog2 = new Dog('来福');
dog1.bark(); // 旺财汪汪叫
dog2.bark(); // 来福汪汪叫
console.log(dog1.bark === dog2.bark); // true(共享同一个方法)
这里的关键关系是:
dog1和dog2的内部[[Prototype]]指向Dog.prototypeDog.prototype本身也是一个对象,它的内部[[Prototype]]指向Object.prototypeObject.prototype的内部[[Prototype]]是null,这是原型链的终点
原型的动态性
原型关系是动态的。即使对象已经创建,修改原型仍然会影响所有现有实例:
Dog.prototype.run = function() {
console.log(`${this.name}跑起来了`);
};
dog1.run(); // 旺财跑起来了(即使dog1已经创建)
这种动态性为JavaScript带来了极大的灵活性,但也需要留意性能影响——原型链上的属性查找比自身属性稍慢。
constructor属性的奇妙作用
每个原型对象都有一个constructor属性,指回构造函数本身:
console.log(Dog.prototype.constructor === Dog); // true
console.log(dog1.constructor === Dog); // true(通过原型链找到)
这个属性可以用来创建新对象:
const dog3 = new dog1.constructor('小花');
原型链与继承的实现
利用原型链,我们可以实现继承:
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
console.log(`${this.name}正在吃东西`);
};
function Cat(name, color) {
Animal.call(this, name); // 调用父构造函数
this.color = color;
}
// 建立继承关系
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat; // 修复constructor指向
Cat.prototype.meow = function() {
console.log(`${this.name}喵喵叫`);
};
const kitty = new Cat('小白', '白色');
kitty.eat(); // 小白正在吃东西(继承的方法)
kitty.meow(); // 小白喵喵叫(自己的方法)
原型链的查找性能与优化
虽然原型链很强大,但使用时需要注意:
- 属性屏蔽:如果对象自身有属性,就不会查找原型链
kitty.name = '小黑'; // 屏蔽原型链上的name
- 检查属性:使用
hasOwnProperty区分自身属性和原型属性
console.log(kitty.hasOwnProperty('name')); // true
console.log(kitty.hasOwnProperty('eat')); // false
- 性能考量:原型链越长,查找越慢。保持原型链的合理深度很重要。
现代JavaScript中的原型操作
ES6引入了更清晰的操作方式:
// 获取原型
Object.getPrototypeOf(kitty);
// 设置原型
const proto = { x: 10 };
const obj = {};
Object.setPrototypeOf(obj, proto);
// 更推荐的对象创建方式
const newObj = Object.create(proto);