JavaScript原型与原型链:理解核心概念,构建强大的前端技能

444 阅读5分钟

网易面试题:所有对象身上都有隐式原型吗?为什么?

这道题你会吗?原型和原型链作为js语言的核心概念之一,为我们提供了强大的对象继承机制。今天我们将详细讲解js的原型以及原型链,帮助新手从零开始理解这一重要概念,学完这些,你就能完美解答网易的这道面试题啦!

原型 (显式原型)

  1. 定义:原型(prototype)是函数天生具有的属性,它定义了构造函数制造出来的对象的公共祖先。通过该构造函数创建的对象,可以隐式的继承函数原型上的属性和方法。

我们来看代码示例:

function Person() {
    this.name = '小明'
    this.age = 18
}

Person.eat = function() {
    console.log('想吃面条')
}

let p = new Person()
let p2 = new Person()

p.eat()//报错 

我们定义了一个构造函数Person,并创建了两个它的实例对象p和p2,Person里面有属性name和age,我们还往Person上添加了一个方法eat。

如果我们输出p,可以得到:

image.png

p这个实例对象里会有name和age,但是没有eat,如果调用p.eat会直接报错,eat也是Person里的方法,为什么p没有呢?我们看name和eat的区别:name前面有this.。p是通过new创建出来的对象,它会继承Person中的this.namethis.age。关于它是如何继承的如果你还不了解请看《关于数据类型分类,构造函数和包装类的重要底层原理》这篇文章中的new的过程这一部分。而eat是只在Person里的。

但是如果我们在Person的原型上添加一个say方法(原型prototype是函数天生就具有的属性):

Person.prototype.say = function() {
    console.log('hello,world!')
}
console.log(p);
p.say()

我们会看见结果是:

image.png

输出p里面仍然只有name和age,但我们调用p.say却是可行的,这是因为say是在Person的原型上,p可以隐式地继承在Person.prototype上的属性和方法。

console.log(p.say===p2.say)

p和p2是两个独立的对象,如果我们输出这个,得到的结果会是true,这说明say方法就是Person原型上的那个,所以说原型定义了构造函数制造出来的对象的公共祖先。

  1. 意义:可以提取共有属性,简化代码的执行

     function Car(owner,color) {
         this.name = 'BMW'
         this.lang = 4900
         this.height = 1400
         this.owner = owner
         this.color = color
     }
     let car = new Car('小A','red')
     let car2 = new Car('小B','pink')
     
    

    我们定义一个构造函数Car,里面有name,lang和height属性,还有需要传参数进来的owner和color,创建两个实例对象car和car2,那么两个实例对象中都会有这些属性,且name,lang和height的值是固定的。如果我们用prototype将其提取:

     Car.prototype.name = 'BMW'
     Car.prototype.lang = 4900
     Car.prototype.height = 1400
     function Car(owner,color) {
         this.owner = owner
         this.color = color
     }
     let car = new Car('小A','red')
     let car2 = new Car('小B','pink')
     
    

    那么我们就能简化代码,而实例对象仍能正常访问到这几个属性。

  2. 原型上的属性修改只能原型自己操作,实例对象无权修改

     car.name = '奔驰'
     console.log(car.name);//奔驰
     console.log(car);
     console.log(car2.name);//BWM
     console.log(car2);
    

如果我们将car.name修改为奔驰,来看一下它们的输出:

image.png

实际上car.name = '奔驰'只是往对象car里面新增加了一个值为奔驰的name属性,并没有修改原型上的name,从car2的输出我们就可以看出来,原型上的name属性是并未被修改的。但若是原型自己修改的属性:Car.prototype.name = '奔驰',则被Car创建的所有实例对象去访问name都会是奔驰

对象原型 (隐式原型)

在JavaScript中,每个实例对象都有一个特殊的属性称为"隐式原型"(__proto__[[prototype]]),它指向该对象的构造函数的原型。即实例对象的隐式原型===构造函数的显式原型。

继续以之前的代码为例:

console.log(car.__proto__===Car.prototype);
console.log(car.__proto__);

输出的结果是:

image.png

new的过程

我们之前在《关于数据类型分类,构造函数和包装类的重要底层原理》这篇文章中提到过new的过程有三个步骤,但其实是不完整的,第三个步骤应该是将 this 的隐式原型指向构造函数的显式原型,之后再进行this对象的返回。

  1. 创建this空对象
  2. 执行构造函数中的逻辑,将属性和方法添加到 this 对象上
  3. 将 this 的隐式原型指向构造函数的显式原型
  4. 返回this对象

这样子创建的对象才会能够访问构造函数的显式原型。

原型链

构造函数的原型也是一个对象,而对象就会有隐式原型,所以构造函数的原型也有隐式原型:

function Foo() {}
let foo = new Foo();

foo.__proto__ === Foo.prototype 

Foo.prototype.__proto__ === Object.prototype

Object.prototype.__proto__ === null

foo是构造函数Foo的实例对象,所以foo的隐式原型foo.__proto__恒等于Foo的原型Foo.prototype,而Foo原型的隐式原型则指向对象的构造函数原型Foo.prototype.__proto__ === Object.prototype,对象的构造函数原型的隐式原型则指向null,再往上就没有了。

顺着对象的隐式原型不断向上查找上一级的隐式原型,直到找到目标或者null为止,这种查找关系,就叫做原型链。

网易面试题:所有对象身上都有隐式原型?

看到这里,你的答案是肯定的吗:所有对象身上都有隐式原型。不不不,答案没有那么简单,不得不说,这道题是真的有点坑。

绝大多数的对象身上都有隐式原型,而那个没有的特例,让我们一起来看看吧:

let c = Object.creat(null)

如果我们用Object.creat(null)(不能为空,一定要有参数)方法来创建一个空对象,那么这个对象是会没有隐式原型的,只有这一个特例。

来挑战一下这张图,如果你能将每条链都看懂,那么你就彻底掌握原型与原型链啦:

image.png

你成功了吗ヾ(◍°∇°◍)ノ゙