-前言-
js呢,是一种基于对象的语言,但它与真正的面向对象语言又是不同的。 首先在关键字中,早期js中,不提供class、implements(接口) 、extend(继承)以及class中public、private、static、protected等等多种访问修饰符;当然现在已经可以在js使用class,具体用法呢,可能会在写完本篇后总结,因为这个方面自己也挺感兴趣的(●'◡'●)。
话题引过来,所以,在早期的js要怎么实现这些功能呢?
这个答案就是原型!
1. 对象字面量与Object.prototype
在《JavaScript语言精粹》的第三章对象中提到过“每个对象都连接到一个原型对象,可以从中继承属性”。我们都清楚在js中创建标准对象使用对象字面量实现的,现在我们用对象字面量来创建一个猫类,拥有name和color属性:
let Cat={
name:'',
color:''
}
然后我们创建两个它的子类对象cat1和cat2,我们只能同样通过对象字面量来创建:
let cat1={
name:'大猫',
color:'黑色'
}
let cat2={
name:'小猫',
color:'灰色'
}
从上面代码,我们首先能明显的看出通过字面量来实现其他语言中的面向对象,代码描述是比较繁琐的,其次就是父与子之间,即对象与实例之间没有存在什么约束,它们虽然都具有统一个原型对象Object.prototype,但是它们之间并不存在原型链。
在面向对象中,首要任务是要把类里属性和方法封装起来,成为实例们的模板,所以很可惜对象字面量好像不能履行这个任务o(TヘTo)。那我们就得思考一下了,除了对象字面量可以构建对象外,是不是在js中还有构造函数也能创建对象(~ ̄▽ ̄)~
2.构造函数与prototype对象
主角来了!在js没有class关键字的时代,常用构造函数+prototype对象来模拟传统面向对象编程,称之为原型式的面向对象。所以我们就来用构造函数写一下上面的Cat例子:
let Cat=function(name,color){
this.name=name;
this.color=color;
}
let cat1=new Cat('大猫','黑色')
let cat2=new Cat('二猫','灰色')
为了让大家更清晰的知道为什么选择构造函数,这里有几个必须要知道的知识点:
- 使用构造函数构造的函数有一个大写约定,即该函数名首字母需要大写。
- 通过构造器产生的新函数对象(即Cat),新函数对象被赋予一个prototype属性,其值是一个包含一个constructor属性且属性值为该新函数对象;该prototype对象是存放继承特征的地方(重点!!!)
- 用new前缀调用的函数创建实例,this也会绑定到这个新实例。
- 实例们通过自身的__proto__(两边都是两个下划线哦)私有属性,共享构造函数的原型对象上的方法。
看到这你们可能会有些晕晕的,但是不要担心,我们通过上面的Cat类例子来具体了解这个constructor+prototype:
console.log(Cat.constructor)
// 输出的是 [Function: Function]
console.log(Cat.prototype)
// 输出的是在Cat.prototype上创建方法,因为这里还没创建,所以输出的是{}
console.log(Cat.prototype.contructor);
// 输出的是 [Function: Object](若在Cat.prototype上创建了方法)
(若没有在Cat.prototype上创建方法,输出的是[Function:Cat])
// 这里验证了构造函数构造的Cat函数对象拥有一个prototype属性,其值包含一个constructor属性。
2.1 _ptoto_与prototype
该构造函数通过new前缀去创建的实例对象,会继承来自构造函数的原型对象。在这里我们必须要理解每个对象自身都是有一个私有的属性_proto_,它与prototype的关系是:
- _proto_是当前对象的函数原型对象(隐式原型),而prototype是当前构造函数的原型对象(显示原型)
- 每个对象的_proto_属性指向自身构造函数的prototype!
我们用Cat的例子验证下:
console.log(cat1.__proto__==Cat.prototype)
// 输出的是true
console.log(cat2.__proto__==Cat.prototype)
// 输出的也是true
2.2 验证继承
那这样是不是就实现了面向对象中的继承,如果不相信话也可以通过下面这些例子验证一下:
//验证父类:
console.log(cat1.constructor);
// 在控制台输出的是 [Function:Cat]
console.log(cat2.constructor)
// 在控制台输出的也是 [Function:Cat]
//zui两个输出我们可以看出 cat1.constructor和cat2.constructor 指向的都是它们自身的构造函数Cat,那么我们就能使用这个方法验证一些实例对象是不是兄弟关系;
//验证兄弟:
console.log(cat1.constructor==cat2.constructor)
// 输出 true
//验证子类:
console.log(cat1 instanceof Cat);
// 输出 true
console.log(cat2 instanceof Cat);
// 输出 true
除了上述验证是否继承,这里还有一个更简单的方法,调用Cat类中的方法。那么问题来了,我们是直接在Cat里创建方法呢,还是在Cat.ptototype上创建呢?
答案很明显就是在Cat.prototype上创建,前面也已经说过该prototype对象是存放继承特征的地方,还有一个最主要的地方就是在原型对象上创建的方法,实例们能自己调需要的方法,相比于在Cat构造函数内创建方法来得要更灵活一些,更省很多内存!( ̄︶ ̄)↗
让我们现在在Cat.prototype上创建一个eat()方法:
Cat.prototype.eat=function(){
console.log('吃喵粮~');
}
// 实例调用:
cat1.eat(); //输出:吃喵粮~
同理,在Cat.prototype上创建属性也是实例们可以共享的,我们在这原型对象上创建一个kinds(种类)属性:
Cat.prototype.kinds='';
// 实例调用:
cat1.kinds='田园猫';
cat2.kinds='咖啡猫';
2.3 原型链上的常用方法
大家看到这可能会有些疑惑,在想为什么在原型上创建属性这么简单的例子要写出来?答案就是我们引入原型链上常用的方法:hasOwnProperty()和isPrototypeOf(). 首先要声明的一点就是,在js中,除了number、string、undefined、boolean、null这些简单数据类型之外,其他都是复杂类型对象。所以这Cat构造函数以及cat1、cat2实例本质都是对象,都会继承Object.prototype,而hasOwnProperty()和isPrototypeOf()就是其原型链上的方法:
- 使用hasOwnProperty()方法判断的是某自身对象是否含有特定的属性:
// kinds属性是我们在Cat.prototype上创建的,实例cat1和cat2可以从原型链上继承到此属性,但是自己本身对象上是不存在的;我们用hasOwnProperty()方法来验证一些:
console.log(cat1.hasOwnProperty('name')); //输出 true
console.log(cat2.hasOwnProperty('kinds')); //输出 false
- isPrototypeOf()方法则是用来判断要检查其原型链的对象是否存在于指定对象实例中:
//Cat.prototype是不是cat1的原型,也就是说Cat.prototype是cat1的原型则为true;简单来讲就是验证cat1是否继承Cat.prototype,该方法可用于前面所讲的验证父类:
console.log(Cat.prototype.isPrototypeOf(cat1)); //输出true;
既然我们在上面提到了Object.prototype上的方法,那我们就在这里最后讲述一下在原型链上如何检索的吧( •̀ ω •́ )✧
在《JavaScript语言精粹》中的第三章中有提到过,获取对象的某个属性值,若该对象没有此属性名,则JS会从其原型对象去获取该属性值。依次类推,直到该过程终点Object.prototype,如果属性完全不属于原型链中,则返回undefined。
在知晓这样一个过程后,我们依然可以用cat1这个实例来验证一下:
console.log(cat1.__proto__);
// 输出:{ eat: [Function (anonymous)], kinds: '田园猫' }
等同于Cat.prototype
console.log(cat1.__proto__.__proto__);
// 输出:[Object: null prototype] {}
等同于Object.prototype
console.log(cat1.__proto__.__proto__.__proto__);
// 输出:null
//从上方三个输出,我们可以得知new出来的实例cat1继承Cat.prototype,所有对象又都继承Object.prototype,而Object.prototype的最终指向null,这也间接证明了在原型链找寻一个属性值,一层一层往上检索,若一直没有则会返回undefined
3.总结
自己写这篇文章的最主要原因是因为最近在读《JavaScript语言精粹》还是上的课都碰到js中的原型,有点被绕晕了,所以自己下定决心来好好整理一些这方面的知识。尤其是这个语言精粹的时候,会发现里面的每一句话在碰到自己上课所遇到的js例子的时候,就会产生一种“原来是这样的!!!”的想法,觉得js精粹这本书真的牛!反复研读会都会收获到不同的理解。
当然了,自己写的这篇文章关于js的原型并不是特别的深入,但还是希望我写的这篇文章能给学js的友友有点帮助,一点点点都是对我最大的鼓励,同时还希望大佬们在看我的这篇文章的时候如果发现有写的不好或错误的地方,可以在评论区帮忙指出,我会积极的去接受并改过来的( •̀ ω •́ )