什么是原型
原型的本质是一个对象
无论何时,只要创建一个函数,就会按照特定的规则为这个函数创建一个 prototype 属性(指向原型对象)。默认情况下,所有原型对象自动获得一个名为 constructor 的属性,自定义构造函数,默认情况下constructor指回与之关联的构造函数,其他的所有方法都继承自Object。
构造函数可以是函数表达式也可以是函数声明,但不可以是箭头函数
构造函数与原型之间的关系如下图所示:
下面这个例子也可以直接证明构造函数与原型之间的关系:
function Person(name,age) {
this.name = name;
this.age = age;
}
console.log([Person]);//构造函数有一个prototype属性
console.log(typeof Person.prototype);//object 原型本质是对象
console.log(Person.prototype.constructor === Person);//true 原型对象有一个contractor属性,且指向这个构造函数
原型与实例的关系
来看个例子,如下,person1中没有school属性,为什么打印出值不是undefined,而是构造函数Person原型对象的school属性的值呢?
function Person(name,age) {
this.name = name;
this.age = age;
}
//给Person的原型对象添加一个school属性
Person.prototype.school = 'prototype school';
let person1 = new Person('LiLei', 18);
let person2 = new Person('XiaoMing',18);
person2.school = 'my school';
console.log(person1.school,person2.school)//prototype school my school
原来,每个通过构造函数创建的实例都有一个属性__proto__,它指向的也是构造函数的原型对象。通过对象查找某个属性时,搜索开始于对象实例本身。如果在这个实例上发现了给定的名称,则返回该名称对应的值(如person2.school);如果没有找到这个属性,则搜索会沿着__proto__进入原型对象,然后在原型对象上找到属性后,再返回对应的值。
通过下面这个例子也可以证明上述所说:
console.log(person1.__proto__ === Person.prototype);//true
实例与原型之间的关系也可用下图表示:
原型链
通过上面的分析,我们知道了构造函数与原型、实例与原型的关系,那还有几个问题需要思考一下:
- 原型的原型是什么呢?
- 每个原型是否都是对象?
- 如果在实例本身和构造函数原型上都没有查找到属性,那应该如何去搜索这个属性值呢?
- 对于第一个问题(原型的原型是什么呢?),我们通过下面这个例子来探索一下:
function Person(name,age) {
this.name = name;
this.age = age;
}
function Shcool(name){}
console.log(Person.prototype.__proto__)//注:构造函数的原型是一个对象,对象应使用__proto__查看原型
console.log(Shcool.prototype.__proto__);
打印结果如上图所示,图上红框中我们可以看到,两个普通函数原型的原型中constructor指向的构造函数是Object,而构造函数Object原型指向的是Object.prototype,由此我们可以得出结论:普通函数 原型的原型是Object.prototype,我们也可以通过如下例子得出这个结论:
//正常原型链都会终止于Object的原型对象
console.log(Person.prototype.__proto__ == Object.prototype);//true
console.log(Person.prototype.__proto__.constructor == Object);//true
- 对于第二问题(每个原型是否都是对象?),我们还是通过一个例子来看一下:
function Person(name,age) {
this.name = name;
this.age = age;
}
console.log(Person.prototype.__proto__)//注:构造函数的原型是一个对象,对象应使用__proto__查看原型
从图中红框我们可以看到Object.prototype的原型为null,不是对象,我们同样用代码来证实一下:
//Object的原型的原型是null
console.log(Person.prototype.__proto__.__proto__ == null); // true
console.log(Object.prototype._proto__ == null); //true
根据以上两个问题:我们在来完善一下关系图:
- 对于第三个问题(如果在实例本身和构造函数原型上都没有查找到属性,那应该如何去搜索这个属性值呢?)我们可以浅浅猜想一下,对象查找某个属性时,搜索开始于对象实例本身。如果在这个实例上没有发现了给定的名称,则搜索会沿着__proto__进入原型对象,去原型对象上找属性,如果在原型上还是没有找到这个属性,就会去原型的原型上查找,直到最顶层null为止。
我们还是通过一个例子来分析一下是否和我们的猜想一致:
function Person(name,age) {
this.name = name;
this.age = age;
}
function Shcool(name){}
Object.prototype.sex = 'no sex';
let person = new Person('LiLie',18);
console.log(person.sex);//no sex
console.log(person.school);//undefind
分析一下这个例子,我们发现,person对象在查找sex属性时,在对象本身和__proto__指向的原型对象上都没有查找到该属性,但依然返回了sex属性的值,且这个值是Object.prototype(Object的原型对象)sex属性的值,而Object.prototype又是person.__proto__的原型,正好与我们的猜想一致,而我们的这个猜想其实就是原型链
我们来看一下红宝书对于原型链的描述: ECMA-262 把原型链定义为 ECMAScript 的主要继承方式。其基本思想就是通过原型继承多个引用类型的属性和方法。重温一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。如果原型是另一个类型的实例呢?那就意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。这样就在实例和原型之间构造了一条原型链。这就是原型链的基本构想。
总结一下什么是原型链: 通过对象查找某个属性时,搜索开始于对象实例本身,如果在这个实例上没有发现给定的名称,则搜索会沿着__proto__进入原型对象,如果在原型对象上也没有发现给定的名称,就去原型的原型上去搜索,直到最顶层null为止,像这样,通过原型一层层相互关联的链状结构就称为原型链。
最终的关系图如下所示:
参考文章
- JavaScript红宝书
- JavaScript深入之从原型到原型链