一.写在前面
原型和原型链是 JavaScript 中的重难点之一,虽然 ES6 我们已经可以使用class
进行定义类,可以使用extends
来继承父类,但究其本质在 JavaScript 的内部还是使用的原型和原型链来实现的,所以学习和理解原型和原型链对于理解和深入 JavaScript 是必不可少的,好了 🦀 废话不多说,让我们开始今天的学习吧! 这篇文章我们会按照下述的内容模块进行学习和介绍。
二.普通对象的原型
当我们在浏览器上运行如下的代码的时候,我们会看到输出的内容中有一个比较特殊的对象[[Prototype]]
,这个对象就是我们编写的对象的原型对象.
let obj = { name: 'CodeUp', age: 12, } console.log(obj)
我们想要获取这个对象不能直接使用obj.[[Prototype]]
的方式来获取,JavaScript 给我们提供了 API 来获取普通对象的原型对象。
console.log(obj.__proto__) // 不推荐,仅仅是浏览器的实现 console.log(Object.getPrototypeOf(obj)) // 推荐,标准的实现
我们 来试一下,发现都是可以正常获取到的,但是在开发中推荐使用标准的写法__proto__
可能在某些浏览器上存在不兼容的情况,但是鉴于大多数浏览器都支持,这个 API 使用起来也方便,我们在使用的时候增加判断,防止某些情况下的不兼容即可。
三.函数对象的原型
在 JavaScript 函数也是对象的一种,它是Object
类的子类,关于Object
我们在下面会介绍,函数对象也具有(__proto__
),因为不经常使用我们称之为隐式原型,同时也具有作为函数自身的原型(prototype
)由于经常使用我们称之为显式原型,简单理解就是普通对象有的函数也有,但是是有差别的,作为函数自身它具有prototype
这个才是函数货真价实的原型。
function foo() {} console.log(foo.prototype) console.log(foo.__proto__)
函数对象和普通的对象是有关系的,为什么这么说哪? 因为所有的函数都是可以new
的,也就是通过函数可以创建对象,他们之间的关系我们可以通过以下的代码来看出来
function Foo(name, age) { this.name = name this.age = age } let obj1 = new Foo('芒果', 12) let obj2 = new Foo('招财', 13) console.log(Foo.prototype === obj1.__proto__) console.log(Foo.prototype === obj2.__proto__)
通过代码我们可以看出,函数的prototype
和所创建对象的__proto__
指向的是同一个对象,也就是本质上说这两个东西是相等的。
四.函数原型的 constructor
事实上原型对象上还有一个属性叫做constructor
属性,默认情况下原型上都会添加一个属性叫做 constructor,这个 constructor 指向当前的函数对象,我们可以使用代码来验证下
function Person() {} var personPrototype = Person.prototype console.log(personPrototype) console.log(personPrototype.constructor) console.log(personPrototype.constructor === Person) // true
讲到这里我们已经基本理清楚了,对象,函数,隐式原型,显式原型以及constructor
之前的关系,一图胜千言,他们之间的关系用图来表示其实就是这样的。
五.重写原型对象
如果我们需要在原型上添加非常多的东西的时候我们可能需要重写原型对象,我们可以直接对原型对象进行赋值,举个简单的例子,当我们在编写一个函数的时候需要往原型上添加很多的属性,就像下列的代码一样。
function Person() {} Person.prototype.message = 'Hello Person' Person.prototype.info = { name: '哈哈哈', age: 30 } Person.prototype.running = function () {} Person.prototype.eating = function () {}
但是需要挂载在原型上的东西非常多的时候我们也可以直接对原型对象进行重写。
Person.prototype = { message: 'Hello Person', info: { name: '哈哈哈', age: 30 }, running: function () {}, eating: function () {}, }
但是虽然重写比较方便但是这样其实也会造成一个问题,那就是constructor
从上面的关系图我们可以看出来,构造函数会指向这个函数,所以我们就需要增加一行代码,并且原来的constructor
是不可枚举的,所以我们还需要通过属性描述符来进行不可枚举的控制。
Person.prototype = { message: 'Hello Person', info: { name: '哈哈哈', age: 30 }, running: function () {}, eating: function () {}, } Object.defineProperty(Person.prototype, 'constructor', { value: Person, enumerable: false, })
六.对象的原型链
当我们了解了对象的原型以及对象的原型与函数的原型,以及和constructor
之间的关系后,我们就可以通过对象之间一层一层的关系来研究和学习一下一个重要的概念原型链
var obj = { name: 'mongo', age: 1, } console.log(obj.message)
当我们在浏览器中执行上述的代码的时候,会经历如下几个阶段
obj
上查找message
属性obj.__proto__
上面查找对应的属性obj.__proto__.__proto__
上查找结果为 null 返回 undefined
上述的这个过程就是标准的原型链中查找对应属性的过程,接下来我们来对上述的代码进行改造一下
obj.__proto__ = { message: 'Hello AAA', } obj.__proto__.__proto__ = { message: 'Hello BBB', } obj.__proto__.__proto__.__proto__ = { message: 'Hello CCC', }
我们可以对原型对象进行重新赋值,然后obj
查找就会沿着原型链进行查找,当最后没有继续赋值的话默认会指向Object.prorotype
然后会指向null
七.Object 详解
在 JavaScript 中Object
是一个非常特别的存在,如果你熟悉 JavaScript 那么你肯定见过如下的代码
let obj = new Object() let obj = {}
其实这两种操作本质上是一样的,对象字面量本质上就是使用Object
构造函数来创建的,在这里Object
是一个函数,从浏览器的输出我们可以看出来指向的是一个相同的对象,并且constructor
属性指向的是一个函数。
其实我们在上面对对象进行重写的时候我们直接使用对象覆盖了对象的原型,同时也覆盖了对应的constructor
如果我们不重写constructor
函数将它赋值为原函数,其实赋值的这个对象的原型就是指向Object.prototype
这个对象的原型是对象的默认指向。
let obj = { name: 'CodeUp', age: 12, } obj.__proto__ = { message: '招财是只鸟', } console.log(obj.__proto__.__proto__)
八.总结
这篇文章我们学习了原型和原型链,普通对象和函数都有自己的原型,但是比较常用的是函数的prototype
我们称之为显式原型,对象的原型默认指向的是Object.prototype
当我们对某个对象的原型进行重写后默认指向的都是这个,原型之间这样一层一层组成的链,我们称之为原型链,在 JavaScript 中继承是基于原型的,这写内容也是我们在后续学习 ES5 相关继承方式的前提,虽然原型设计的非常巧妙,但是它并非是 JavaScript 的原创,但是确是这门语言的设计哲学的精妙所在。