JavaScript的原型与原型链总结

5,636 阅读5分钟

关于JavaScript的原型与原型链的理解可能是前端工程师的一个难点。其实这部分知识还是很容易理解的,只是有些书或者博客把简单的东西写得抽象化了。

本文今天就带大家彻底掌握js相关的对象、构造函数、原型与原型链。

一、先了解一下对象

对象这个概念在很多高级语言中都有,JavaScript中的对象也无非是由一些无序的key-value对组成。其中value可以是基本值,对象或者函数。

var maque = {
    name: '麻雀',
    color:'gray',
    feet:'2',
    isFly:true,
    say: function() {
        console.log('I have two feet.')
    }
}  
// 这便是一个关于Bird的对象
// 也可以这样写,非常原始的方式(不太建议)
var maque = new Object();  // 直接点:var Bird ={}
maque.name='麻雀';
maque.color = 'gray';
maque.feet = '2';
maque.isFly = true;
maque.say = function() {
        console.log('I have two feet.')
    }

以上便是JavaScript创建对象的方式。假如现在需要我们写一百个鸟种的对象,难不成真要这样写一百个吗?想想都知道,正常程序员谁会干这种事?正经编程语言设计者也会为你考虑这件事!

怎么样才能让这个场景用最少代码的实现?学过其他语言的同学,立马想到——继承一个鸟类模板

在了解JavaScript的继承之前,我们需要再了解一下JavaScript的构造函数。

二、构造函数

构造函数的作用就是提供模板,生成对象实例,作为函数就可以通过传参进行复用。

JavaScript的构造函数,其实该函数与普通函数并无区别。具体代码如下:

var Bird  = function (name,color) {
    this.name=name,
    this.color=color,
    this.feet='2',
    this.isFly=true,
    this.say=function() {
        console.log('I have two feet.')
    }
} 
// 写好鸟的构造函数,就相当于一个鸟类生成模板做好
// 接下啦就是不断生成不同鸟了

var maque = new Bird('麻雀','gray');
var bage = new Bird('bage','black');
// 这么看起来,确实要比挨个写不同鸟的对象要简单方便多了

似乎这样我们就可以基于Bird这个构造函数(模板)不断new出不同鸟种了,确实要比之前挨个写不同鸟的对象要节省代码很多。但是又出现一个问题了,比如不同鸟的属性——feetisFlysay都是一样的。

我们知道new操作会在内存空间给这些对象们开辟一个空间,相当于每个对象空间里面都有这个三个共同而值也相同的属性。

maque.say === bage.say  // false

以上代码说明同样的say方法在不同的对象体内,从极客角度来说,这是一种对空间的浪费!假如你是开发js的工程师,想想要怎么处理?

三、原型与原型链

当年设计JavaScript的作者,就想到一个方法:拿Bird来说,不同鸟的namecolor都是不同的,好!这算私有属性,就通过构造函数继承各自赋值就可以。

而像feetisFlysay所有的鸟都是一样,没必要放在各自对象的空间里面,想知道顺继承链路往上找父辈查询即可。

比如JavaScript引擎想知道bage.feet到底有几支脚,在bage当前对象找不到,行,那顺着继承路径往上找,发现其继承的构造函数Bird上这个属性,并且有值,feet=2。那么bage.feet就会返回2。

同理bage.hihi,在bage当前对象找不到,在其继承的构造函数Bird也找不到,Bird在js世界也不是无中生有的,也是继承着js的函数对象的,也找不到。

要实现这个方法,需要有两套机制:

1、给构造函数增添一个有别于私有属性的共享属性,值是对象形式。js中把这个公共属性对象,叫作原型对象,关键词:prototype

2、生成的对象与1的这个公共属性之间要有一条链路的,通过这条链路能取到对应的属性值。js中把这条,叫作原型链,关键词:__ proto __

JavaScript有了这两套机制,在继承方面就可以实现构造函数+原型的组合继承了。

var Bird  = function (name,color) {
    this.name=name,
    this.color=color
}
Bird.prototype.feet = '2';  
Bird.prototype.isFly = true;
Bird.prototype.say=function() {
        console.log('I have two feet.')
    }
    
var bage = new Bird('八哥','black')
bage.name // "八哥"
bage.feet  // "2"
var maque = new Bird('麻雀','gray')
bage.say === maque.say  // true,不同对象函数属性是一样

同时在Chrome中打印出来可以看到:

bage.__proto__  === Bird.prototype // true

以上两个的内容是完全一样的,就相当于通过Bird``new出来的实例,通过__proto__属性,指向构造函数的原型对象,让实例对象也能够访问原型对象上的属性值和方法。

其实在浏览器引擎眼里:bage.saybage.__proto__.say的简写,因为在bage这个实例对象上找不到对应的say属性。浏览器引擎先是通过bage.__proto__链接到了Bird的原型对象Bird.prototype,然后在Bird.prototype找到对应的say属性方法。

假如实例对象bage觉得say说出“I have two feet.”很傻X,想改成“I am bage!”霸气点。代码如下:

bage.say=function() {
        console.log('I am bage!')
    }
bage.say // I am bage! 来自实例
maque.say //但其他实例对象还是:I have two feet. 来自原型

在这个例子中,bagesay被一个新值给屏蔽了,但其他如实例对象还是返回Bird原型对象中值。说明该实例添加这个属性,在该实例身上已经搜索到了该属性对应的值,就不会再网上搜索了。但这个行为,不能该表该实例所指向Bird的原型对象中属性值。

JavaScript中也提供delete操作符完全删除实例属性,只能重新访问原型中的的属性。

delete bage.say
bage.say // I have two feet. 来自原型

了解到这儿,基本算是把原型和原型链基本原理搞清楚了。