给我三分钟,带你完全理解JS原型和原型链

160 阅读5分钟

前言

当我们要对一个对象上的属性进行基本的增删改操作时,对应的代码语句会写成下面这样:obj.abc = 'student'delete obj.abcobj.abc = teacher。这些语句操作的都是obj对象上的显式属性,显式属性是直接定义在对象字面量或者构造函数中的属性。然而一个对象上可以有显式属性也可以有隐式属性,隐式属性可以来源于创建这个对象的构造函数的原型,接下来就来探讨一下JS编程中的一个核心概念————原型

读完探析JS中的对象和包装类 - 掘金 (juejin.cn)这篇文章,再来学习本文体会会更加深刻

对原型的基本认知

函数的原型是函数身上自带的一个属性prototype,它定义了构造函数制造的对象的公共祖先。

  • 通过new一个构造函数创建出来的对象 会隐式继承到构造函数的原型上的属性

以下列代码为例,往构造函数Car的原型上添加属性lastName和方法say,通过new Car实例化出来的对象p的字面量上却没有显式具有这个属性和方法

然而通过 . 直接却能够直接访问到car对象上的lastName属性值,也能直接调用car对象上的run方法,说明构造函数Car实例化出来的对象隐式具有了lastName属性和say方法

Car.prototype.lastName = 'xiaomi'  // 往原型这个对象上添加属性
Car.prototype.run = function() {   // 往原型这个对象上添加方法
    console.log('running');
}
function Car() {
    this.name = 'su7'
}

let car = new Car()
console.log(car); // 打印出:Car { name: 'su7' }
console.log(car.lastName); //  打印出:xiaomi
car.run() // 打印出:running
  • 实例对象可以修改显示继承到的属性,但是无法修改隐式继承到的(原型上的)属性

修改对象上的属性和给对象增加一个属性都是通过.的方式,假若我要修改对象car上隐式集成到的lastName属性,我会通过 car.lastName = 'huawei' 来修改,然而这只会给对象car显式地增添一个新属性lastName,通过构造函数的原型隐式继承到的lastName的属性值xiaomi并不会被修改成huawei

  • 实例对象无法给原型新增属性

要给原型新增属性只能靠构造函数自己,Car.prototype.color = red就可以给构造函数Car的原型新增一个color属性

  • 实例对象无法删除原型上的属性

同理 要删除原型上的属性也只能靠构造函数自己,delete Car.prototype.color就可以删除构造函数Car的原型上的color属性,再通过 car.color访问color属性已经是undefined了

  • 可以通过原型修改construct指向

每个对象都会隐式具有一个属性constructor,它也是继承自构造函数的原型上的constructor。通过对象上的constructor可以记录该对象是由谁来创建的 即谁是它的构造函数。 比如: console.log(car.constructor) 打印出[Function: Car]。 然而constructor可以通过原型修改指向至别的函数:(但是一般都不会这样去用)

function Bus() {}

Car.prototype = {
    constructor: Bus
}

function Car() {}

let car = new Car()
console.log(car.constructor);    // 打印出[Function: Bus]

对象的原型

函数有原型prototype,对象也有原型_proto_,我们人为地将函数的原型称为显式原型,对象的原型称为隐式原型。并且,对象的隐式原型 完全等于 创建它的构造函数的显式原型

当往构造函数的原型上增添属性时,我们不能直接看见对象字面量上新增了一个属性,但能访问到对象上的这个属性,根本原因就是该属性没有直接添加在对象上,而是添加在了对象的原型上,即往构造函数的显式原型上添加属性会同步添加在对象的隐式原型上。

js引擎在查找属性时会先查找对象显式具有的属性,找不到再去查找对象的隐式原型 (proto) 。所以当我们使用数组、字符串等数据结构身上的一些方法时,实际上是在使用这些数据结构的构造函数(Array、String)的显式原型上的方法

原型链

函数的原型是一个对象,而几乎所有对象都会有自己的隐式原型。

js引擎在查找属性时,会顺着对象的隐式原型向上查找,找不到则查找隐式原型的隐式原型,一直向上,直到找到null为止,这种查找关系,称之为原型链。

看下面的例子理解一下:(查找son身上的属性找不到就找隐式原型上的属性,son的隐式原型是Father构造函数创建的对象,如果还找不到就继续向上查找到GrandFather构造函数创建的对象,再找不到就继续向上...)

GrandFather.prototype.say = function() {
    console.log('grandfather say');
 }
function GrandFather() {
    this.age = 60
    this.like = 'drink'
}

Father.prototype = new GrandFather()
function Father() {
    this.age = 40
    this.fortune = {
        card: 'visa'
    }
}

Son.prototype = new Father()
function Son() {
    this.age = 18
}

let son = new Son()
console.log(son.age); // 18
console.log(son.fortune); // { card: 'visa' }
console.log(son.like);  // drink
console.log(son.say()); // grandfather say

特例

只有一种对象没有隐式原型:let obj = Object.create(null)通过这种方法创建出来的obj对象是没有原型的

Object.create(a)方法创建一个对象,让新对象隐式继承a对象的属性

结语

原型在理解对象的继承和属性查找机制方面起着关键作用,对象用自己继承构造函数体里的属性,用自己的原型隐式继承构造函数的原型上的属性。

认真读完后相信你已经对js的原型知识点有了更加深刻的理解,阅读过程中若发现有任何不足或亮点,欢迎大家在评论区指出交流~