以下内容都是基于读者有了一定的原型链理解基础上写的,仅仅只是浅薄的理解,如有错误欢迎指出。
首先需要明白这么一点,也是理解原型链的基础:原型链的作用是什么?
为了回答这个问题,要引入以下情景(或许你看过,但这个例子我认为是最合适作为理解原型链的作用的一个):
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); // 结果为false
这表示通过构造函数生成对象的时候,内部的变量和方法都会重新生成,且需要重新开辟内存空间,这就导致了性能上和空间上的浪费:因为对于每一个猫猫来讲,他们都会发出叫声,所以这个方法应该是Cat
构造函数生成的 cat
对象之间共享
的。
所以为了解决这个性能、空间上会浪费的问题,引入了原型链
的概念。
在讲我个人的理解之前,首先要讲下上面那个理解如何让voice在对象之间共享:
Cat.prototype.voice = function () {
console.log('喵喵');
};
cat1.voice(); // 发出喵叫
cat2.voice();
console.log(cat1.voice === cat2.voice); // true
直接在
Cat
构造函数的prototype
对象
上定义voice
方法即可。
这么直接写一段抽象的话可能很难理解,但是不要害怕,往下看。
我以我个人的理解来阐述下我是如何去理解原型链这个概念的。
我默认大家对
__proto__、prototype
等属性已经有了一个大致或者全面的了解。不了解的话可以先去了解下,知道他们是干嘛,有啥关系就行。
原型链
的理解,我着重在链
字,想象一串链条,一条一条链条结组成长长的链,可以从末端顺着链条
爬上顶端,有开始
也有结束
,他们之间通过链条结
之间一个一个互相连接起来
每一个由构造函数生成的对象都会有一个__proto__
属性,该属性只存在对象中,而在构造函数中含有prototype
属性。比如上面的例子中,cat1
就有:cat1.__proto__
,而Cat有:Cat.prototype
二者是严格相等的!
console.log(cat1.__proto__ === Cat.prototype); // true
于是就可以这么说: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)翻译成雏形
会不会更好理解一点,原型链是为了解决什么问题而造出来的,对象和原型对象之间通过是什么链接起来,二者有啥对等关系没有?
可能我理解的,或者说我书写的有误,欢迎大家指出。