理解Javascript的原型和原型链

1、原型是什么东西?

说到原型,那么我们就要来理解一下Javascript的function类型,我们知道,javascript中是没有类这个概念的,它是一门函数式编程的语言。类有一个很重要的特性,就是它可以根据它的构造函数来创建以它为模板的对象。

在javascript中,函数就有2个功能:

  • 第一、 作为一般函数调用
  • 第二、 作为它原型对象的构造函数 也就new()

还是很模糊,那么我们直接来看创建一个函数的过程都做了什么吧


function person(){
		this.name = 'a';
	}
  • 1、会创建一个函数对象,也就是person本身。
  • 2、会创建一个a的原型对象,称为prototype
  • 3、函数对象会有一个prototype指针,它指向了对应的原型对象,这里就指向了prototype
  • 4、prototype对象中有一个construtor指针,指向它的构造函数,这里就指向了person

在这里插入图片描述
原型,在我们生成一个函数之后,浏览器会按照一定的规则为我们创建一个对象,这个对象就是原型对象,这个对象实际上是储存在了内存中,而在这个函数中会有一个属性prototype,这个属性就指向浏览器生成的原型对象。

2、什么是构造函数

构造函数,先来说一下为什么会有构造函数,假设我们要用一些对象代表一个班级的同学,那么我们需要这样写:

var p1 = { name: 'zs', age: 6, gender: '男', hobby: 'basketball' };
var p2 = { name: 'ls', age: 6, gender: '女', hobby: 'dancing' };
var p3 = { name: 'ww', age: 6, gender: '女', hobby: 'singing' };
var p4 = { name: 'zl', age: 6, gender: '男', hobby: 'football' };
// ...

在每个对象中我们都需要写上name、age、gender…等等信息,这样写代码就十分冗余,因此我们就可以用构造函数的方式,写一个函数,我们只需要按顺序传入值,就可以拿到我们想要的对象:

function Person(name, gender, hobby) {
    this.name = name;
    this.gender = gender;
    this.hobby = hobby;
    this.age = 6;
}

构造函数的使用,需要配合一个关键字new,我们看看现在我们创建对象需要怎么做吧:

var p1 = new Person('zs', '男', 'basketball');
var p2 = new Person('ls', '女', 'dancing');
var p3 = new Person('ww', '女', 'singing');
var p4 = new Person('zl', '男', 'football');
// ...

虽然从上面的例子上看,并不觉得会减少代码量,但是一旦有成千上百个对象需要创建的时候,这样写就会更加简便。
在使用对象字面量创建一系列同一类型的对象时,这些对象可能具有一些相似的特征(属性)和行为(方法),此时会产生很多重复的代码,而使用构造函数就可以实现代码复用。
我相信此时一定有小伙伴有疑问了,为什么通过一个new关键字就能做到构造函数呢?new关键字做了哪些事呢?

其实当一个函数创建好以后,我们并不知道它是不是构造函数,即使像上面的例子一样,函数名为大写,我们也不能确定。只有当一个函数以 new 关键字来调用的时候,我们才能说它是一个构造函数。
构造器执行过程:

  • 当new 关键字调用时,会创建一个新的内存空间,标记为 Person的实例
  • 函数体内部的 this 指向该内存
  • 通过以上两步,我们就可以得出这样的结论。
var p2 = new Person('ls', '女', 'dancing');  // 创建一个新的内存 #f2
var p3 = new Person('ww', '女', 'singing');  // 创建一个新的内存 #f3
// ...

每当创建一个实例的时候,就会创建一个新的内存空间(#f2, #f3),创建 #f2 的时候,函数体内部的 this 指向 #f2, 创建 #f3 的时候,函数体内部的 this 指向 #f3。

  • 执行函数体内的代码:通过上面的讲解,你就可以知道,给 this 添加属性,就相当于给实例添加属性。
  • 默认返回 this :由于函数体内部的this指向新创建的内存空间,默认返回 this ,就相当于默认返回了该内存空间,也就是上图中的 #f1。此时,#f1的内存空间被变量p1所接受。也就是说 p1 这个变量,保存的内存地址就是 #f1,同时被标记为 Person 的实例。

提到了返回值,那么我们也会好奇,构造函数是否可以有返回值?答案是肯定的:

  • 当没有返回值得时候,构造函数返回默认的this:
function Person(name) {
    this.name = name;
}
var p1 = new Person('ls');  // 创建一个新的内存 #f2

在上述执行流程中,执行到new关键字,此时会在内存中开辟一个空间#f2,并标记为Persion的实例,然后函数内部的this就指向了#f2,之后执行函数中的代码,由于函数体内部的this 指向该内存空间,而该内存空间又被变量 p1 所接收,所以 p1 中就会有一个 name 属性,属性值为 ‘ls’

console.log(p1)//{name:'ls'}
  • 如果我们返回一个基本类型,对结果不会产生影响。
function Person(name) {
    this.name = name;
    return 20
}
var p1 = new Person('ls'); 
console.log(p1)//{name:'ls'}
  • 如果返回一个对象类型,那么此时p1就是返回的这个对象
function Person(name) {
    this.name = name;
    return {name:'张三',age:'15'}
}
var p1 = new Person('ls'); 
console.log(p1)//{name:'张三',age:'15'}

3、原型是用来干嘛的?解决了什么问题?

看到这里,肯定有人会有疑问,不是说原型的吗?为什么要提到构造函数呢?不急,当我们理解了构造函数之后,我们就可以来看看原型能用来干嘛了。
这里我们创建一个Person构造函数,它有一个属性name,一个方法callName,然后我们创建两个对象p1和p2

    function Person (name) {
	    	// person
			this.name = name
            this.callName=function(){
                console.log(this.name);
            }
	    }
	    let p1 = new Person('张三')
		let p2 = new Person('李四')
		console.log(p1);
        console.log(p2);

在这里插入图片描述
我们可以看到这两个对象中都有callName这个函数,也就是说,虽然这个方法是同名的,但是在内存中却占用了两片空间,我们来验证一下

    function Person (name) {
	    	// person
			this.name = name
            this.callName=function(){
                console.log(this.name);
            }
	    }
	    let p1 = new Person('张三')
		let p2 = new Person('李四')
		console.log(p1);
        console.log(p2);
        console.log(p1.callName==p2.callName);

在这里插入图片描述
p1.callName==p2.callName的结果是false,不难看出,这两个对象中的callName不是同一个。
而我们使用原型就可以做到,让所有的实例共享一个方法。只需要将构造函数的原型中添加了这个方法就可以了。

 function Person (name) {
	    	// person
			this.name = name
            // this.callName=function(){
            //     console.log(this.name);
            // }
	    }
        Person.prototype.callName=function(){
            console.log(this.name)
        }

		let p1 = new Person('张三')
		let p2 = new Person('李四')
        console.log(p1);
        console.log(p2);
        console.log(p1.callName==p2.callName);

在这里插入图片描述
看到这里可能有人还会问了:哎,你这两个对象中没有callName这个方法啊?这个时候,就要引出另一个概念了,原型链

4、原型链

我们在刚才第一节的时候说过,当我们创建一个函数的时候,浏览器就会为这个函数创建一个原型对象,那么原型对象是否也有原型对象呢?答案是肯定的。

  <script type="text/javascript">
	    function Person (name) {
	    	// person
			this.name = name
            // this.callName=function(){
            //     console.log(this.name);
            // }
	    }
        Person.prototype.callName=function(){
            console.log(this.name)
        }

		let p1 = new Person('张三')
		let p2 = new Person('李四')
        console.log(p1);
        console.log(p2);
        console.log(p1.callName==p2.callName);
        console.log(Person.prototype);
        console.log(Person.prototype.__proto__);
        console.log(Person.prototype.__proto__.__proto__);
        console.log(Person.prototype.__proto__.__proto__.__proto__);

在这里插入图片描述

不难看出,原型对象也是有原型的,看打印出来的对象的contructor的类型可以知道,原型对象的原型就是object,同样object的原型对象是null,而再去找null的原型对象,就是null了,但是当我们试图去拿null的原型的时候就会报错。这种向链条一样的关系,就称为原型链,而原型链的最高层就是null。
在这里插入图片描述

通过画图简单把逻辑搞清楚。

  • 首先说好了的每一个实例都有__proto__,而这个就指向的是构造函数的对象原型,所以P1.__proto__和Person.prototype是等价的,即P1.proto是怎么指向Person的构造函数呢?本质上就是通过Star.prototype指回去的。
  • 那么我们就会想到Star.prototype的__proto__指向的会是谁呢,一打印就能看的出指向的是Object.prototype,所以不用想也知道他的构造函数肯定是Object了
  • 那Object.prototype还有没有__proto__,打印一看就知道是为null,所以我们知道原型的终点就是null

每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么假如我们让原型对象等于另一个类型的实例,结果会怎样?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立。如此层层递进,就构成了实例与原型的链条。这就是所谓的原型链的基本概念。
有点绕哈,不过没关系,概念性的东西,我们知道个大概就可以,这要想真明白只能靠自己代码中去找他们的关系。现在我们去理解另一个概念,原型链的成员查找机制
还是先通过概念去理解:
当访问 p 中的一个非自有属性的时候,就会通过 proto 作为桥梁连接起来的一系列原型、原型的原型、原型的原型的原型直到 Object 构造函数为止。
然后我们进行实践:
在这里插入图片描述
我们看到object中有一些方法:tostring、valueof等等,按照原理所说,那么我们创建一个对象,是可以直接调用tostring方法的。

	    function Person (name) {
			this.name = name
            this.age=10
	    }
        Person.prototype.callName=function(){
            console.log(this.name)
        }
		let p1 = new Person('张三')
        console.log(typeof p1.age);
        console.log(typeof p1.age.toString());

在这里插入图片描述
结果如上,没有报错,并且成功调用tostring方法将一个number类型的数据转换成了string类型。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值