注:本文首发于我的博客,移步 获取更好的阅读体验。
抛开封面图,我们先以 MDN 的一句话作为开端,
对于使用过基于类的语言 (如 Java 或 C++) 的开发人员来说,JavaScript 有点令人困惑,因为它是动态的,并且本身不提供一个 class
实现。虽然在 ES2015/ES6 中引入了 class
关键字,但那只是语法糖,JavaScript 仍然是基于原型的。
那么到底什么是原型?
原型
我们先用构造函数创建一个实例对象,
其实理解原型,就是理解构造函数,实例对象和原型对象之间的关系,
function Engineer(name) {
this.name = name
}
Engineer.prototype.coding = function() {
console.log('write less, do more.')
}
const engineer = new Engineer('campcc')
复制代码
JavaScript 中,每一个构造函数都有一个 prototype
属性,它指向构造函数的原型对象:
Engineer.prototype // {coding: ƒ, constructor: ƒ}
复制代码
原型对象中有一个 constructor
属性指回构造函数:
Engineer.prototype.constructor === Engineer // true
复制代码
而每一个实例对象都有一个 __proto__
属性,当我们使用构造函数创建实例时,实例的 __proto__
属性就会指向构造函数的原型对象:
engineer.__proto__ === Engineer.prototype // true,__proto__ 其实是一个 JavaScript 的非标准但许多浏览器都实现的属性,从 ECMAScript 6 开始,支持通过符号 [[Prototype]] 或者方法 Object.getPrototypeOf() 访问
复制代码
构造函数,实例对象与原型对象的关系为:
原型链
为了更好的理解什么是原型链,我们尝试调用实例对象的几个方法,
engineer.coding() // write less, do more.
engineer.toString() // "[object Object]"
engineer.map() // Uncaught TypeError: engineer.map is not a function
复制代码
结果看似很出乎意料,因为我们其实并没有在实例里定义 coding
和 toString
方法啊,但是它们却能够被成功调用,为什么?因为在 JavaScript 中,当我们试图访问一个对象的属性或方法时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索:
我们尝试访问 map
方法时报错了,因为原型链上找不到此方法。原型链查找会一直持续,直到找到一个名字匹配的属性或方法,或者达到原型链的末尾,而根据定义,null
就是原型链的末尾:
Object.prototype.__proto__ === null // true,null 在这里可以理解为 “没有对象”
复制代码
如果查找过程中遇到同名属性或方法,位于原型链底端的属性或方法会被优先应用,这叫做 “属性遮蔽(property shadowing)”。比如我们在 Engineer
的原型对象上声明一个同名的 name
属性:
Engineer.prototype.name = 'engineer'
engineer.name // campcc,这里不会打印 'engineer',因为在原型链查找的过程中,实例对象中就已经存在 name 属性了
复制代码
总结一下,原型链其实就是对象或原型对象的 __proto__
组成的一条原型查找链。
几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例,但有两个例外:
- Object.prototype:我们刚才说了,它的原型是 null
- Object.create(null):创建一个原型为 null 的空对象
补充
关于封面图,其实隐喻了 JavaScript 中一直存在争议的一个问题,
Function.__proto__ === Function.prototype // true
复制代码
先来看看在 Chorme V8 中的打印结果:
Function.__proto__ // ƒ () { [native code] }
Function.prototype // ƒ () { [native code] }
Object.__proto__ // ƒ () { [native code] }
复制代码
从打印结果来看,根本不存在 "Function 也是 Function 本身的一个实例" 的说法,因为不管是 Function.__proto__
, Function.prototype
还是 Object.__proto__
都是引擎创建的,Function
也是由引擎创建的,至于为什么会相等,只能说每个语言都存在缺陷,这个问题就像为什么 typeof null === "object"
一样,不用过于纠结。
总结
JavaScript 中,
- 每个函数都有
prototype
属性,代表构造函数的原型对象 - 每个实例对象都有
__proto__
属性,指向构造函数的原型对象 - 每个原型对象中都有
constructor
属性,指向构造函数,标识原型对象的是由哪个函数构造的 - 对象或原型对象的
__proto__
组成了一条原型链,原型链其实也是一条查找链 - 原型链查找会一直持续,直到找到同名的属性或方法,或者到达原型链的末尾
null
- 原型链查找会遵循 属性遮蔽 原则,位于底层的属性或方法会被优先找到
勘误与提问
如果有疑问或者发现错误,可以在相应的 issues 进行提问或勘误
如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励
(完)