前言
总所周知,原型在javascript中十分重要,今天把原型的知识复习了半天,在此做一个记录。 记录知识点的时候还是顺带着说一下这个知识点的作用。学的知识就是为了利用知识本身去完成某件事情。
花十分钟看完你将学会的知识点如下:
- 原型对象是什么
- 原型的作用
- 原型对象、对象实例、构造函数的关系
- constructor的作用是什么
- __proto __的产生(new一个对象的过程)
原型是什么
MDN官方文档对于js是一种基于原型的语言的解释是(拓展知识可以先不看):
javaScript 常被描述为一种基于原型的语言
(prototype-based
language)——每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链
(prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法。
原型是函数的一个属性,假如有一个构造函数F2E(说明一下这个构造函数命名是不规范的但是这里没有影响),那么这个构造函数上就有一个属性prototype,这个属性的值是一个对象,因此原型也叫作原型对象。
需要注意的是原型不是构造函数的属性,他是所有函数的属性。在创建一个function的时候就自动创建了这个显式属性。
但是有一个例外let fn=Function.prototype这个fn函数是没有prototype属性的,这是为什么呢,因为Function.prototype是引擎创建出来的函数,引擎认为不需要给这个函数对象添加 prototype 属性,不然 Function.prototype.prototype… 将无休无止并且没有存在的意义。
看到这如果你看到这里看不下去了请记住一点,原型是函数的一个属性名为prototype的属性;属性值是一个对象。创建一个函数的时候就创建了这个属性,但有一个例外引擎创建的函数Function.prototype没有这个属性。
原型的作用
原型存在那么肯定有他存在的意义。那原型存在的作用是什么呢?
原型解决实例化对象中的一些不足,具体我们通过代码来说明。
function F2E(name, age) {
this.name = name;
this.age = age;
this.type = '前端';
this.smile= function () { console.log('我笑了');}
}
var f1 = new F2E('玲珑', 19);
var f2 = new F2E('宁静', 20);
上面的代码中创建了两个对象实例,分别是f1、f2。这两个对象有共同的属性type前端和方法smile。两次创建出现相同的类容还好,那么如果出现100个对象呢?创建了100个相同的属性和方法,这样既浪费了空间又不环保效率上也有问题。我们可以亲自实践一下控制台打印f1.type === f2.type。如果不相等那么说明他们是的地址是不同的,开辟了两个空间。
那么有没有办法将这些共有的方法和属性在内存中只生成一次,然后所有实例都指向那个内存地址呢?回答是可以的。
接下来就是我们的原型出场了,我们将共同的内容放到原型对象中去。来看代码。
function F2E(name) {
this.name = name;
}
F2E.prototype.ht = function () {
console.log('我会写html结构');
}
F2E.prototype.cs = function () {
console.log('我会写css样式');
}
var f1 = new F2E('玲珑');
console.log(F2E.prototype);
console.log(f1);
console.log(F2E.prototype.constructor);
console.log(f1.__proto__.constructor);
上面的代码中F2E是一个构造函数,name不是固定的,但是两个方法是所有该构造函数实例化对象所共有的,将他们加入到prototype对象中,原型对象中的所有属性和方法都可以被构造函数的实例化对象继承,也就意味着我们可以把共同的属性和方法直接定义在原型对象上。
构造函数,实例,原型对象三者关系
在前面的代码中我们看到了prototype,还看到了__proto __、constructor。
prototype是原型对象;__proto __是什么对象的原型;constructor construcror主要用于记录该对象引用了哪个构造函数,他可以让原型对象重新指向原来的构造函数。
我们看看上面代码分别输出的结果。console.log(F2E.prototype);console.log(f1);console.log(F2E.prototype.constructor);
console.log(f1.__proto __.constructor);结果依次如下:
接下来讲解构造函数和原型对象和实例化对象之间的关系,弄懂了关系就能看懂结果了,他们的关系如下图。
构造函数中的prototype是一个属性,属性值是一个对象我们叫原型对象,所以通过F2E.prototype可以找到原型对象;原型对象中有一个构造器属性叫constructor,这个属性值是构造函数。好比父亲和孩子,父亲有个厉害的儿子叫原型对象,儿子有个厉害的老爸叫构造函数,他们之间关系的确认就是通过这两个属性。
然后来看看实例化对象和原型对象之间的关系,实例化对象有一个__propo
__属性,这个属性值是prototype,也就是实例化对象的__propo __属性指向了原型对象。
然后再来看实例化对象和构造函数的关系,new
构造函数创建了一个实例化对象,new的过程放到后面去了解。同时实例化对象中也有一个构造器属性,指向了构造函数,在上面的图中表示这段关系的线是橘(特)色(殊)的,我是想表达实例化对象的构造器属性并不是直接指向构造函数的,而是因为实例化对象能够找到原型对象而原型对象上的构造器属性指向构造了函数,所以实例化对象也可以找到构造函数。
总之,他们之间的关系是一个三角形的关系。
构造器的作用
构造器是原型对象下面的属性,通过构造器能够找到他爹也就是构造函数。那么如何体现出构造器的价值呢,我们看代码。
function F2E(name) {
this.name = name;
}
// F2E.prototype.ht = function () {
// console.log('我会写html结构');
// }
// F2E.prototype.cs = function () {
// console.log('我会写css样式');
// }
F2E.prototype = {
ht: function () {
console.log('我会写html结构');
},
cs: function() {
console.log('我会写css样式');
}
}
var f1 = new F2E('玲珑')
// console.log(F2E);
// console.log(f1);
console.log(F2E.prototype);
console.log(f1);
console.log(F2E.prototype.constructor);
console.log(f1.__proto__.constructor);
上面的代码中我们创建了一个原型对象,而不是直接用引擎为我们创建的原型对象,这个时候就存在构造器属性但是属性值不是原来的构造函数,因为我们重新创建了一个原型对象,没有创建原型对象的时候是在原来的原型对象上添加方法所以可以看到constructor的值是构造函数。
这里我们改变原型对象后在控制台输出的结果是function Object(){native code},他的构造函数是ObjectObject是最顶层的函数,native code表示本地代码。
如何让构造器属性指向原来的构造函数呢。
我们需要在原型对象中加上构造器属性,代码结果如下,主要是红色框框的部分。
总结一下作用就是当我们修改了原型对象后能够通过构造器属性找到原来的构造函数,儿子能够找到爸爸。
new一个对象的内部操作
接下来将最难理解的知识点,new一个对象的过程,下图是我第一眼看到new过程的第一反应。我太弱了。
new一个对象的时候在对象内部产生隐式属性__proto __
__proto __这是每个对象都有的隐式原型属性,指向了创建该对象的构造函数的原型。
实例对象的 proto 如何产生的当我们使用 new 操作符时,生成的实例对象拥有了 _proto_属性。那么new一个对象到底有哪些过程呢,红宝书中是四步,但是我觉得网上的五部比较完善。五部法可能很多人一眼就能看明白,但是我这个小白琢磨了好一会才弄清原理。
如果下面的案例看明白了就不用看我后面的解释啦,毕竟自己思考印象更加深刻。
function create(){
1. 创建一个空对象
var obj = {}
2. 拿到构造函数
var Con = [].shift.call(arguments);
3. 连接到原型
obj.__proto__ = Con.prototype;
4. 绑定this,执行构造函数
var result = Con.apply(obj, arguments);
5. 返回新对象
return typeof result === 'Object'? result: Object;
}
function Car(color) {
this.color = color;
}
Car.prototype.start = function() {
console.log(this.color + " car start");
}
var car = create(Car, "black");
car.color;
// black
car.start();
// black car start
如果不是很明白就看看下面的解释吧。
1.创建一个空对象
这里不用过多说明,{}或者new Object()都可以创建一个空对象。
2.拿到构造函数(详细说明)var Con = [].shift.call(arguments)
这一步具体的原理实现我也是捉摸了好久,想要拿到构造函数为什么不直接arguments.shift()呢?首先arguments是类数组对象,他不具备数组的方法。arguments不是真正数组那么如何使用shift方法呢?所以就用到了call方法让类数组对象可以拥有shift方法。至于其中的原理我目前也还没弄懂,希望有大神解答一下
》 关于shift方法可以看一张图片
图中返回了f[0],在create函数的实参中的第一个值就是构造函数。
3.连接到原型obj.proto = Con.prototype
在这里很简单,让对象的属性等于函数的属性拿到原型上的东西
4.绑定this,执行构造函数(详细说明)var result = Con.apply(obj, arguments)
所谓的绑定this就是让构造函数的this指向我们刚刚创建的空对象obj,为什么要这么写,首先要明白apply方法的作用,简单来说改变this指向,第一个参数就是this指向,第二个是参数是数组(伪数组)形式,apply方法欢迎看我昨天写的博客。
通过apply将this指向obj,并将arguments传给构造函数Con,用result接收构造函数的结果。这就是这句代码的解释。
5.返回新对象return typeof result === ‘Object’? result: Object;
返回新对象就是通过new产生的对象,返回的时候需要做一个判断,看构造函数本身是否有一个返回的对象,记住是对象哦(字符串数字等类型是不可以的哦,亲)如果返回的是一个对象那么就优先返回这个对象,如果不是就返回我们创建的obj对象。所以会有一个typeof判断。
五部法弄懂后我自己在编辑器里手动实现了一下发现成功啦,图的左边是代码,右边是结果。crate方法中传递构造函数F2E,打印name属性和原型上的ht方法没有问题。
总结
原型暂时先总结到这里了,当然还是需要多做题多实践看看自己是否掌握。回顾一下这篇涉及的内容。
- 原型对象prototype是构造函数上的
- 对象的原型__ proto__是实例化对象上的
- constructor是原型对象上的属性属性值是构造函数
- 手动实现new的过程
看文章不易,码字不易,看完了就留下痕(点)迹(赞)再走吧 😃
以上内容都是我自己的一些感想,分享出来欢迎大家指正,顺便求一波关注,里面的资料各位小伙伴关注我后私信【Java】就可以免费领取~