JS应知应会之原型和原型链

159 阅读3分钟

从构造函数开始

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.address = 'shenzhen';

let person1 = new Person('xiaoming',22);
console.log(person1.name); // xiaoming
console.log(person1.age); // 22
console.log(person1.address); // 'shenzhen'

现在有一个叫 Person 的函数,通过它我们 new 了一个实例 person1 出来,对象 person1 上面拥有 nameage 属性, 但是输出的时候,person1 却可以打印出 address 的值?

那么,person1是怎么和 Person 建立起联系的呢?为什么 person1 能输出一个不属于自己的值呢?

ok,例子很简单,带着这这个问题我们继续往下看。

prototype

每一个函数都会拥有一个 prototype 属性,这个属性指向哪里呢?指向了一个对象,这个对象拥有这样一些属性(address 是添加进去的)

当这个函数作为构造函数去创建实例的话,那么这个实例的原型就会指向这个对象,也就是指向这个 Person.prototype。你也可以将 Person.prototype 理解指向了一个共享的对象。

通过构造函数创建的实例,并不是完全独立的,实例和构造函数间还保持着一些联系。

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.address = 'shenzhen'

let person1 = new Person('xiaoming',22);
let person2 = new Person('xiaohong',21);

console.log(person1.address); // 'shenzhen'
console.log(person2.address); // 'shenzhen'

构造函数和原型对象之间的关系如下图

_proto_

person1person2 都是通过 Person 创建出来的实例,他们都拥有 address 属性。也就是说,他们其实是在共享 Person.prototype上的属性。那他们是怎么拿到原型对象上的属性的呢?

__proto__ 是每个对象(除了null)都拥有的属性,实例的 __proto__ 会指向构造函数的 prototype 属性,也就是指向原型对象。

person1.__proto__ === Person.prototype // true
person2.__proto__ === Person.prototype // true
person1.__proto__ === person2.__proto__ // true

那么变量 person1 “拥有”(后面会解释到这个拥有为什么打引号)__proto__属性,并且还指向 Person.prototype,那就意味着 person1.__proto__.address === Person.prototype.address

constructor

打印 Person.prototype我们可以看到,除了添加上去的 address 属性之外,还有一个constructor属性

可以从图中看到,constructor属性其实是指向 Person 构造函数的。

Person === Person.prototype.constructor // true
Person === person1.__proto__.constructor // true
Person === person1.constructor // true

原型链

当我们想获取实例 person1 上的 address 属性时,首先会在 person1 本身寻找,如果 person1 没有这个 address 属性,那就会去它的原型对象 person1.__proto__ 上寻找,如果还没有,就继续在原型对象的原型上找,一直找到 Object.prototype(因为所有对象最终都可以追溯到由 Object 构造函数生成的) 上,还没有的话那就只能是 undefined 了。

所以我们看实例 person1 上好像并没有 constructor属性,但是 person1 却拥有了 constructor 属性,就是 person1 在自身并没有找到 constructor,但是它在 person1.proto上找到了。

串联起来就是

Person.prototype === person1.__proto__
Person.prototype.constructor === Person
Person.prototype.__proto__ === Object.prototype
person1.constructor === person1.__proto__.constructor

那么 Object.prototype 的原型又是啥呢?它就是 null啦,即 Object.prototype.__proto__ === null

读到这里,大家应该就明白实例person1 是如何读取到 Person.prototype.address的值了吧。

但是要注意 Person.prototypeconstructor 属性这是 Person 函数在声明时的默认属性,如果你创建了一个新的对象并且替换了函数默认的 prototype 对象引用,那么新对象并不会自动获取到 constructor 属性

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype = {};

let person3 = new Person('xiaoqiang',23);

console.log(person3.constructor === Person) // false
console.log(person3.constructor === Object) // true

Person.prototype 在被赋予一个新对象的时候,其中的 constructor 属性就已经没有了。

person3 本身并没有 constructor 属性,就会往上找 person3.__proto__,也就是 Person.prototype,但是它此时只是一个空对象,并没有 constructor 属性,所以还得继续往上找,找到了 Object.prototype,这个对象有 constructor 属性,所以 person3.constructor === Object,实际上的寻找过程是这样的 person3 -> person3.__proto__(Person.prototype) -> Person.prototype.__proto__(Object.prototype)

总结

我们总结一下:

所有函数都拥有 prototype 属性,它指向了实例的原型对象,每个对象都拥有一个 __proto__ 属性,也指向原型对象,而原型对象上有一个 constructor 属性,constructor 属性指向了构造函数。如果要访问对象上一个并不存在的属性的时候,它就会沿着原型链向上寻找,直到找到 Object.prototype 上,这是原型链的顶端,如果都找不到,那就返回了 undefinedtoString()valueOf()和其他一些通用的功能,都存在于 Object.prototype 对象上,因此所有对象都可以使用它们。