前言
本文是在阅读《你不知道的 JS(上)》的第二部分 this 和对象原型 后,结合自己的想法,将我想要表达的观点,重新呈现一遍。
前情提要
本文是建立在对 JS 的原型链机制有一定了解的基础上进行的小拓展,原型链机制简单来说就是,JS 中所有对象都有个自己的原型对象,它们自上而下构建成一个原型链,当对一个对象的方法进行调用时,会在这个原型链里自下而上寻找这个方法,当找到原型链尽头(一个 null)时仍没有找到该方法就会报错。
函数是对象
在 JS 里函数实际上是特殊的对象,称为函数对象,内置的函数对象有 Object、Array、Function等等。
JS不存在类
严格上讲,JS 并不存在类,而是一个个对象。
所谓的继承实际上是委托,因为继承实际上需要复制机制,将子类和父类的内存隔离,子类继承父类的属性和方法是复制的,也就是子类对其进行重写不会影响到父类。但是 JS 的“子类”和“父类”其实并没有隔离,“子类”使用“父类”的方法的机制实际上的委托原型。
所谓的实例化(new)或者说调用构造函数,实际上我更情愿将这称为函数构造调用,因为在 JS 里实际任何函数都可以被 new 关键字调用,new 会把这个函数当做构造器去创建一个对象,并把这个对象的__proto__赋值为该函数的 prototype。即把任意函数当做构造对象的方法进行调用。
[!Note]
具体可以自己去品鉴一下《你不知道的 JS(上)》这本书
定义
好了,进入正题,先来简单介绍一下 __proto__ 和 prototype 的定义
__proto__
__proto__ 是一个存在于所有对象(包括函数)中的内部属性,它是在对象被创建时,由 JavaScript 引擎自动添加的,并且指向创建该对象的构造函数的原型对象(prototype)。称之为隐式原型。
prototype
prototype是函数对象独有的属性,指向一个对象,而该对象上有一个 constructor 方法,就是该函数对象。
prototype 这个属性名称之为函数原型,其属性值是一个对象,也可以称之原型对象。
🤡弗雷尔卓德规则怪谈
JS 中对象的关系大致如下图所示,真是没比《百年孤独》里的人物关系简单多少啊。
new 进行函数构造调用
任何普通对象都是通过函数对象的构造调用创建的,JS 引擎会将函数对象的 prototype 里的值(即原型对象)赋值给普通对象的__proto__,以此形成原型链。
自定义函数的原型对象其实也是由 Object 方法构造的
Object 原型对象
一般情况下,所有的原型对象都会指向 Object 原型对象, Object 原型是内置的原型对象,Object原型对象上有下列这些实例方法,不一一介绍了,可以留意一下其中的 Object.prototype.toString() 方法。
- Object.prototype.hasOwnProperty()
- Object.prototype.isPrototypeOf()
- Object.prototype.propertyIsEnumerable()
- Object.prototype.toLocaleString()
- Object.prototype.toString()
- Object.prototype.valueOf()
[!Note]
为什么说一般情况,因为如果你直接用 var obj = Object.create(null),而不是使用 new 关键字进行函数构造调用, 那么这个对象的原型对象就是直接指向 null 的
Function函数是个怪咖
特例是, Function.prototype === Function.__proto__ // true
所有的函数对象都是 Function 的实例, 所有的函数对象的__proto__都会指向 Funciotn 原型
function Foo(){
this.name = 'Foo'
}
console.log(Object.__proto__ === Function.prototype); // true
console.log(Foo.__proto__ === Function.prototype); // true
Function 原型对象是 JS 引擎内置的原型对象,Function 原型对象上有 apply, call, bind, toString, "instance of" 等方法,所以所有函数都可以使用这些方法
[!Note]
instance of 实际上就是自动调用了这里的
[Symbol.hasInstance]方法,a instance of b (a 是普通对象,b 是函数对象)其实就是在判断 b 的 prototype 在不在 a 的原型链上
Object.prototype.toString 🫸🫷Function.prototype.toString
下面是脑筋急转弯的题目了,来检测一下自己有没有理解 or 记住这奇奇怪怪发规则,看不懂可以多看几遍。
console.dir(Object.prototype.toString.call({}))
// 输出[Object object]
console.dir(Object.prototype.call({}))
// 报错TypeError
这是因为 call 方法是在 Function 原型对象的, Function 原型对象在 Object 原型对象的下游,所以 Object.prototype 是不能使用 call 方法的。 但是 Object.prototype.toString是一个函数,还记得吗,对 toString 进行 call 方法掉用,它就会通过原型链找到 Function 原型对象,即使用的实际上是 Function.prototype.call 方法。
console.dir(Object.toString.call({}))
// 报错TypeError
console.dir(Object.toString.call(Object))
// 输出 function Object() { [native code] }
??? 这是怎么回事,为什么Object.toString用不了了
这其实从侧面揭示了__proto__是原型链的本质,JavaScript 就会沿着 __proto__ 指向的原型去查找,如果还找不到,就继续沿着原型的 __proto__ 向上查找,直到找到属性或到达原型链的顶端(null)。
Object.toString实际上是在调用它的__proto__,也就是 Function.prototype。 前文说过了,Function 原型对象上定义了自己的 toString 方法,由于 Function 原型对象是在 Object 原型对象的下游,Function.prototype.toString 会覆盖掉 Object.prototype.toString,也就是说 Object.toString 先委托给了 Function.prototype,,就没 Object.prototype 什么事情了。
而Function.prototype 上的 toString 方法规范要求调用该方法时,this 必须是一个函数对象(Function object)。当 this 是普通对象(如 {})时,会抛出 TypeError。
后记
关于__proto__ 和 prototype,我大致上就了解到这里了,还有什么其他信息欢迎大家在评论区补充,有什么不对的地方,也欢迎指正。