关于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出不同鸟种了,确实要比之前挨个写不同鸟的对象要节省代码很多。但是又出现一个问题了,比如不同鸟的属性——feet、isFly、say都是一样的。
我们知道new操作会在内存空间给这些对象们开辟一个空间,相当于每个对象空间里面都有这个三个共同而值也相同的属性。
maque.say === bage.say // false
以上代码说明同样的say方法在不同的对象体内,从极客角度来说,这是一种对空间的浪费!假如你是开发js的工程师,想想要怎么处理?
三、原型与原型链
当年设计JavaScript的作者,就想到一个方法:拿Bird来说,不同鸟的name、color都是不同的,好!这算私有属性,就通过构造函数继承各自赋值就可以。
而像feet、isFly、say所有的鸟都是一样,没必要放在各自对象的空间里面,想知道顺继承链路往上找父辈查询即可。
比如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.say是bage.__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. 来自原型
在这个例子中,bage的say被一个新值给屏蔽了,但其他如实例对象还是返回Bird原型对象中值。说明该实例添加这个属性,在该实例身上已经搜索到了该属性对应的值,就不会再网上搜索了。但这个行为,不能该表该实例所指向Bird的原型对象中属性值。
JavaScript中也提供delete操作符完全删除实例属性,只能重新访问原型中的的属性。
delete bage.say
bage.say // I have two feet. 来自原型
了解到这儿,基本算是把原型和原型链基本原理搞清楚了。