不求甚解 - - liao一下prototype
如果你爱我,就干了这碗热热的毒鸡汤!
在父母的期望面前我们不敢说不行,我们总是用行动告诉他们我们是真的不行。欧耶!
关于prototype,怎么说呢,以前的前端开发是经常用的,但是最近忽然发现,好像很久都没用过这个属性了。因为现在封装好的主流框架和插件很多,用着方便,底层的东西都不怎么用了,也用不太到了。
最近自己在开发一款插件,突然发现这个东西我好像忘的差不多了。我的良心受到了深深的谴责,我怎么可能是这么不求甚解的人呢?(来自灵魂的拷问:难道不是吗?)所以写点东西,记录一下。哼哼~
什么是prototype?
prototype [ˈprəʊtətaɪp] : 原型
大家知道JavaScript是面向对象的,还有很多语言也是面向对象的,比如C++,Java等,但是JavaScript厉害了,官方定义:在JavaScript中一切皆为对象:字符串、数字、数组、日期,等等。(这么多对象,单身狗是不是很心动?咳咳)
话说JavaScript的面向对象和其它的语言的一样吗?首先,两者间面向对象的原理肯定是一样的。但是呢,又存在着一些细微的差别,在我的感觉上呢:一个是恪守成规的书香门第,一个是随遇而安的江湖浪子。毕竟当时Brendan Eich开发JavaScript的时候也是比较随性的,怎么简单好用怎么来,也方便了现在很多的初学者。拿Java和JavaScript举例吧,毕竟JavaScript是在Java盛行时的大环境下开发出来的:
在Java的面向对象中,类和实例是不同的实体,但是JavaScript中,一切皆为对象。
在Java的面向对象中,类不能当做函数去运行,但是JavaScript可以。
在Java的面向对象中,有丰富的继承机制,但是JavaScript仅通过原型链继承。
在Java的面向对象中,良好的成员作用域支持(private,protected,public等),但是JavaScript中全继承。
还有很多哈,在这就不一一列举了(下面会有,感兴趣的去看),JavaScript和Java一样是面向对象,但是JavaScript比Java的面向对象更加的灵活,方便。
所以JS中对象的定义: 拥有属性和方法的数据。
定义的非常的简单大气:一个对象,就是一个独立的个体,比如定义一个字符串,它很独立的,它有自己的长度属性:str.length;还有它的方法,比如subString(),indexOf() 等等,所以它就是一个字符串对象。
跑题了?没有没有,因为在JavaScript中,prototype对象是实现面向对象的一个重要机制。上面埋了伏笔,JavaScript是通过原型链来实现继承的。
先看一下prototype的官方定义: prototype 属性使您有能力向对象添加属性和方法。
从这两个定义中是不是看出了点什么,没错,prototype就是为了实现面向对象的继承被弄出来的。因为Brendan Eich大神不想讲Java上那套完整的继承机制全搬到JavaScript上,所以他决定为构造函数设置一个prototype属性,指向一个原型对象,而所有的对象都会继承原型对象的属性和方法。
所以原型链继承方式就这么形成了:
- 创建构造函数
- 通过new创建构造函数的实例,即对象
- 通过构造函数自带的prototype对象,即原型对象,来修改对象的属性和方法
举个栗子:
function beauty () { // 构造函数
this.skin = 'white';
}
let my = new beauty(); // 实例化对象
let your = new beauty()
console.log(my.skin); // 继承原生属性
beauty.beautiful = 'yes'; // 直接为构造函数添加属性
console.log(my.beautiful); // 没继承到
beauty.prototype.realBeautiful = 'yes'; // 通过原型对象给构造函数添加属性
console.log(my.realBeautiful); // 继承到了
console.log(your.realBeautiful); // 其它实例也继承到共享属性
执行结果:
总结一下:
prototype是构造函数创建完成时自动生成的一个prototype 属性。该属性是个指针,指向了一个对象,这个对象我们称之为原型对象。我们可以在任意时刻和位置,通过修改该原型对象的属性或方法,为实例添加共享属性或方法。
用白话说,就是你的项目中大量的需要一个共享属性的时候,用prototype就对了。
再举个栗子,比如说我的项目中每个日期实例都需要一个返回去年年份的函数。可以这么做:
Date.prototype.getLastYear = () => {
let now = new Date();
return now.getFullYear() - 1;
};
let d = new Date();
console.log(d.getLastYear());
结果:
就是随便举个栗子,大概意思就是这么回事,理解了就好。
但是一般我们开发的过程中是不会这么用的,毕竟改了一些公共的类会导致很多问题的,在实际开发过程中,我们一般都这么搞:
function myDate () {
}
myDate.prototype = new Date(); // 用一个新的构造函数去封装我们要继承的类
myDate.prototype.getLastYear = () => {
let now = new Date();
return now.getFullYear() - 1;
};
let d = new myDate(); // 创建这个新的构造函数的实例,继承原型对象的方法
console.log(d.getLastYear());
创建一个新的构造函数,原型指向我们需要的类,这样我们给构造函数原型添加公共属性时就很安全,方便了。(实现这种继承有很多方式,下一篇会写。)
prototype、constructor、__proto__
了解了原型,再了解一下原型链吧。
JavaScript 对象是动态的属性“包”(指其自己的属性)。JavaScript 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。
比如我写一个继承关系:
function beauty () { // 构造函数
this.skin ='white';
this.stature = '170';
}
beauty.prototype.realBeautiful = 'yes';
let my = new beauty(); // 实例化对象
console.log(beauty.prototype);
console.log(my);
看控制台上会怎么显示:
可以发现,原型对象里面,除了我们设置的两个属性外,还有两个属性(constructor,__proto__)。
而继承的对象上也多出了一个属性(__proto__)。
接下来,我们先看一下constructor属性吧:
诶,很神奇哦,它也有prototype属性,而且跟外层我们打出来啊的prototype属性一毛一样,如果再打开里边的prototype的constructor,就是个死循环了。
所以我们发现了beauty.prototype.constructor指向的是beauty函数,即该原型的所属构造函数。
再接再厉,打开__proto__属性,看里面有什么:
不难发现,原来__proto__指向的是该对象的原型对象。即my.__proto__ === beauty.prototype
beauty.__proto__ === Object.prototype。
也就明白了,为啥对象都可以使用上图中的hasOwnProperty,toString之类的方法了。
了解了这几个属性后,我们再看下属性到底是怎么继承的:
还是这个例子:
function beauty () { // 构造函数
this.skin ='white';
this.stature = '170';
}
beauty.prototype.skin = 'green';
beauty.prototype.realBeautiful = 'yes'; // 通过远行对象给构造函数添加属性
// 不要在beauty函数的原型上直接定义beauty.prototype = {realBeautiful:'yes'};这样会直接打破原型链
let my = new beauty(); // 实例化对象
my.stature = '160';
console.log(my.skin);
console.log(my.realBeautiful);
console.log(my.stature);
console.log(my.face);
执行结果:
从执行结果可以看到:
对于JS对象的属性继承,是存在一个顺序的:
- 首先,自身的属性:像上例中的 my.stature,my.skin,自身属性的值是可以任意修改的。(虽然beauty的原型中也设置了skin属性,但是还没到查找原型属性的那一步,这就是传说中的“属性遮蔽”)
- 其次,原型的属性:像上例中的 my.realBeautiful,自身的属性中没有找到,会去原型中(my.__proto__中)找。
- 最后,原型的原型的属性:比如上例中的my.face,自身的属性中没有,去原型中找,发现原型中(my.__proto__中,即beauty.prototype中)也没有,那就去原型的原型中找(my.__proto__.__proto__中,即Object.prototype中),知道返回的prototype值为null为止,返回属性值为undefined。
这种搜索的轨迹,形似一条长链,又因prototype在这个规则中充当链接的作用,于是我们把这种实例与原型的链条称作 原型链。
到此,JS的继承关系,所谓原型链,以及prototype,、__proto__,constructor之间的关系,应该都比较清楚了。
拓展(有兴趣的看)
1、Java和JavaScript面向对象的区别
该表格摘抄自: https://www.iteye.com/blog/jobar-2015021
Java | JavaScript |
---|---|
静态类型 | 动态类型 |
自定义类型可以是类,接口或枚举定义 | 自定义类型由函数或原型定义 |
类型不可在运行时改变 | 类型可在运行时改变 |
定义变量需要声明具体类型(强类型) | 定义变量不需要声明具体类型(弱类型) |
构造器是具体的方法 | 构造器只是一个函数,构造器与函数之间无区别 |
类和实例是不同的实体 | 一切均为对象,构造器函数和原型也是对象 |
支持静态和实例成员 | 不直接支持静态和实例成员 |
由抽象类和接口支持抽象类型 | 不直接支持抽象类型 |
良好的成员作用域支持(private, package, protected,public) | 仅支持public的成员 |
丰富的继承机制 | 仅通过原型继承机制 |
支持方法重载和方法重写 | 不直接支持方法重载和方法重写 |
丰富的反射机制 | 有反射特性 |
由包来支持模块化 | 无直接的包或模块化支持 |
2、关于为啥new一个对象的时候,会被自动添加一个__proto__ 属性呢?
let my = new beauty();
当你在执行这样的语句时,JavaScript实际执行的语句是:
var my = new Object();
my.__proto__ = beauty.prototype;
beauty.call(my);
3、如何判断某个对象是否为数组。(说白了就是判断继承关系)
有两种方法可以确定实例和构造函数之间的关系: instanceof ,isPrototypeOf
①、使用 instanceof 方法,判断某实例是否为某构造函数的实例。(凡是在二者的原型链中出现过的构造函数,都会返回true)
let arr = [1, 2, 3];
console.log(arr instanceof Array); // 返回true
②、使用 isPrototypeOf 方法,判断某原型是否为某实例原型链中出现过的。(凡是在原型链中出现过的原型,都会返回true)
let arr = [1, 2, 3];
console.log(Array.prototype.isPrototypeOf(arr)); // 返回true
4、我在测试第3题的过程中,发现个现象,觉得也要提一下。
看个例子,我定义了三个字符串。
let str = 'happy';
let str1 = String('happy');
let str2 = new String('happy');
console.log('str:', str);
console.log('str1:', str);
console.log('str2:', str);
console.log(str instanceof String);
console.log(str1 instanceof String);
console.log(str2 instanceof String);
执行结果:
于是我又这么干了一下:
let str = 'happy';
let str1 = String('happy');
let str2 = new String('happy');
console.log('str:', typeof str);
console.log('str1:', typeof str1);
console.log('str2:', typeof str2);
结果是这样的:
发现只有第三种定义方式才是对象,不是说JavaScript中一切皆为对象吗?所以继续看:
console.log(str.__proto__);
结果:
又确实是个对象,而且和str2.__proto__完全相同。
脑子感觉又乱了哈,没事,从头捋一捋。
首先呢,看一下JS的数据类型有哪些?
值类型(基本类型):字符串(String)、数字(Number)、布尔(Boolean)、对空(Null)、未定义(Undefined)、Symbol。
注:Symbol 是 ES6 引入了一种新的原始数据类型,表示独一无二的值。
引用数据类型:对象(Object)、数组(Array)、函数(Function)。
那么上边所谓JS的一切皆为对象的话,到底对不对呢?
来细细的扒一下。
我们在上边文章开始的位置提过JS对对象的定义:拥有属性和方法的数据。
那么基本类型是否拥有属性和方法?
很显然,有!
比如这些我们经常会用到属性:
let str = '123'; // 定义一个基本类型的字符串
console.log(str.length); // 字符串长度属性
console.log(str.substring(1,2)); // 截取字符串的方法
那它就是对象,没跑了。但是基本类型的对象,和我们上面说的Object对象还是不大一样的。
比如字符串,数值还有布尔型的数据。他们拥有对应的类String,Number,Boolean所拥有的的全部属性和方法。但是又跟new出来的String,Number,Boolean类的对象不一样:基本类型的对象无法修改或增加属性,但是new出来的可以。
举例:
let str = 'happy';
let str2 = new String('happy');
str.shuxing = '1alala';
str2.shuxing = '1alala';
console.log(str.shuxing);
console.log(str2.shuxing);
结果:
所以可以从这个方向理解,就是字符型,数值型以及布尔型,不是String类,Number类,以及Boolean类直接创建的对象,但是却继承了对应的类的所有的属性和方法。
为啥? JS就是这么规定的。
简单的说,就是需要自定义一些属性或方法的时候,可以new,不需要的话,基本类型足矣。
如果对堆栈比较了解的同学们,估计这么说会了解的更透彻:
String、Number、Boolean基本类型是存储在栈(stack)内存中的,数据大小确定,内存空间大小可以分配。而引用类型是存储在堆(heap)内存中的,例如对象。栈中存在的仅仅是一个堆的指针。
较真的同学又问了:那null和undefined呢?也是对象?
null 类型概念上说是一个空对象,即是一个不存在的对象的占位符,或者说是指向空对象的一个指针。使用typeof运算得到 “object”,所以说它是一个特殊的对象不为过。
undefined是从null派生出来的,即当声明的变量还未被初始化时,变量的默认值为undefined。
我个人理解大概就是:两个变量,undefined表示未初始化,null表示初始化为空对象。
官方的意思是希望undefined表示出错了,忘记赋值的意思,而null表示一种空对象的正常现象。意会意会!
差不多了,我已经到极限了。
来,跟我一起喊:在JS中一切皆为对象!