什么是原型原型链
前言
大家好,我是安步尘,欢迎关注我的公众号,有时间也可以逛下我的博客 这篇文章是面试经常会被问的问题,同时也是比较难理解的知识,所以总结下我理解的
先简单介绍一下: 每个函数都有一个prototype原型(原型就是对象),原型对象有一个constructor属性,指向的是构造函数,访问对象的某一个属性或者方法时,会从对象自身查找,如果查找不到,就会去原型链上去找,原型的最终目的就是让所有的实例能够共享其属性和方法(很重要)。
我们先用代码来看下
function People() { };
const people = new People();
People.prototype.name = "xiaoxu";//在构造函数上添加原型属性
我们打印原型看看是什么鬼
console.log(People.prototype)
我们看到了原型就是一个对象,没错,就是一个对象
结合上面的简单介绍,那么问题来了,上面提到构造函数、实例对象、constructor属性、,并且说了每一个函数都有原型,这些东西又是什么?了解原型原型链必须要先了解下这些知识,这样后面理解起来会更容易
,我们一个一个来看
- 什么是构造函数?
- 构造函数和普通函数有什么区别?
- 什么是实例对象?
- constructor又是什么?
- 什么是原型原型链?
- 为什么会有原型原型链?
- 最后总结
什么是构造函数
构造函数是用来创建新的实例对象的 使用new关键字来创建的 那么new的过程执行了什么呢? 这个问题也是一道面试题哦
new关键字执行的过程
- 创建一个新对象
- 将实例对象的__proto__隐式原型指向了People构造函数的原型
- 让构造函数中的this指向我们的实例对象
- 判断构造函数中有没有返回值,如果显示的return了一个对象,则返回该对象,如果没有返回值或者返回的不是一个对象,那么就返回一个新创建的对象,也就是会生成一个全新的对象
我们先创建一个构造函数
function People() {
this.name = 'xiaoxu';//自身属性
this.age = 28;//自身属性
// return { address: '江西上饶' }; //如果返回的是对象,那么下面的people实例对象为 {address: "江西上饶"} ,上面定义的name和age则不会包含在里面
// return 20; 如果返回的是基础数据类型,则不会返回该数据,会创建一个全新的对象
}
var people = new People();
console.log(people);//{name: "xiaoxu", age: 28}
我们看上面代码中间的两行注释的return语句,再看上面new关键字执行的过程中的第4条就明白了
构造函数和普通函数有什么区别
区别如下
- 构造函数的this指向实例本身
- 普通函数的this指向调用者
- 构造函数需使用new关键字来创建新的实例对象,普通函数不需要
- 构造函数首字母要大写 如 var a = new Object() , var a = new Array() 都是首字母大写
我们来看个例子:
function People(name, age) {
this.name = name;
this.age = age;
console.log(this);
}
然后执行构造函数和普通函数,看下它们各自打印的this是什么
// 执行构造函数
var people = new People('xiaoxu', 28);
// 执行普通函数
People('xiaoxu', 28);
打印结果如下图:
构造函数:如果使用构造函数方式调用则this指向当前的实例对象 普通函数:调用后this指向调用者,这里指向的是window
什么是实例对象
- 创建实例对象必须要使用new关键字
- 我们使用new关键字来创建的构造函数,构造函数返回的对象就是实例对象
- 我们可以通过instanceof来检测这个对象是不是构造函数的实例
function People(name, age) {
this.name = name;
this.age = age;
}
var people = new People();
console.log(people instanceof People);//true
说明people是People构造函数的实例对象
constructor是什么?它有什么作用
原型对象prototype有一个constructor属性,默认指向原型对象prototype所在的构造函数,但是它容易被更改, 它主要是指向当前对象的构造函数,我们也可以用来判断数据类型,重新指向某个构造函数
function People() { };
var people = new People();
console.log(people);
打印结果如下图:
我们看到实例对象中的constructor指向的是People构造函数,我们来确认下
console.log(people.constructor === People);//true 可以看到是指向了People构造函数
也可以用constructor判断数据类型
var string = new String('这是字符串');
var arr = new Array(1, 2, 3);
console.log(string.__proto__.constructor === String);//true
console.log(arr.__proto__.constructor === Array);//true
var A = null;
var B = undefined;
console.log('A.__proto__', A.__proto__);//Uncaught TypeError: Cannot read properties of null (reading '__proto__')
console.log('B.__proto__', B.__proto__);//Uncaught TypeError: Cannot read properties of undefined (reading '__proto__')
console.log(A.__proto__.constructor === null);//Uncaught TypeError: Cannot read properties of null (reading '__proto__')
console.log(B.__proto__.constructor === undefined);//Uncaught TypeError: Cannot read properties of undefined (reading '__proto__')
可以看到null和undefined是没有隐式原型__proto__的,所以也没有constructor
那么constructor如何会被更改的呢?我们再来看一个例子 先定义一个函数
function People() { };
然后修改该函数的原型
People.prototype = {
name: 'xiaoxu',
handleEvent: function () {
console.log('这是处理事件方法');
}
}
var people = new People();
console.log(people.constructor);//打印为:ƒ Object() { [native code] } 这是Object构造函数
console.log(people.constructor === People);//false 因为构造函数指向了Object
注意
:
这里constructor属性没写,然后打印的结果是constructor指向了Object,并且people.constructor === People返回的是false
那么现在我们指定constructor指向构造函数
People.prototype = {
constructor: People,
name: 'xiaoxu',
handleEvent: function () {
console.log('这是处理事件方法');
}
}
var people = new People();
console.log(people.constructor);//打印:People构造函数
console.log(people.constructor === People);//true 是指向当前对象的构造函数
然后people.constructor就指向了People构造函数,并且people.constructor === People返回的也是true
所以如果我们更改原型对象,里面的constructor一定要重新指向对象的构造函数,否则就会指向最顶层的js内置的Object构造函数
什么是原型原型链
function People() {
this.name = "xiaoxu"
};
var people = new People();
People.prototype.address = "江西上饶";
People.prototype.handleEvent = () => {
console.log('这是处理事件方法');
};
console.log('实例对象', people);
原型指的就是prototype,它是一个对象,我们在上面定义的属性和方法就是在原型上定义的
,我们看上面图片,它最开始只有自身的name属性,展开原型后,就看到了在原型上定义的address属性和handleEvent方法了,这就是原型
我们再来看下能不能获取原型上定义的address属性和handleEvent方法
console.log(people.address);//江西上饶
people.handleEvent();//这是处理事件方法
打印结果中我们看到确实是获取到了原型上定义的属性和方法,那是怎么获取的呢?
我们再来看下如下代码
console.log(people.__proto__.address);//江西上饶
people.__proto__.handleEvent();//这是处理事件方法
竟然同样能获取到
这里就要说下每一个对象都有一个隐式原型
,指的就是__proto__,也就是通过它往上查找原型上的属性和方法,如果还没有那么会继续向上查找,直到找到最顶层的Object对象,它的__proto__是Null,这样一层一层通过__proto__向上查找的过程就是原型链
我们再来看个例子就更清楚了
function People() { };
People.prototype.name = "xiaoxu"
var people = new People();
console.log('构造函数的原型', People.prototype);
console.log('实例对象的隐式原型', people.__proto__);
console.log('判断实例对象的constructor是否指向了构造函数', people.constructor === People);//true
console.log('判断实例对象的隐式原型是否指向我们的构造函数原型', people.__proto__ === People.prototype);//true
// 下面我们沿着People构造函数继续向上找
console.log('这是最顶层的Object对象的原型', people.__proto__.__proto__);
// 我们继续向上找Object对象的原型链 打印出来是null
console.log('这是最顶层的Object对象的原型链', people.__proto__.__proto__.__proto__);//null
接下来我们看下我们熟悉的例子
var arr = new Array(1, 2, 3, 4, 5, 6);
console.log(arr);
展开了原型后是不是看着很熟悉,这里就是我们熟悉的数组方法,当你在使用数组方法时,其实已经通过原型链去找这些方法了
还有其他的内置构造函数,一起测试看下
var a = new Array();//数组
var f = new Function();//函数
var d = new Date();//日期
var r = new RegExp();//正则
下面使用console.dir方法打印出构造函数上原型的所有的属性和方法
console.dir(Array.prototype);
console.dir(Function.prototype);
console.dir(Date.prototype);
console.dir(RegExp.prototype);
可以看到上面图片上都有原型上定义的方法,都是通过原型链去拿到的,它们的原型上面封装了自己的方法
我们来看下是不是指向Object构造函数
console.dir(a.__proto__.__proto__.constructor);// Object {}
console.dir(f.__proto__.__proto__.constructor);// Object {}
console.dir(d.__proto__.__proto__.constructor);// Object {}
console.dir(r.__proto__.__proto__.constructor);// Object {}
打印后是的,那么我们继续往上找
console.dir(a.__proto__.__proto__.__proto__);// null
console.dir(f.__proto__.__proto__.__proto__);// null
console.dir(d.__proto__.__proto__.__proto__);// null
console.dir(r.__proto__.__proto__.__proto__);// null
所以它们都继承最顶层的Object构造函数原型上的toString()和valueOf()等方法,这两个方法也是原型上定义的,我们可以通过打印Object.prototype
就可以看到这两个方法了
为什么会有原型原型链
我们再来看个例子
function People() { };
var people1 = new People();
var people2 = new People();
//我们先在people1实例对象上添加一个方法
people1.handleEvent = () => {
console.log('这是实例对象的方法');
}
然后执行people1实例对象上的方法
people1.handleEvent();//这是实例对象的方法
可以执行,没毛病
我们再来看下实例对象people2上面有没有这个方法
people2.handleEvent();//报错:Uncaught TypeError: people2.handleEvent is not a function
上面报错,但是这时候我想让people2实例对象也想有一个handleEvent方法,怎么办,我们不能又写一遍这个方法吧,如果实例对象过多,添加一样的方法不仅浪费资源而且添加起来特麻烦
所以我们就可以在原型上添加属性和方法,然后通过原型链来调用,这样就能节省资源
People.prototype.handleEvent = () => {
console.log('这是原型上添加的需要共享的方法');
};
people1.handleEvent();//这是原型上添加的需要共享的方法
people2.handleEvent();//这是原型上添加的需要共享的方法
记住,原型原型链最终的目的是让所有的实例对象能够共享其属性和方法
记住,原型原型链最终的目的是让所有的实例对象能够共享其属性和方法
记住,原型原型链最终的目的是让所有的实例对象能够共享其属性和方法
重要的事情说三遍
我们现在来看下它们的关系图
最后总结
- 当一个对象查找属性和方法时会从自身查找,如果查找不到则会通过__proto__指向被实例化的构造函数的prototype
- 隐式原型也是一个对象,是指向我们构造函数的原型
- 除了最顶层的Object对象没有__proto_,其他所有的对象都有__proto__,这是隐式原型
- 隐式原型__proto__的作用是让对象通过它来一直往上查找属性或方法,直到找到最顶层的Object的__proto__属性,它的值是null,这个查找的过程就是原型链
转载自:https://juejin.cn/post/7071109617593352206