从构造函数开始
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 上面拥有 name 和 age 属性, 但是输出的时候,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_
person1 和 person2 都是通过 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.prototype 的 constructor 属性这是 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 上,这是原型链的顶端,如果都找不到,那就返回了 undefined,toString()、valueOf()和其他一些通用的功能,都存在于 Object.prototype 对象上,因此所有对象都可以使用它们。