备战26春招,彻底搞懂前端八股之原型链(上):知识点讲解+面试示例回答

在 Javascript 中,有一个很基础、很核心,又有点绕的知识点,就是原型和原型链。很多同学对原型链有一定了解,但是感觉在面试中说不明白、说不完整。本文将带你彻底搞懂原型和原型链相关的知识点,并且会给出面试中让面试官满意的示例回答,还准备了一些问答题和常考题

知识点讲解

原型

在 JS 中,所有的函数,都自带一个属性,叫做 prototype,它是一个对象类型,它的中文名,叫做「显式原型」。在 prototype 中,又有一个属性,叫做 constructor,这个属性指向那个函数,即:假设有个函数叫 func,则 func.prototype.constructor === func 。如下图

当我们以 func 为构造函数,构造一个新的实例对象 a 时,a 会有一个私有属性,中文名叫「隐式原型」,之前各大浏览器使用 __proto__来代表隐式原型,后来使用 [[Prototype]] 来代表隐式原型,在 EcmaScript 中,有一个 API Object.getPrototypeOf 来 获取隐式原型,也就是说, a.__proto__ === a.[[Prototype]]===Object.getPrototypeOf(a)。实例对象的隐式原型,就指向构造函数的显式原型,也就是说,Object.getPrototypeOf(a)===func.prototype。如下图

在 JS 中,当我们找 a 的某个属性或方法时,它会先在自身找,如果找不到,就去它的隐式原型上找。举个例子:假设我们找 a.getName,如果 a 本身没有 getName,而 a.__proto__中有 getName,也是可以返回的。

说了半天,隐式原型、显式原型,这些属性,到底有什么用呢?

用处就是,共享属性和方法。

什么意思呢?

我们可以给 func.prototype 加入一个 getName 方法,这样每当我们创建一个 func 的实例对象,这些对象都可以共享 getName 方法。

在 JS 中,有几个特殊的构造函数,它们分别是:Array、Function、Object

所有的数组,他们的构造函数都是 Array,这意味着,如果我们给 Array.prototype 中加入一个方法,那么所有的数组都可以使用这个方法。聪明的你已经想到了,我们平时在用数组的时候,经常使用 [1,2,3].push(4) 这样的写法,那么 push 这个方法,很有可能,就是 JS 官方添加到 Array.prototype 中的。我们可以在浏览器控制台打印一下 Array.prototype,验证一下:

果然,我们常用的 concat、push、pop、map 等等这些方法,都是放在 Array.prototype 中的。

所有的函数,他们的构造函数都是 Function。这意味着,如果我们给 Function.prototype 中加入一个方法,那么所有的函数都可以使用这个方法。聪明的你已经想到了,我们经常对函数使用 func.call(null) 这样的写法,那么 call 这个方法,很有可能,就是 JS 官方添加到 Function.prototype 中的。我们可以在浏览器控制台打印一下 Function.prototype.call,验证一下。在这里我就不验证了,同学们可以自己尝试操作一下~

聪明的你猜到了,接下来我要说 Object 了。没错,大部分对象,他们的构造函数,都是 Object。注意,这里我没有像之前的一样说全部,那么剩余的「小部分对象」是什么情况呢?就是像 const a = new func() 这样,a 也是个对象,它的构造函数就不是 Object,而是 func。所以对象就分为两种情况,一种是使用 new 创建的对象,一种是不使用 new 创建的对象。前者的构造函数是 new 后面的函数,后者的构造函数是 Object。

根据以上内容,我们就可以发现很多神奇的事情了。

首先,Array、Object,还有我们上面提到的 func,它们都是函数,也就是说,它们的构造函数都是 Function。也就是说,Object.getPrototypeOf(Array) === Object.getPrototypeOf(Array) === Object.getPrototypeOf(func) === Function.prototype

相信细心的你已经发现了,原来 Function.__proto__也指向 Function.prototype ! 这意味着 Function 的构造函数是它自己!没错,因为 Function 也是函数,只要是函数,它的构造函数就是 Function

还有一个事情。我们之前提到,函数的 prototype ,都是**对象类型。**有几个特例:Function.prototype 是个函数类型,Array.prototype 是个数组类型。但是呢,你去看 Object.getPrototypeOf(Function.prototype) 和 Object.getPrototypeOf(Array.prototype),会发现它的 constructor 也仍然是 Object。这几个特例要注意。

既然函数的 prototype ,他们的类型都是 对象,那就意味着,这些 prototype 的 proto,指向 Object 的 prototype。如下图(为了方便查看,我把新增的内容标为绿色):

到此为止,图中所有的对象(这里的对象是广义的对象,也包括函数)都有了自己的 proto,除了 Object.prototype。它当然也有自己的 proto,但是它的__proto__比较特殊,是 null,它是原型链的尽头。

到此为止,原型的所有相关概念,都已解释清楚。我们接下来看一下,原型到底是怎么形成链的。

原型链

所谓的原型链,全名其实叫「隐式原型链」。之前我们已经提到过,当我们找 a 这个对象的某个属性或方法时,它会先在 a 自身找,如果找不到,就去 a 的隐式原型上找。那如果还没找到呢?那就会继续去 a 的隐式原型 的隐式原型 上找,如果还找不到,再去 a 的隐式原型 的隐式原型 的隐式原型 上找,直到找到 Object.prototype.proto

这么说还是有点抽象,我们还是以 func 和 a 为例,用代码来看一下:

假设我们 要找 a.name 属性

第一步: 在 a 这个对象本身找有没有 name 属性,如果没有,则进入第二步

第二步:在 a.__proto__这个对象上找name 属性。 由于 a 的构造函数是 func,所以 a.__proto__实际上就是 func.prototype。如果在 func.prototype 上没有找到 name 属性,就进入第三步。

第三步:在 a.proto.__proto__这个对象上找 name 属性。由第二步我们可以知道,a.__proto__实际上就是 func.prototype,所以 a.proto.__proto__实际上就是 func.prototype.proto。我们已经知道,func.prototype 构造函数是 Object,所以 func.prototype.__proto__实际上就是 Object.prototype。所以第三步就是 在 Object.prototype 上找 name 属性,如果没找到,则进入下一步

第四步:在 a.proto.proto.__proto__这个对象上找 name 属性。由第三步我们知道,a.proto.__proto__实际上就是 Object.prototype,所以 a.proto.proto.__proto__实际上就是Object.prototype.proto,也就是 null。到了这一步,就走到了原型链的尽头,就会返回一个 undefined。

我们用一张图来辅助理解:

到这里,我们就彻底理解了原型和原型链是怎么一回事。

instanceof

在 JS 中,有一个方法可以用来判断 某个对象 是不是 某个构造函数 的实例。它的原理,就是判断 这个构造函数的 prototype 在不在这个对象的原型链上。这么说有点抽象,我们用代码来解释一下。

function func() {

}
const a = new func();
const b = [];

a instanceof func; // true
a instanceof Object; // true
b instanceof func; // false
b instanceof Array; // true
b instanceof Object; // true
func instanceof Function; // true
func instanceof Object; // true

instanceof 的具体原理可以参考以下代码(你暂时还不需要能够手写以下代码,只要能看懂就行,后面会有专门的手写代码教程,可以期待一下~)

function myInstanceof(left, right) {
  
  //注释掉的是一些特殊情况,我们不用管,看后续的核心逻辑就行
  
  // // 右侧必须是函数(构造函数),否则会抛错
  // if (typeof right !== 'function') {
  //   throw new TypeError('Right-hand side of instanceof is not callable');
  // }

  // // 原始值(number、string、boolean、symbol、bigint、null、undefined)直接返回 false
  // if (left === null || (typeof left !== 'object' && typeof left !== 'function')) {
  //   return false;
  // }

  // 取右侧构造函数的原型对象
  var prototype = right.prototype;
  // 取左侧对象的原型链起点
  var proto = Object.getPrototypeOf(left);

  // 一直往上找原型链,直到尽头(null)
  while (proto !== null) {
    // 找到了同一个原型对象,返回 true
    if (proto === prototype) {
      return true;
    }
    proto = Object.getPrototypeOf(proto);
  }

  // 原型链到底也没找到
  return false;
}

// 使用示例:
function Person(name) {
  this.name = name;
}
var p = new Person('Tom');

console.log(myInstanceof(p, Person));   // true
console.log(myInstanceof(p, Object));   // true
console.log(myInstan


面试回答示例

如果你能在面试中一口气回答出以下所有内容,你将跟其他候选人大幅拉开差距

面试官问:说说你对原型链的理解/你如何理解原型链/你知道原型链吗?

示范回答:

原型和原型链是 JS 用来实现继承的一种机制。

所有的对象都有一个属性叫 proto,所有的函数都有一个属性叫做 prototype,实例对象的 __proto__就指向构造函数的 prototype,并且 prototype对象中有一个属性叫 constructor,指向这个构造函数。

当我们访问对象(假设叫 obj)的某个属性或方法的时候,JS会先在obj内部寻找,如果没找到,就会去obj.proto(也就是它构造函数的prototype,假设构造函数叫 func)上面去找,如果还是没找到,就继续去 func.prototype.__proto__去找(由于 func.prototype 是个对象类型,所以func.prototype的构造函数是Object,所以 func.prototype.__proto__就是 Object.prototype),如果还没有找到,就继续去 Object.prototype.__proto__上去找。Object.prototype.__proto__是null,所以到此为止就查找结束。

原型链主要用来让实例对象继承构造函数的一些方法和属性,比如我们创建一个数组,它能够使用 slice、splice 这些方法,就是因为这些方法定义在 Array.prototype 上面,而所有的数组都是 Array 的实例。

面试高频题

接下来,我们用一些练习题,来巩固一下以上所学知识点。

问答题

  1. Object.prototype.proto 是什么
  2. Function.prototype.proto 是谁
  3. 构造函数自身的__proto__是什么
  4. Object.proto 答案是什么
  5. Object instanceof Function 是 true 还是false
  6. Function instanceof Object 是 true 还是 false
  7. Function.prototype===Function.__proto__是true还是false

先自己思考、回答,然后对答案

答案:

  1. Object.prototype.proto 是 null,也是原型链的尽头
  2. Function.prototype.__proto__是谁呢,这要看 Function.prototype 的构造函数是谁。上面我们已经说过,它的构造函数是 Object,所以它的__proto__就等于Object.prototype。即:Function.prototype.__proto__是Object.prototype
  3. 构造函数的__proto__是谁,要看构造函数的构造函数是谁(听上去有点拗口,但是这句话类似于「父亲的父亲是谁」,这么类比就明白了)。所有函数的构造函数都是Function。所以构造函数的 __proto__是Function.prototype
  4. Object.__proto__是谁,要看 Object 的构造函数是谁。Object是个函数,所有函数的构造函数都是Function。所以Object. __proto__是 Function.prototype
  5. Object instanceof Function 是 true 还是 false?那要看 Object 的原型链上有没有 Function.prototype。Object的原型链如下: Object 自身 -> Object.proto(由第四题我们知道,它是 Function.prototype) -> Object.proto.proto(即 Function.prototype.proto,由第二题我们知道,它是 Object.prototype)->Object.proto.proto.proto(即Object.prototype.proto,由第一题我们知道,是null)。所以Object 的原型链上有 Function.prototype,所以是 true
  6. Function instanceof Object 是 true 还是 false?那要看 Function 的原型链上有没有 Object.prototype。Function 的原型链如下:Function 自身 -> Function.proto(由第三题我们知道,它是 Function.prototype) -> Function.proto.proto(即 Function.prototype.proto,由第二题我们知道,它是 Object.prototype)->Object.proto.proto.proto(即Object.prototype.proto,由第一题我们知道,是null)。所以Function 的原型链上有 Object.prototype,所以是 true
  7. 由第三题我们知道,所有函数的构造函数都是 Function。Function 也是个函数,所以它的构造函数也是Function(没错,是它自己),换句话说,Function是Function的实例对象,Function也是Function的构造函数。所以 Function.prototype===Function.proto

代码输出题(答案和详解下篇出)

在面试中,你回答完「对原型链的理解」后,面试官可能会给你一段代码,让你回答输出。这里我总结了一些常考输出题,你可以先自己尝试回答一下,回答完后把代码复制到浏览器控制台对对答案。明天我会再出一篇文章,给出答案和详细解释。你可以先点个关注,以免迷路~

function Person(name) {
    this.name = name
}
var xinyishui = new Person('前端新一水');
console.log(xinyishui.__proto__) //
console.log(xinyishui.__proto__.__proto__) 
console.log(xinyishui.__proto__.__proto__.__proto__) 
console.log(xinyishui.__proto__.__proto__.__proto__.__proto__)
console.log(xinyishui.__proto__.__proto__.__proto__.__proto__.__proto__)
console.log(xinyishui.constructor)
console.log(xinyishui.prototype)
console.log(Person.constructor)
console.log(Person.prototype)
console.log(Person.prototype.constructor)
console.log(Person.prototype.__proto__)
console.log(Person.__proto__) 
console.log(Function.prototype.__proto__)
console.log(Function.__proto__)
console.log(Object.__proto__)
console.log(Object.prototype.__proto__)


function Person () {
 getName = function () {
   console.log(1);
 }
 return this;
}

Person.getName = function () {
 console.log(2);
}

Person.prototype.getName = function () {
 console.log(3);
}

var getName = function () {
 console.log(4);
}

function getName () {
 console.log(5);
}

Person.getName();
getName();
Person().getName();
getName()
new Person.getName();
new Person().getName();
new new Person().getName();

var Person = function() {};
Object.prototype.a = function() {
  console.log('a');
};
Function.prototype.b = function() {
  console.log('b');
}
var xinyishui = new Person();
xinyishui.a();
xinyishui.b();
Person.a();
Person.b()

function Person(){
    Person.a = function(){
        console.log(1);
    }
    this.a = function(){
        console.log(2)
    }
}

Person.prototype.a = function(){
    console.log(3);
}

Person.a = function(){
    console.log(4);
}

Person.a();
let xinyishui = new Person();
xinyishui.a();
Person.a();

function Person() {
    this.name = '前端新一水'
  }
  Person.prototype.speak = () => {
    console.log('前端面试,看我就够了!')
  }
  const xinyishui = new Person()
  console.log(Person.prototype.constructor === Person && xinyishui.constructor === Person && xinyishui instanceof Person)

var A = {n: 1};
var B =  function(){this.n = 2};
var C =  function(){var n = 3};
B.prototype = A;
C.prototype = A;
var b = new B();
var c = new C();
A.n++
console.log(b.n);
console.log(c.n);

function A(){
}
function B(a){
  this.a = a;
}
function C(a){
  if(a){
this.a = a;
  }
}
A.prototype.a = 1;
B.prototype.a = 1;
C.prototype.a = 1;
 
console.log(new A().a);
console.log(new B().a);
console.log(new C(2).a);

function Person() {
    this.a = 1;
    this.b = [1, 2, this.a];
    this.c = { demo: 5 };
    this.show = function () {
        console.log(this.a , this.b , this.c.demo );
    }
}

function Student() {
    this.a = 2;
    this.change = function () {
        this.b.push(this.a);
        this.a = this.b.length;
        this.c.demo = this.a++;
    }
}

Student.prototype = new Person();
var person = new Person();
var student1 = new Student();
var student2 = new Student();
student1.a = 11;
student2.a = 12;
person.show();
student1.show();
student2.show();
student1.change();
student2.change();
person.show();
student1.show();
student2.show();

function SuperType(){
    this.property = true;
}

SuperType.prototype.getSuperValue = function(){
    return this.property;
};

function SubType(){
    this.subProperty = false;
}

SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function (){
    return this.subProperty;
};

var instance = new SubType();
console.log(instance.getSuperValue());

你可以先自己尝试回答一下,回答完后把代码复制到浏览器控制台对对答案。明天我会再出一篇文章,给出答案和详细解释。你可以先点个关注,以免迷路~

#备战春招##前端实习##前端面试#
前端新一水八股系列 文章被收录于专栏

前端新一水八股系列,每日讲解一道面试高频八股题,涵盖CSS、JS、Vue、React、工程化、性能优化、场景题等

全部评论

相关推荐

虹数据库工程师岗开抢!不卷996,用技术搞定数据“地基”,毕业即拿稳职业跳板还在担心校招找不到“能学到真东西、成长看得见”的技术岗?虹科数据库工程师校招通道开启,不用跟资深大佬卷项目经验,我们更看重你对数据逻辑的敏感度,带你从0到1搭建企业级数据架构,把校园里的理论知识直接落地成行业认可的项目履历!在这里,你不用做重复枯燥的运维,核心工作全是“练手级”硬活——参与公司核心业务的数据库设计:1.负责数据库日常管理,协助性能分析与竞品对比,优化性能与稳定性;2.协助数据库部署、监控、备份及容灾,处理常见问题,保障安全可用;3.参与数据库POC方案设计,协助客户实施与故障诊断,提供技术支持;4.撰写客户需求解决方案与技术文档,协助创作技术内容、制作分享材料;5.联动多团队,提炼产品技术亮点,助力产品推广与品牌建设。我们不搞“画饼式”福利,能给到的全是实打实的诚意:薪资对标行业top30%,毕业就能实现经济独立;弹性上下班,拒绝无效加班,保证你有时间深耕技术,也有精力平衡生活;入职即配笔记本电脑,定期组织技术沙龙,还能接触到工业领域前沿的数据应用,简历含金量远超同龄人。只要你满足这3点,就赶紧投!1.本科及以上,1-3年数据库经验,熟1种主流数据库;2.善沟通、有基础英语能力,会写技术文档;3.对数据库感兴趣,爱学习,有技术分享经验优先。校招窗口期就15天,与其纠结“能不能行”,不如投了再说!虹科不看你现在有多厉害,只看你未来想变多厉害,期待和你一起,用数据技术撑起企业核心竞争力!
点赞 评论 收藏
分享
评论
4
5
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务