春招面试复盘,理清原型、原型链~ 【JS Plan】

1,512 阅读10分钟

在这里插入图片描述

前言

从一道滴滴面试题开始说起,是很基础的有关原型,原型链问题,但是也是需要清楚的知道原型和原型链之间的关系,才能更好的回答,毕竟可能你清楚的思路也很重要哦。废话不多说,先来看题目吧:

Object.prototype.a = "jing1";
Function.prototype.a = "jing2";
function Preson() {};
var p = new Preson();
console.log(p.a);
console.log(p.constructor);
console.log(p.__proto__.__proto__.constructor);

我想原型基础比较好的你,应该已经可以说出答案来啦,请在脑海里留下你的答案。不过也不要因为太基础就不看下面的文章啦,我只是把它当成引子,来理清一下有关这方面的知识点。

好的,我们开始吧。先放导图: 在这里插入图片描述

一、对象

1、创建对象

对象可以使用两种形式定义

  • 文字语法:
var jingObj = {
  key: value;
  //
}
  • 构造形式:
var jingObj = new Object();
jingObj.key = value;

两种形式没有什么很大区别,就是文字语法创建对象可以定义多个对象,二构造函数式需要一个个添加(很少使用这种)。

2、对象的内容

对象是一个容器,封装了属性(property)和方法(method)。

3、属性访问

var jingObj = {
  a: 2
}
jingObj.a = 2; //2

我们创建了一个jingObj对象,通过jingObj.a进行一次属性访问,但这种访问,其实涉及到了以下几步:

  • jingObj.a在jingObj上实际上实现了[[Get]]操作,对象默认的[[Get]]操作首先回来对象中查找是否有名称相同的属性,如果有的话,就返回这个属性的值。
  • 如果它没有找到名称相同的属性,按照[[Get]]算法的定义会执行在原型链中查找,当原型链上有这个属性就会返回它的值(下文介绍)
  • 如果一直找到尽头(这个“尽头“也是下文会介绍)还是找不到,那么[[Get]]就会返回undefined

二、原型

1、介绍

在javascript中,对象有个特殊的[[Prototype]],就是对于其他对象的引用,几乎所有的对象在创建的时候[[Prototype]]属性都会赋一个非空的值。(不过它是可以为空的哦)

注意: ([[prototype]]和__proto__意义相同,均表示对象的内部属性,其值指向对象原型。前者在一些书籍、规范中表示一个对象的原型属性,后者则是在浏览器实现中指向对象原型。)

好了,知道对象有这个属性了,那么它到底有什么用呢? 还是刚才的代码:

var jingObj = {
  a: 2
}
jingObj.a = 2; //2

我们在上面说过了,当它在对象调用[[Get]]上找不到这个属性,那么就会在原型链上查找,比如我们创建一个新的对象关联到jingObj上:

var jingObj = {
  a: 2
}
var newObj = Object.create(jingObj);
console.log(newObj.a);//2

我们通过Object.create()创建了一个对象,并把这个对象的[[Prototype]]关联到指定的对象。

但是,如果newObj.a,在jingObj上也没找到a的属性,并且[[Prototype]]不为空,它依然会继续查找,一直到找到匹配的属性名输出该值,或者是一直没找到,输出undefined。

var jingObj = {
 
}
var newObj = Object.create(jingObj);
console.log(newObj.a);//undefined

2、原型链尽头

在前面没有解释原型链的尽头到底是哪里,现在我们来看看。事实上所有的普通的[[Prototype]]链最终都会指向Object.prototype.这个Object.prototype对象,包含了javascript中许多通用的功能。

3、Object上的一些方法

(稍微留点印象就可以啦,等下也会用到,加深印象哦) 比如: -.create(): 该方法接受一个对象作为参数,然后以它为原型,返回一个实例对象。该实例完全继承原型对象的属性

//原型对象
var Jing = {
  name: function() {
    console.log('jingda');
  }
};

//实例对象
var Hao = Object.create(Jing);
Object.getPrototypeOf(Hao) === Jing;
Hao.name();
console.log(Jing.name === Hao.name);
  • .toString() :
  • .valueOf() :
  • .hasOwnProperty() : 对象实例的hasOwnProperty方法返回一个布尔值,用于判断某个属性定义在对象自身,还是定义在原型链上
Date.hasOwnProperty('length') // true

Date.hasOwnProperty('toString') // false

length是Date的属性(构造函数Date可以接受多少个参数),但是toString不是。

个人的话,在深拷贝的时候会用的。不过有一个需要注意的是: hasOwnProperty方法是 JavaScript 之中唯一一个处理对象属性时,不会遍历原型链的方法

  • .isPrototypeOf() : 实例对象的isPrototypeOf方法,用来判断该对象是否为参数对象的原型。
  • .getPrototypeOf() : Object.getPrototypeOf方法返回参数对象的原型。这是获取原型对象的标准方法 使用:
var F = function () {};
var f = new F();
Object.getPrototypeOf(f) === F.prototype // true

三、“三角恋”关系

有关“三角恋”关系,我想大家知道,像下面这张图描述的: 在这里插入图片描述 从上面这个图可以看到:实例对象可以由构造函数通过new创建生成,这个实例对象上有个属性__proto__属性指向了构造函数的原型对象。构造函数和原型对象也有关系,就是构造函数上有个prototype属性指向的就是这个原型对象,而原型对象上也有个属性constructor指向的是这个构造函数。

让我们通过一个代码例子来看看:

function Person() {
}
var p = new Person();
console.log(Person.prototype === p.__proto__);//true
console.log(Person.prototype.constructor === Person);//true

console.log(p.constructor === Person);//true

我想你应该注意到了,在上面的“三角恋”图上,我并没有说明 实例到构造函数的指向关系,但在代码中,我又使用了p.constructor === Person,是因为实例p上本身是没有constructor属性,这个属性是Person.prototype上才有的,它默认指向的是Person这个构造函数。因此这里的p.constructor事实是委托了Person.prototype。

Person.prototype的.constructor属性只是Person函数在申明时的默认属性

当我们尝试定义一个Person.prototype,它会继续往原型链上方找,比如像这样:

function Person() {
}
Person.prototype = {}
var p = new Person();

console.log(p.constructor === Person);//false
console.log(p.constructor === Object);//true

这时候,p.constructor是指向Object而不是Person,因为我们这里创建了一个新对象并替换了函数默认的.prototype对象引用。

四、再看原型链(不简单呐)

第一眼看到这个图,嗯。。。。感人。但是,理解它,才是放这张图的目的。回想一下文章开头的那个面试题吧,你的答案是不是下面这个呢? jing1 [Function: Preson] [Function: Object]

好吧,基础好的你就当复习一下,不清楚的就直接跟我来走一遍这个图吧。

  • 这个图分为三部分,(左边)是new的实例对象 (中间)是构造函数,包括自定义的函数,Function,Object (右边) 原型对象,也就是中间这几个构造函数的原型对象。
  • 我们把它围绕三个中心开始分析,分别是

1、以构造函数Foo()为中心

2、以构造函数function Object() 为中心

3、以构造函数function Function()为中心

在这里插入图片描述

1、以构造函数function Foo()

1、通过一个new Foo()操作,可以创建一个实例对象(像f1),这个对象实例有一个__proto__属性指向这个构造函数的原型对象Foo.prototype()

2、Foo.prototype和构造函数,就是Foo.prototype.constructor === Foo();

3、Foo().proto === Function.prototype

2、以构造函数function Object() 为中心

1、通过一个new Object()操作,可以创建一个实例对象(像o1),和上面的f1对象实例一样,它衍生出的三角恋关系,就不再重复说明了

2、此外,要注意的,Object.prototype.proto === null;

3、最后,Object.proto === Function.prototype;

3、以构造函数function Function()为中心

1、Function和Function.prototype之间存在联系(图上很清楚了),但是这里要注意的是,Function.proto === Function.prototype

2、以及Function.prototype__proto__ === Object.prototype

回看文章开头的问题

Object.prototype.a = "jing1";
Function.prototype.a = "jing2";
function Preson() {};
var p = new Preson();
console.log(p.a);//jing1
console.log(p.constructor);//[Function: Preson]
console.log(p.__proto__.__proto__.constructor);//[Function: Object]
  • p.a 因为p上没有a的属性,因此它会到原型链上去找,首先是p.proto
console.log(p.__proto__)//Preson {}

也就是我们的Person.prototype 为Preson {},可以看到里面是没有a属性的,于是我们继续查找,

console.log(p.__proto__.__proto__)//{ a: 'jing1' }

好啦,我们找到了,Person.prototype.__proto__是我们的Object.prototype,上面有我们的属性a,因此输出它的值

  • p.constructor 默认指向我们的构造函数Person,因此输出是 [Function: Preson]。
  • p.proto.proto.constructor,这个的话,通过上面的原型链图也可以很清楚的知道就是[Function: Object]

大概有关原型和原型链的知识就先说到这了,下面来看看一些其他涉及到知识。(new instanceOf实现原理等。)

五、new实现原理

我们知道在通过构造函数创建实例对象的时候,需要用到的是new,那么这个new是如何实现,中间过程又是怎样的呢?

使用new来调用函数,或者说发送构造函数调用时,会自动执行下面的操作:

  • 1、创建(或者说)构造一个全新的对象;
  • 2、让这个对象挂到构造函数的原型链上(这里可以使用Object.create());
  • 3、这个新对象会绑定到函数调用的this(考虑改变this的执行 apply等);
  • 4、如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象,否则是返回 那个 返回的对象哦(下面我会把两种情况都拿出来)

好了,来看代码实现:

const myNew = function (func, ...args) {
	if (typeof func !== 'function') {
		return new TypeError('this is not a function');
	}
	// 创建一个空对象,指定原型为 func.prototype
	const obj = Object.create(func.prototype);
	// 绑定 this 并执行构造函数内部代码给新对象加上属性
	const result = func.apply(obj, args);
	// 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
	return result && result instanceof Object ? result : obj;
};

这是一个实现new的代码,让我们来测试一下,因为上面写了在返回结果的时候是两种不同的情况的,所以我们分开讨论:

  • 没有返回其他对象:
// 测试简单构造函数
function Person(name, age) {
 	this.name = name;
 	this.age = age;
 }

// 自己写的new
console.log('myNew');
const jing = myNew(Person, 'jing', 22);
console.log(jing.name,jing.age)//jing 22

// 原生new
console.log('new');
const hao = new Person( 'jing', 22);
console.log(hao.name,hao.age)//jing 22
  • 返回了其他对象:
// 用来测试构造函数返回了一个引用数据类型
function Person(name, age) {
	this.name = name;
	this.age = age;
	return {	
    name:'hao',
    age:23
  }
}

// 自己写的new
console.log('myNew');
const jing = myNew(Person, 'jing', 22);
console.log(jing.name,jing.age)//hao 23

// 原生new
console.log('new');
const hao = new Person( 'jing', 22);
console.log(hao.name,hao.age)//hao 23

当返回的是一个引用类型的时候,最后实例上使用的属性或者方法就是这个返回的引用类型,(这个也是有关this的指向变了,我在上一篇文章里面也说过哦 【春招面试复盘,重拾this)】

六、instanceOf运算符

instanceof运算符返回一个布尔值,表示对象是否为某个构造函数的实例。 像我们在实现new的过程中也使用到了,判断构造函数返回的是否为对象:result instanceof Object

让我们来看看如何实现,首先搞清楚它的使用及原理:

instanceof运算符的左边是实例对象右边是构造函数。它会检查右边构造函数的原型对象(prototype),是否在左边对象的原型链上。


const instanceofFunc = function (left,right) {
  if (typeof left !== 'object' || left === null) return false;
  // let proto = Object.getPrototypeOf(left);
  let proto = left.__proto__;
  while(true) {
    if(proto === null) return false;
    if(proto === right.prototype) return true;
    // proto = Object.getPrototypeOf(proto)
    proto = proto.__proto__
  }
}
// 定义构造函数
function C(){}
function D(){}
var o = new C();
console.log(instanceofFunc(o,C))//true
console.log(instanceofFunc(o,D))//false

这里有两个方法:获取原型对象,其中Object.getPrototypeOf方法返回参数对象的原型。这是获取原型对象的标准方法,前面也讲到了。

总结

好啦,今天就到这里了。原型和原型链的知识是比较基础,但也说实话,真的很常见的面试题,无论是问知识点,或者是看题。希望大家都能理清楚。

不过个人写的可能有点不系统,或者不是很清楚,欢迎评论区指正和反馈,你也可以前往“小黄书”-原型这一节看一下哦。

👇👇👇

我是婧大,一个大三学崽,目前正在准备实习面试。

这段时间应该主要是理理一些知识点,欢迎一起学习。wx:lj18379991972 💕💕💕

你的点赞是给我最大的支持🤞

在这里插入图片描述

参考文献: MDN 对象原型

Javascript 教程 - 原型

小黄书 - 原型 [[prototype]]