likes
comments
collection
share

什么是原型原型链

作者站长头像
站长
· 阅读数 26

前言

大家好,我是安步尘,欢迎关注我的公众号,有时间也可以逛下我的博客 这篇文章是面试经常会被问的问题,同时也是比较难理解的知识,所以总结下我理解的

先简单介绍一下: 每个函数都有一个prototype原型(原型就是对象),原型对象有一个constructor属性,指向的是构造函数,访问对象的某一个属性或者方法时,会从对象自身查找,如果查找不到,就会去原型链上去找,原型的最终目的就是让所有的实例能够共享其属性和方法(很重要)。

我们先用代码来看下

function People() { };
const people = new People();
People.prototype.name = "xiaoxu";//在构造函数上添加原型属性

我们打印原型看看是什么鬼

console.log(People.prototype)

什么是原型原型链

我们看到了原型就是一个对象,没错,就是一个对象

结合上面的简单介绍,那么问题来了,上面提到构造函数、实例对象、constructor属性、,并且说了每一个函数都有原型,这些东西又是什么?了解原型原型链必须要先了解下这些知识,这样后面理解起来会更容易,我们一个一个来看

  • 什么是构造函数?
  • 构造函数和普通函数有什么区别?
  • 什么是实例对象?
  • constructor又是什么?
  • 什么是原型原型链?
  • 为什么会有原型原型链?
  • 最后总结

什么是构造函数

构造函数是用来创建新的实例对象的 使用new关键字来创建的 那么new的过程执行了什么呢? 这个问题也是一道面试题哦

new关键字执行的过程

  1. 创建一个新对象
  2. 将实例对象的__proto__隐式原型指向了People构造函数的原型
  3. 让构造函数中的this指向我们的实例对象
  4. 判断构造函数中有没有返回值,如果显示的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条就明白了

构造函数和普通函数有什么区别

区别如下

  1. 构造函数的this指向实例本身
  2. 普通函数的this指向调用者
  3. 构造函数需使用new关键字来创建新的实例对象,普通函数不需要
  4. 构造函数首字母要大写 如 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

什么是实例对象

  1. 创建实例对象必须要使用new关键字
  2. 我们使用new关键字来创建的构造函数,构造函数返回的对象就是实例对象
  3. 我们可以通过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();//这是原型上添加的需要共享的方法

记住,原型原型链最终的目的是让所有的实例对象能够共享其属性和方法 记住,原型原型链最终的目的是让所有的实例对象能够共享其属性和方法 记住,原型原型链最终的目的是让所有的实例对象能够共享其属性和方法

重要的事情说三遍

我们现在来看下它们的关系图

什么是原型原型链

最后总结

  1. 当一个对象查找属性和方法时会从自身查找,如果查找不到则会通过__proto__指向被实例化的构造函数的prototype
  2. 隐式原型也是一个对象,是指向我们构造函数的原型
  3. 除了最顶层的Object对象没有__proto_,其他所有的对象都有__proto__,这是隐式原型
  4. 隐式原型__proto__的作用是让对象通过它来一直往上查找属性或方法,直到找到最顶层的Object的__proto__属性,它的值是null,这个查找的过程就是原型链
转载自:https://juejin.cn/post/7071109617593352206
评论
请登录