JS的原型与原型链
以代码案例+问题引导的方式讲解原型与原型链
一、从实例+构造函数中发现原型链
构造函数constructor能够通过实例化(new),生成实例。
构造函数constructor
自身有一个属性prototype
,用于存储一些公用的属性和方法,来提供给自己生成的实例使用。
//自定义一个构造函数Pokemon
function Pokemon(name,attr,skill){
this.name = name;
this.attr = attr;
this.skill = skill;
}
//往prototype上添加属性
Pokemon.prototype.fight = function(){
let {name,skill} = this;
return `${name},使用${skill}!`
}
let Pikachu = new Pokemon('皮卡丘','电','十万伏特');
//创建的实例可以使用fight这个方法。
console.log(Pikachu.fight());// >>> 皮卡丘,使用十万伏特!
console.log(Pikachu.toString());// >>> [object Object]
为什么实例可以使用自己构造函数prototype的方法?
因为每当定义一个带继承的实例(对象),这个对象都会存在一个__proto__
(隐式原型),指向的是这个对象的构造函数的prototype
(显式原型)。
console.log('两者相等:',Pikachu.__proto__ === Pokemon.prototype);//true
如果访问了对象自己没有的属性或者方法,就会自动通过__proto__
属性进行查找,从上面的例子来看,调用Pikachu.fight()
时,就访问到了它构造函数上定义的fight()
函数。
那为什么构造函数prototype上没有toString()方法也能调用呢?
因为构造函数身上的prototype
,这只是一个普通的对象,既然是对象,就可以把它看做一个实例,自然它也会有自己的构造函数。
JS构造函数一般都用大写字母开头做区分:
- Object :对象的构造函数
- Function:函数的构造函数
- Array:数组的构造函数
- …
所以prototype
,把它看做普通对象,它的构造函数自然就是Object
。而它的隐式原型:prototype.__proto__
,就会指向构造函数的原型Object.prototype
。
所以执行Pikachu.toString()
的顺序是:
- 自己身上没有toString,访问
Pikachu.__proto__
- 到达构造函数原型
Pokemon.prototype
,发现也没有toString,继续访问Pokemon.prototype.__proto__
- 到达构造函数原型
Object.prototype
,找到了toString,执行。
那么会不会一直__proto__访问下去?
并不会,最终会指向对象源头Object构造函数的prototype,而这个最终的prototype的__proto__
是null。
// 到达Pokemon的原型,访问fight
console.log(Pikachu.__proto__);
// 到达prototype原型(Object.prototype),访问到toString
console.log(Pikachu.__proto__.__proto__);
// 到达原型链尽头(Object.prototype.__proto__) null
console.log(Pikachu.__proto__.__proto__.__proto__);
// 直接到达尽头:null
console.log(Object.prototype.__proto__);
原型链定义: 对象没有的属性或者方法,会通过__proto__
属性指向原型对象的prototype
一层一层往上找,直到Object的原型对象位置,层层继承的链式结构就叫原型链,而null就是原型链的尽头。
二、实例、构造函数、原型三者关系
上面对于原型链的访问过程中,貌似没有构造函数constructor
的身影
那么它在实例和原型之间的关系是怎么样的呢?
prototype
:是构造函数所独有的,本质是一个普通的对象,存在的目的是为实例提供各种方法与属性。__proto__
:是实例对象所独有的,指向自身构造函数的原型对象prototype,本质是普通的对象,存在的目的是完成原型链的访问。constructor
:原型对象prototype上都有constructor属性,指向的是这个prototype关联的构造函数,存在的目的是为了让prototype能访问回构造函数
三、原生原型链关系
我们平时创建一个数组array,创建一个对象object,创建一个函数function、set、map。这些实例都能使用它们原型对象上的方法。
比如函数的call/bind/apply,数组的filter/reduce/slice等方法。
原生的JS就已经定义好了他们的构造函数和原型对象,我们在此基础上去自定义构造函数,或者类,都是在对原型链的延长。
原生JS已经构造好的原型链关系又是怎么样的?
其实从上面就不难看出,所谓的实例,构造函数,原型都是相对的概念。
Pikachu
作为实例,有它的构造函数和原型- 但它自己的原型
Pokemon.prototype
作为实例,也有构造函数和原型
这里比较绕脑子的是,只要是对象和函数,都可以作为原型,找到自己的构造函数和原型。
- 构造函数本身也是函数,可以看做实例
- 原型本身就是对象,也可以看做实例
这里需要补充一下说明:
f-native(底层函数),是构造函数Function
的原型,也就是Function.prototype
访问到的目标,它本身是一个匿名函数,这里用底层函数来称呼它,关于它的特殊性,在文章末尾会说明。
console.log(Function.prototype);//输出的是ƒ () { [native code] }
console.dir(Function.prototype);//输出的是ƒ anonymous()
如果把不同的目标看做实例,可以有以下的对应关系,可以在编译器上输出验证以下的结果。
实例 | 构造函数 | 隐式原型(__proto__ ) |
---|---|---|
Pikachu | Pokemon | Pokemon.prototype |
Pokemon | Function | f-native |
Object | Function | f-native |
f-native | Function | Object.prototype |
Function | Function | f-native |
Object.prototype | Object | null |
四、底层函数f native
上文说到,一般来说,Object的prototype,或者一个构造函数的prototype。其本身是向构造函数提供方法和属性的,所以prototype都是非函数,是一个普通的对象,拥有__proto__
,却没有prototype。
但在图解中出现了一个特殊的角色,那就是Function
的原型f native
,它特殊的地方在于,它并不是一个对象,而是一个函数。
从上面的图解可以看出:
- 构造函数Function的prototype还是一个函数
- 构造函数Function的constructor总是指向自己本身,并且可以无限调用constructor
以下为验证过程:
console.warn('Function构造函数原型的特殊性');
console.log('Pokemon的原型是对象:',typeof(Pokemon.prototype));
console.log('Object的原型是对象:',typeof(Object.prototype));
console.log('Function的原型是底层函数,依旧是函数:',typeof(Function.prototype));
console.warn('Function的constructor是自己,并且可以无限调用:');
console.log('Function.constructor:',Function.constructor.constructor);
console.warn('Function的prototype与__proto__都指向底层函数f native:',);
console.log(Function.prototype === Function.__proto__);
- 原型链的顶端就是null,这是为了防止原型链的查找没有尽头,这是js的设计思想。
- 函数访问的顶端是就Function,即使调用了constructor还是会指向自己,防止了函数的调用会有null的出现,这是也是js的设计思想。
第一篇文章,如果有错误请指出,让我快速纠正…