文章目录
前言
初入前端领域的萌新说起原型通常都会觉得晦涩,但是原型及原型链作为前端领域的基础原理又是必须掌握的,下面就让我们来详细说说原型&原型链。
一、原型的来源和定义
1)原型的来源
在面向对象编程中,继承是非常实用也非常核心的功能,很多面向对象语言都支持两种继承:接口继承和实现继承。前者只继承方法签名,后者继承实现的方法。例如C++,Java语言中的类,子类可以继承父类中的某些属性和方法,这样可以实现数据的共享。但JavaScript中只有对象没有类(ES6中添加了类Class,之前是没有的),为了解决共享数据的问题,JavaScript的开发者们提出了原型这一概念,来实现数据和方法的共享。
2)原型的定义
-
在ECMAScript标准2019规范中的原型(prototype)描述:
给其他对象提供共享属性的对象
-
《 Javascript高级程序设计 》中的原型描述:
每个函数都会创建一个prototype属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。实际上,这个对象就是通过调用构造函数创建的对象的原型。使用原型对象的好处是,在它上面定义的属性和方法都可以被对象实例共享。原来在构造函数中直接赋给对象实例的值,可以直接赋值给它们的原型。
-
《 你不知道的javascript 》中的原型描述:
Javascript中的对象有一个特殊的 [[Prototype]] 内置属性,其实就是对其他对象的引用。几乎所有的对象在创建时 [[Prototype]] 都会被赋予一个非空的值。
3)小结
-
原型是一个对象,为其他对象提供共享属性的对象,又称原型对象。可以说原型不是一个固定的对象,它只是承担了某种职责。当某个对象,承担了为其他对象提供共享属性的职责时,它就成了该对象的原型。换而言之,不同对象的原型可能都是不一样的。
-
几乎所有对象在创建时都会被赋予一个非空的值作为原型对象的引用,来实现共享数据的效果。
-
在不同的对象上原型存放的方式也有所差别:
函数(function):函数是一种特殊的对象,函数的原型存放在其prototype属性上。
对象(Object): 普通对象的原型是存放到内置属性[[Prototype]]上,可以通过对象的__proto__来访问对象的原型。
数组(Array): 数组也是一种特殊的对象,但与函数不同的是它的原型和普通对象一样,也是存放到内置属性[[Prototype]]上,可以通过数组的__proto__来访问数组的原型。
二、原型的创建
1)函数的原型的创建
无论何时,只要创建一个函数,就会按照特定的规则为这个函数创建一个prototype属性,该属性指向函数的原型对象。
const Person = function(){}
console.log(Person.prototype)
刚创建的函数原型对象,包含一个constructor的函数和[[Prototype]]的内置属性,如图所示:
我们可以在函数原型对象上添加属性和方法,通过构造函数生成的实例共享原型对象的属性和方法:
const Person = function(){}
Person.prototype.age = 10
Person.prototype.say = function(){
console.log('你好,我是函数原型上的方法')
}
const xiaochen = new Person() // 生成实例xiaochen
console.log(xiaochen.age) // Person构造函数并没有给实例赋予属性和方法,此二行的属性及方法均来自原型
xiaochen.say()
console.log(Person.prototype)
1-1)构造函数,原型对象和实例对象三者间的关系
在上例中,Person
是构造函数,Person.prototype
是原型对象,xiaochen
是实例对象。
constructor属性
通过前面的内容我们已经了解到函数在创建时会生成一个prototype属性,这个属性指向原型对象。默认情况下,所有函数的原型对象会自动获得一个名为constructor的属性,指回与之关联的构造函数。 看下面一段代码:
console.log(Person.prototype === xiaochen.__proto__) // true
console.log(Person.prototype.constructor === Person) // true
- 构造函数的原型(原型对象)和实例对象的原型是同一个
- 构造函数的原型(原型对象)的constructor属性指向构造函数本身
2)对象的原型的创建
前面已经说到过几乎所有对象(一些特殊的对象之后说明)创建时都会被赋予一个非空的值作为原型。对象的原型是存放到内置对象[[Prototype]]上的,可以通过__proto__访问。
const xiaochen = {}
console.log(xiaochen)
console.log(xiaochen.__proto__)
可以看到,对象的原型对象上包含了我们常用的一些方法,比如:hasOwnProperty/toString/valueOf等,平时我们调用对象的这些方法时其实就是调用的原型对象的方法。
3)数组的原型的创建
数组是一种特殊的对象,我们创建一个空数组来看下。
const xiaochen = []
console.log(xiaochen)
console.log(xiaochen.__proto__)
可以看到数组的内置属性上比对象多了一个length属性,这就是数组创建后可以直接访问的数组长度属性,这是数组对象实例的属性。而在数组的原型对象上有很多我们平时使用的数组处理方法,我们平时调用这些方法的时候其实就是调用的数组原型对象上的方法。这也就是为什么不管你在哪里创建一个数组,都可以随意使用这些方法,因为这些方法是所有数组共享的。
4)小结
通过对函数、对象、数组不同对象原型的查看。我们可以看到,每当我们新创建一个对象或数组时,都可以通过它们的原型来共享一些公共的方法和属性。通过这种特殊的机制(原型)实现了数据和方法的共享。
这里需要注意的一点是:创建函数、对象和数组时为其创建的属性prototype或[[Prototype]]是对其他对象的引用。如果我们通过某个对象修改了它的原型,那其他对象的原型也是引用的同一个的话也会对应变化。
三、原型的层级
1)属性和方法的访问
在通过对象访问属性时,会按照这个属性的名称开始搜索。首先会在对象实例身上查找,找到了给定的名称,则返回该名称对应的值。如果没有找到这个属性,则搜索会沿着指针进入对象的原型对象
,然后在原型对象上找到该属性后,再返回对应的值。这就是原型用于在多个对象实例间共享属性和方法的原理。如果还没有找到则继续上述步骤,直至找不到为止,此时返回undefined。
const obj1 = {}
obj1.__proto__.a = 100
console.log(obj1.a) // 100 来自原型对象
2)属性和方法的遮蔽
如果给对象实例身上添加一个属性,这个属性就会遮蔽原型对象上的同名属性,虽然不会修改它,但会屏蔽对它的访问。JavaScript提供了hasOwnProperty()方法来确定某个属性是在实例上还是在原型对象上。
const obj1 = { a: 50 }
obj1.__proto__.a = 100 // 实际代码不要这么写,会同步到所有对象上噢
obj1.__proto__.c = 5
console.log(obj1.a) // 50 来自实例对象
console.log(obj1.c) // 5 来自原型对象
console.log(obj1.hasOwnProperty('a')) // true 实例有自己的a属性
console.log(obj1.hasOwnProperty('c')) //false 实例无自己的c属性
四、原型链
1)原型链的定义
回顾一下原型的定义:几乎所有对象在创建时都会被赋予一个非空的值作为原型对象的引用。那么原型对象也是一个对象,这个原型对象被创建时也会被赋予一个非空的值作为其原型对象(即原型对象的原型)的引用,如此循环下去就会构造一条无限长的访问链路。访问路径如第三节所述,原型链
就类似于这种访问链路。
2)原型链的终点
有一个特殊的对象在创建时会被赋予一个null值作为其原型对象的引用,这个对象的原型为空。可以把这个对象看做最初原型,作为所有原型链的终点(非手动设置的终点)。这个对象就是创建普通对象时的原型。也就是Object.prototype。
Object.prototype.__proto__ === null // true Object.prototype的原型对象为null
3)普通函数、对象、数组三者之间原型的关系
3-1)对象
构造函数Object的原型是一个比较特殊的对象,我把这个对象称为最初原型对象,这个对象的__proto__属性比较特殊,是指向null的。JS其他对象上的原型对象都是从它继承演变而来的,这也就是为什么我们称JS万物皆是对象(函数、数组,演变而来的特殊对象)。
const obj = {} // 简写方式
const objj = new Object()
console.log(obj.__proto__ === Object.prototype) // true
obj
、Object
、Object.prototype
三者的关系如图所示:
3-2)数组
数组和对象类似,数组是构造函数Array的实例。数组构造函数Array和对象构造函数Object的关联是:构造函数Array的原型对象的原型对象和构造函数Object的原型对象是同一个对象。
const arr = []
console.log(arr.__proto__ === Array.prototype) // true
console.log(Array.prototype.__proto__ === Object.prototype) // true
关系如图所示:
3-3)函数
普通函数的原型对象和Array,Object等构造函数的原型对象是不同的,但它们又最终汇交于最初原型对象。可以说函数的原型对象继承自Object.prototype又发生了演变。
const Person = function (name) {
this.name = name
}
const xiaochen = new Person('xiaochen')
console.log(xiaochen.__proto__ === Person.prototype) // true
console.log(Person.prototype.__proto__ === Object.prototype) // true
console.log(Function.prototype.__proto__ === Object.prototype) // true
关系如下图:
小结
可以发现数组,对象,函数构成的原型链。它们的终点是最初原型对象,而最初的原型对象指向null。这种从实例对象指向原型对象,原型对象又指向自己的原型对象,层层链接的结构我们称之为原型链
。
总结
原型链
是通过对象特有的原型
构成的一种链式结构,主要用来继承多个引用类型的属性和方法。默认情况下,所有引用类型都继承自Object.prototype
。