以下内容都是基于读者有了一定的原型链理解基础上写的,仅仅只是浅薄的理解,如有错误欢迎指出。
首先需要明白这么一点,也是理解原型链的基础:原型链的作用是什么? 为了回答这个问题,要引入以下情景(或许你看过,但这个例子我认为是最合适作为理解原型链的作用的一个):
function Cat(name){
this.name = name;
this.voice = function () {
console.log('喵喵');
};
}
let cat1 = new Cat('mimi');
let cat2 = new Cat('kiki');
以上代码中,我通过Cat 构造函数,定义了两个对象:cat1 和 cat2 ,他们都有对应的名字和方法,但是存在一个问题,cat1和cat2中都会存在一个 voice 方法,直观上看他们是一样的,但是实际上这俩方法并不相等:
console.log(cat1.voice === cat2.voice);
这表示通过构造函数生成对象的时候,内部的变量和方法都会重新生成,且需要重新开辟内存空间,这就导致了性能上和空间上的浪费:因为对于每一个猫猫来讲,他们都会发出叫声,所以这个方法应该是Cat 构造函数生成的 cat 对象之间共享 的。 所以为了解决这个性能、空间上会浪费的问题,引入了原型链 的概念。
在讲我个人的理解之前,首先要讲下上面那个理解如何让voice在对象之间共享:
Cat.prototype.voice = function () {
console.log('喵喵');
};
cat1.voice();
cat2.voice();
console.log(cat1.voice === cat2.voice);
直接在 Cat 构造函数的 prototype 对象 上定义voice 方法即可。 这么直接写一段抽象的话可能很难理解,但是不要害怕,往下看。
我以我个人的理解来阐述下我是如何去理解原型链这个概念的。
我默认大家对 __proto__、prototype 等属性已经有了一个大致或者全面的了解。不了解的话可以先去了解下,知道他们是干嘛,有啥关系就行。
原型链 的理解,我着重在链 字,想象一串链条,一条一条链条结组成长长的链,可以从末端顺着链条 爬上顶端,有开始 也有结束 ,他们之间通过链条结 之间一个一个互相连接起来 每一个由构造函数生成的对象都会有一个__proto__ 属性,该属性只存在对象中,而在构造函数中含有prototype 属性。比如上面的例子中,cat1 就有:cat1.__proto__ ,而Cat有:Cat.prototype 二者是严格相等的!
console.log(cat1.__proto__ === Cat.prototype);
于是就可以这么说:cat1的原型对象是Cat 。可能有的小伙伴对原型对象中这个原型二字感到很抽象,很陌生,或者说很难理解,那我换一个翻译:雏形。
雏形 是啥?CPU的原材料是沙子,或者说雏形是一堆沙子;借用西游记里孙悟空的一句话:妖怪,我要把你打回原形!比如是对一个由蛇精化作的美女而言,孙悟空一棒下去,直接把美女打成一条蛇了,为啥?因为本质没改变,外表是美女,本质是一条蛇,她的雏形(原型)就是一条蛇。
回到上面那个例子,cat1 的原型(雏形)是啥? 那肯定就是 Cat 了,因为 cat1 是由 Cat “变”(用变字可能不是很准确,但你能理解我的意思,对吧?)来的,cat1 再怎么改变,都无法脱离它的本质就是 Cat ,换句话说,原型就是 Cat
理解了上述内容,你可能就有疑问了,别的博客内写的:通过原型链(到现在为止,你都可以在大脑内想象一条链条存在,想象不出来看我上面贴的图 ),可以让对象访问到原型对象上的各种方法,甚至原型对象的原型对象上的方法(是不是比较绕?)。 其实很好理解:一条蛇化作了人,蛇有七情六欲,蛇会吃喝拉撒(神话意义上的蛇,不是动物世界的那种 ),那化作了人之后,不可能自己原来会做的事情,化成人了就不会做了吧?照样会吃,会动情,动怒。对应到Cat那个例子中去,就是:
Cat 会做的,cat1 照样会做。 Cat 在其 prototype 上定义的 voice ,cat1 和cat2 就能直接使用,为啥?因为他俩的本质(雏形,原型)就是Cat ,Cat 会的他俩自己也会。 是不是还是比较难理解?那我换个说法: Cat一出生就会的东西,它化成了其他样子,难道就不会了嘛?(抽象意义上的出生) 婴儿刚出生时就会的呼吸,长大成人了就不会了嘛? 例子可能不是好例子,但是这么理解我觉得是可以的。
你到目前为止可能一直有个疑问:是怎么会的?cat1是怎么会知道Cat有 voice 这个方法的?等等一堆疑问,解决这些疑问的唯一关键字就是:__proto__ 。 没错,对象和它的原型对象(雏形对象,本质对象)之间就是通过__proto__ 链 起来的! 注意这个链 字!
正如链条的链接需要通过一个一个链条结:
对象和原型对象(雏形,最初的那个对象)之间也是需要这种类似“结 ”的东西,来让它们之间链接起来,形成一个整体,一条完整的链条。这个“结 ”就是:__proto__ 。 就是这个小东西,让对象和原型对象之间有了连通的通道 ,有了从链条底端,向链条开端爬去的利器 。通过它,对象就能获取到原型对象上定义的方法(也就是定义在 prototype 对象上的方法 ,实际上在前面,我通过一段代码展示了对象的 __proto__属性是严格等于构造函数上的 prototype,就可以将二者看成是同一个东西,但我怕太抽象,所以就通过简单的例子来讲述这点。)。 你可以想象成在底下链条底下有只蚂蚁向上爬,在链条中的某个结点上有一小撮蜂蜜,蚂蚁爬上第一个结点,发现没有,继续向上爬,直到找到那个蜂蜜。 这里Js解释器就是通过__proto__ 属性一个一个向上去寻找,在cat1对象本身内,没有定义voice,那么就通过__proto__ ,发现它与Cat相等,就在Cat 内寻找,发现有voice 这个方法,于是就调用了。
很明显,小伙伴肯定也会意识到一个问题,链条有底端,也有开端,那么在原型链 概念中,是否也存在呢?
答案是显然的,有相关知识的小伙伴就会明白原型链的最顶端是null ,正如链条的开端上面是空气 一样。 借由前一个蛇精的例子,蛇是卵生,最开始就是一个卵。 所以有这么一个对应关系:人<----蛇精<-----蛇蛋。 蛋怎么来的?那就当做是空气中突然冒出的吧(null )。 前面的所有文字中,我刻意省略了Object这个东西,相信看过不少博客的小伙伴记都记住了这样一个关系(大致模样):obj <----> Funciont <----> Object <----> null 原型链的基础是从null 开始,慢慢延伸出Object ,再是Funcion ,再是一个具体的对象(obj )。 它们之间怎么链接起来的? 答:通过 __proto__
好了,以上就是我思考原型链的全过程。 现在回过头去看看原型链,或者说想一想原型链,你有没有理解到? 问问自己原型(prototype)翻译成雏形 会不会更好理解一点,原型链是为了解决什么问题而造出来的,对象和原型对象之间通过是什么链接起来,二者有啥对等关系没有?
可能我理解的,或者说我书写的有误,欢迎大家指出。
|