傲娇大少之---【JS的原型,prototype、__proto__、constructor】

不求甚解 - - 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

JavaJavaScript
静态类型动态类型
自定义类型可以是类,接口或枚举定义自定义类型由函数或原型定义
类型不可在运行时改变类型可在运行时改变
定义变量需要声明具体类型(强类型)定义变量不需要声明具体类型(弱类型)
构造器是具体的方法构造器只是一个函数,构造器与函数之间无区别
类和实例是不同的实体一切均为对象,构造器函数和原型也是对象
支持静态和实例成员不直接支持静态和实例成员
由抽象类和接口支持抽象类型不直接支持抽象类型
良好的成员作用域支持(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、如何判断某个对象是否为数组。(说白了就是判断继承关系)

有两种方法可以确定实例和构造函数之间的关系: instanceofisPrototypeOf
①、使用 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中一切皆为对象!

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值