原型&原型链

106 阅读5分钟

image.png

1.构造函数创建对象

我们首先使⽤构造函数创建⼀个对象,这里Person()就是构造函数,person就是new出来的实例对象。

function Person() {

}

var person = new Person();
person.name = 'xiaoman';
console.log(person.name) // xiaoman

2.prototype(原型)

JavaScript 常被描述为一种基于原型的语言——每个对象拥有一个原型对象,每个函数都有⼀个prototype属性,⽐如:

//prototype是函数才会有的属性
Person.prototype.name = 'xiaoman';

那这个函数的 prototype属性到底指向的是什么呢?是这个函数的原型吗?

其实,函数的 prototype 属性指向了⼀个原型对象,这个对象正是调⽤该构造函数⽽创建的实例的原型,也就是这个例⼦中的 person 的原型。

用控制台打印Person.prototype得到:

{
    constructor: ƒ Person(),
    __proto__: {
        constructor: ƒ Object(),
        hasOwnProperty: ƒ hasOwnProperty(),
        isPrototypeOf: ƒ isPrototypeOf(),
        propertyIsEnumerable: ƒ propertyIsEnumerable(),
        toLocaleString: ƒ toLocaleString(),
        toString: ƒ toString(),
        valueOf: ƒ valueOf()
    }
}

那什么是原型呢?你可以这样理解:每⼀个JavaScript对象(null除外)在创建的时候就会与之关联另⼀个对象,这个对象就是我们所说的原型,每⼀个对象都会从原型"继承"属性。

⽤⼀张图表示构造函数和实例原型之间的关系:

image.png

这⾥⽤ Person.prototype 表示实例原型。

那么该怎么表示实例与实例原型,也就是 person 和 Person.prototype 之间的关系呢?

每⼀个JavaScript对象(除了 null )都具有的⼀个属性,叫 __ proto__ ,这个属性会指向该对象的原型。

function Person() {

}

var person = new Person();

console.log(person.__proto__ === Person.prototype); // true

用一张图表示一下: image.png

既然实例对象和构造函数都可以指向原型,那么原型是否有属性指向构造函数或者实例呢?

指向实例倒是没有,因为⼀个构造函数可以⽣成多个实例,但是原型指向构造函数是有的:constructor ,每个原型都有⼀个 constructor 属性指向关联的构造函数

console.log(Person === Person.prototype.constructor); //true

image.png

所以,到这⾥可以得到:

console.log(person.__proto__ == Person.prototype) // true
console.log(Person.prototype.constructor == Person) // true
console.log(Object.getPrototypeOf(person) === Person.prototype) // true

2.1 实例与原型

当试图访问一个实例的属性时,它不仅仅在该实例对象上搜寻,还会搜寻该实例对象的原型,以及该实例对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾

准确地说,这些属性和方法定义在Object的构造器函数(constructor functions)之上的prototype属性上,而非实例对象本身

举个例⼦:

function Person() {

}

Person.prototype.name = 'xiaoman';
var person = new Person();
person.name = 'xiaoman';
console.log(person.name) // xiaoman

delete person.name;
console.log(person.name) // xiaoman

在这个例⼦中,我们给实例对象 person 添加了 name 属性,当我们打印 person.name的时候,结果⾃然为 xiaoman。 但是当我们删除了 person 的 name 属性时,读取 person.name ,从 person 对象中找不到name属性就会从 person 的原型也就是 person.__ proto__,也就是 Person.prototype 中查找,结果为 xiaoman 。

2.2原型的原型

其实原型对象就是通过 Object 构造函数⽣成的,结合之前所讲,实例的 __ proto__指向构造函数的 prototype ,所以我们再更新下关系图:

image.png

3.原型链

那 Object.prototype 的原型呢?

null,我们可以打印:

console.log(Object.prototype.__proto__ === null) // true

然⽽ null 究竟代表了什么呢?

null 表示“没有对象” ,即该处不应该有值。

所以 Object.prototype.__ proto__ 的值为 null 跟 Object.prototype 没有原型,其实表达了⼀个意思。所以查找属性的时候查到 Object.prototype 就可以停⽌查找了。

最后⼀张关系图也可以更新为:

image.png

其中,蓝⾊为原型链

4.原型链继承

现在我们知道了什么是原型和原型链,那么什么是原型链继承呢?

如果试图引用对象(实例instance)的某个属性,会首先在对象内部寻找该属性,直至找不到,然后才在该对象的原型(instance.prototype)里去找这个属性.

如果让原型对象指向另一个类型的实例.....有趣的事情便发生了.

即: Person.prototype = person2

鉴于上述游戏规则生效,如果试图引用Person构造的实例person1的某个属性age:

1).首先会在person1内部属性中找一遍;

2).接着会在person1.__ proto__(constructor1.prototype)中找一遍,constructor1.prototype 实际上是person2, 也就是说在person2中寻找该属性age;

3).如果person2中还是没有,此时程序不会灰心,它会继续在person2.__ proto__(constructor2.prototype)中寻找...直至Object的原型对象

搜索轨迹: instance1--> instance2 --> constructor2.prototype…-->Object.prototype

function Father(){
	this.property = true;
}
Father.prototype.getFatherValue = function(){
	return this.property;
}
function Son(){
	this.sonProperty = false;
}
//继承 Father
Son.prototype = new Father();//Son.prototype被重写,导致Son.prototype.constructor也一同被重写
Son.prototype.getSonVaule = function(){
	return this.sonProperty;
}
var instance = new Son();
alert(instance.getFatherValue());//true

instance实例通过原型链找到了Father原型中的getFatherValue方法.

注意: 此时instance.constructor指向的是Father,这是因为Son.prototype中的constructor被重写的缘故.

原型链继承的缺点

问题一: 当原型链中包含引用类型值的原型时,该引用类型值会被所有实例共享;

问题二: 在创建子类型(例如创建Son的实例)时,不能向超类型(例如Father)的构造函数中传递参数.

5.确定原型和实例的关系

使用原型链后, 我们怎么去判断原型和实例的这种继承关系呢? 方法一般有两种.

第一种是使用 instanceof 操作符, 只要用这个操作符来测试实例(instance)与原型链中出现过的构造函数,结果就会返回true. 以下几行代码就说明了这点.

alert(instance instanceof Object);//true 
alert(instance instanceof Father);//true 
alert(instance instanceof Son);//true

由于原型链的关系, 我们可以说instance 是 Object, Father 或 Son中任何一个类型的实例. 因此, 这三个构造函数的结果都返回了true.

第二种是使用 isPrototypeOf()  方法, 同样只要是原型链中出现过的原型,isPrototypeOf() 方法就会返回true, 如下所示.

alert(Object.prototype.isPrototypeOf(instance));//true 
alert(Father.prototype.isPrototypeOf(instance));//true 
alert(Son.prototype.isPrototypeOf(instance));//true

原理同上.

6.其他

6.1 constructor

关于 constructor 属性:

function Person() {

}

var person = new Person();

console.log(person.constructor === Person); // true

当获取 person.constructor时,其实 person 中并没有 constructor 属性,当不能读取到constructor 属性时,会从 person 的原型也就是 Person.prototype 中读取,正好原型中有该属性,所以:

person.constructor === Person.prototype.constructor

6.2 __ proto__

绝⼤部分浏览器都⽀持这个⾮标准的⽅法访问原型,然⽽它并不存在于 Person.prototype中,实际上,它是来⾃于 Object.prototype,与其说是⼀个属性,不如说是⼀个 getter/setter ,当使⽤ obj.__ proto__ 时,可以理解成返回了 Object.getPrototypeOf(obj) 。