面试官:是不是所有对象都有隐式原型?

222 阅读5分钟

写在前面

大家好,这里是哆啦美玲为大家解答这次的知识点:是不是所有对象都有隐式原型?

答案是,否。

为什么呢?那就让我带着大家一起按顺序来学习以下的知识点后,就能解答啦~

构造函数的定义和类型

构造函数 是一种用于创建和初始化 class 对象实例的特殊方法。一个构造函数,可以生成多个实例对象,这些实例对象都有相同的结构,都可以使用这个构造函数的中属性与方法。

我们常见的构造函数有:

let str = new String()  //包装类 -- 字符串对象
let n = new Number()  //创建 Number 对象。
let b = new Boolean() //创建Boolean对象
let obj = new Object()  //将输入转换为一个对象
let arr = new Array() //创建一个Array对象
let fn = new Function()  //创建一个Function对象

原型 prototype

1.原型的定义

首先要认识原型,我们来看如下代码结果:

在上述截图中我们可以看出,我们创建一个构造函数Person,其中分别定义name、age属性并赋值;接着创建一个实例对象p,打印p和构造函数的prototype属性。可以看到prototype属性,是一个函数被定义出来的天生就具有的属性,它就是构造函数的原型,而Person.prototype是一个对象。

2.实例对象与原型的关系

做个小测试:如果往构造函数的原型上面添加属性或者方法,实例对象能否拥有? 显而易见是,实例对象也拥有了say属性。

因此,实例对象能够访问构造函数的原型上所拥有的属性和方法。

那实例可以修改原型上的属性吗?

不确定,那我们再做个测试看看~ 可以看出,在开始我们就重新设置了构造函数String() 的原型上所带的trim函数,是可以打印出新的函数给出的结果;但是试图修改str对象的length属性,但是并没有成功。

而在第二段代码中我们创建了构造函数Car() ,并重写了构造函数的原型的run() 方法,尝试删除实例对象car中的run() 发现打印结果中还有。

由此得出,实例对象无法修改(增删改)函数原型上的属性。
3.原型的意义

我们看下面这段代码:

function Car(color,owner){
    this.name = 'su7'
    this.height = 1400
    this.lang = 5000
    this.color = color
    this.owner = owner
}

let car1 = new Car('red','墩墩')
let car2 = new Car('green','美玲 ')
console.log(car1,car2)

实例对象car1car2在创建过程中,均要执行相同的代码给nameheightlang属性重新赋值,有点冗余了。所以我们是不是可以把重复的代码进行提炼,简化代码。

这让我想到了构造函数的原型,实例对象可以访问构造函数的属性。如下代码展示:

Car.prototype.name = 'su7'
Car.prototype.height = 1400
Car.prototype.lang = 5000

function Car(color,owner){
    this.color = color
    this.owner = owner
}

let car1 = new Car('red','墩墩')
let car2 = new Car('green','美玲 ')
console.log(car1.name,car2.height)

在实例对象创建后,构造函数原型上的属性是不会直接展示在实例对象中,所以需要的时候引用它才会被显示出来的。所以原型存在的意义就是为了让某一种数据结构拥有更多的方法可用,可以将一些固定的属性提取到原型上,减少重复的代码执行。

对象原型——隐式原型

我们先来看下面这张图片:

我们创建了一个obj对象,在js的执行中会自动将let obj = {}执行成let obj = new Object()。在控制台打印结果可以看出,obj对象存在一个prototype属性,而这个就是对象原型,也叫对象的隐式原型。在js代码中,对象的原型我们写成obj.__proto__的形式。

再看下面这张图,发现了什么? 我们会发现,构造函数的原型Object.prototype竟然和对象原型完全一样。为什么呢?

原因是,V8在查找对象的一个属性时,如果改属性不是对象还显示拥有的,那么V8就会去对象的对象原型上查找。

而对象的隐式原型会被赋值成创建该对象的构造函数的显示原型。

还有人和我一样懵逼的对吧,那让我们还原一下这个new的原理。首先是正常的创建一个构造函数和实例对象的过程代码如下:

// 在构造函数的原型上添加run属性
Car.prototype.run = "running"
function Car(){
    this.name = 'su7';
    this.height = 1400;
}
let car = new Car();

但是在我们人为展示这个new的原理时,代码其实可以解读如下:

// new的过程
Car.prototype.run = "running"
function Car(){
    // 创建对象
     var this = {
        name:'su7',
        height:'1400'
    }
    // 将构造函数原型赋值给对象原型
    this.__proto__ = Car.prototype  // {run : 'running'}
    return this;
}
let car = new Car();

总的来说分为以下四个步骤:

  • 创建一个 this 对象
  • 让构造函数中的逻辑正常执行(相当于往 this 对象上添加了属性)
  • 让 this 对象的__proto__ = 构造函数的 prototype
  • return this 对象

所以,我们可以搞明白了,并不是实例对象会直接去构造函数的原型上面访问属性和方法,而是会去已经copy了一份一样的对象原型上查找。

回到面试问题

都说JS中万物皆对象,构造函数的显示原型是对象,函数也不例外是个对象,这样查找的过程中原型就会形成原型链。 如图的结果显示,构造函数的原型对象的.__proto__指向的原型对象是Object, 而原型链的顶端是null。

那为什么不是所有的对象都有隐式原型呢? 我们再看一个例子。 图中代码的Object.create() 静态方法,以一个现有对象作为原型,创建一个新对象。所以对象b的隐式原型就是a;而对象c没有任何属性,所以没有__proto__属性,即没有隐式原型。

let obj = Object.create(null) 没有隐式原型。

到这,我的知识点干货就结束了。呼,还有不懂的或说的不对的地方还请留言区告诉我哦~

喜欢的话,记得点个赞喔~