js进阶系列-原型对象VS对象原型

1,770 阅读12分钟

前言

总所周知,原型在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()都可以创建一个空对象。

  1. 拿到构造函数(详细说明)var Con = [].shift.call(arguments)

这一步具体的原理实现我也是捉摸了好久,想要拿到构造函数为什么不直接arguments.shift()呢?首先arguments是类数组对象,他不具备数组的方法。arguments不是真正数组那么如何使用shift方法呢?所以就用到了call方法让类数组对象可以拥有shift方法。至于其中的原理我目前也还没弄懂,希望有大神解答一下

》 关于shift方法可以看一张图片

图中返回了f[0],在create函数的实参中的第一个值就是构造函数。

  1. 连接到原型obj.proto = Con.prototype

在这里很简单,让对象的属性等于函数的属性拿到原型上的东西

  1. 绑定this,执行构造函数(详细说明)var result = Con.apply(obj, arguments)

所谓的绑定this就是让构造函数的this指向我们刚刚创建的空对象obj,为什么要这么写,首先要明白apply方法的作用,简单来说改变this指向,第一个参数就是this指向,第二个是参数是数组(伪数组)形式,apply方法欢迎看我昨天写的博客。 通过apply将this指向obj,并将arguments传给构造函数Con,用result接收构造函数的结果。这就是这句代码的解释。

  1. 返回新对象return typeof result === 'Object'? result: Object;

返回新对象就是通过new产生的对象,返回的时候需要做一个判断,看构造函数本身是否有一个返回的对象,记住是对象哦(字符串数字等类型是不可以的哦,亲)如果返回的是一个对象那么就优先返回这个对象,如果不是就返回我们创建的obj对象。所以会有一个typeof判断。

五部法弄懂后我自己在编辑器里手动实现了一下发现成功啦,图的左边是代码,右边是结果。crate方法中传递构造函数F2E,打印name属性和原型上的ht方法没有问题。

总结

原型暂时先总结到这里了,当然还是需要多做题多实践看看自己是否掌握。回顾一下这篇涉及的内容。

  • 原型对象prototype是构造函数上的
  • 对象的原型__ proto__是实例化对象上的
  • constructor是原型对象上的属性属性值是构造函数
  • 手动实现new的过程

看文章不易,码字不易,看完了就留下痕(点)迹(赞)再走吧 :)


关于result是undefined这里做一个说明:上面的第四部绑定this执行构造函数中,构造函数执行完将结果赋值给result变量。result是构造函数执行完的结果。但是我上面的例子里构造函数中没有return语句,也就是没有返回一个结果,没有返回一个结果所以result的值是undefined。那么构造函数里面返回一个对象有什么不一样,看看下面的代码。

我们在构造函数中return对象,有一个属性age。在create函数中打印result结果是这个对象。

因为此时返回result对象所以name和ht方法都是undefined。

年龄是result中的属性,所以打印结果20

为了方便大家,我把代码贴在下面。

<script>
	function F2E(name) {
		this.name = name;
		return {age:20}
	}
	F2E.prototype.ht = function () {
		console.log('我会写html结构');
	}
	F2E.prototype.cs = function () {
		console.log('我会写css样式');
	}
	function create() {
		var obj = {};
		var Con = [].shift.call(arguments);
		obj.__proto__ = Con.prototype;
		var result = Con.apply(obj, arguments);
		console.log(result);
		return typeof result == 'object'? result: obj;
	}
  var f2 = create(F2E, 'ahh');
  console.log(f2.name);
  console.log(f2.ht);
  console.log(f2.age);

	// F2E.prototype = {
	// 	ht: function () {
	// 		console.log('我会写html结构');
	// 	},
	// 	cs: function() {
	// 		console.log('我会写css样式');
			
	// 	},
	// 	// constructor:F2E
	// }
	// 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);
	
	</script>

3/24号更新

鸡蛋问题

原型链中的鸡蛋问题一直弄不明白,今天又看了木易杨5-3期的博客。顿时豁然开朗。

还是这张图,看完这张图又有了新的收获,如下:

  1. 不是是所有的对象{}__proto__属性。不是所有的函数function (){}prototype属性,函数既可以有prototype也可以有proto。
  2. Object是一个函数function。

什么是鸡蛋问题:

Object instanceof Function 		// true
Function instanceof Object 		// true

Object instanceof Object 			// true
Function instanceof Function 	// true

我们知道instanceof可以用来判断左值的proto是否在右值的原型链上,我是这么理解的:如果左值是右值的实例对象就返回true。

那么Objec和Function到底谁是谁的实例呢?先有Object还是Function呢?这就是鸡蛋问题。

原因

那么导致鸡蛋问题的原因是:

Function.__proto__   <=> Function.Prototype
Object.__proto__  => Function.Prototype

最主要的原因是Function.__proto__ <=> Function.Prototype。于是争论就变成Function 对象是不是由 Function 构造函数创建的一个实例?存在两种说法。

  • 第一种说法:是的。a 是 b 的实例即 a instanceof b 为 true,默认判断条件就是 b.prototype 在 a 的原型链上。而 Function instanceof Function 为 true,本质上即 Object.getPrototypeOf(Function) === Function.prototype,正符合此定义。
  • 第二种说法:不是。Function 对象不是由 Function 构造函数创建的一个实例。先有Function的原型对象,才有构造函数。
    • Function是build-in内置对象

    • 先有 Function.prototype 然后有的 function Function() ,所以就不存在鸡生蛋蛋生鸡问题了,把 Function.proto 指向 Function.prototype 是为了保证原型链的完整,让 Function 可以获取定义在 Object.prototype 上的方法。