谈谈prototype和__protp__
文章目录
前言
诶,人不努力,那又和大学生有什么区别呢?(别骂了别骂了)
相信很多像我这样的编程小白在入门js一段时间后,对JS中的prototype
、__proto__
与constructor
这三个属性存在许多困惑。接下来我将分享一下我个人做的一些笔记。
其实说实话,这三者就相当于是一个指针,js利用它们实现了一些机制。
ECMA 允许通过 构造器(constructor)创建对象。每个构造器实际上都是一个 函数(function) 对象。
- 所有的函数都有一个 prototype(显示原型) 属性,这个属性引用了一个对象,即原型对象,每个函数被创建的时候都会有一个prototye属性,这个属性会指向函数的原型对象。
- 所有对象都有 constructor 属性,这个属性指向构造此对象的函数的引用。
- 所有对象都有
__proto__
(隐式原型) 属性(只读),这个属性指向构造此对象函数的原型。
对象可以由"new 关键字 + 构造器调用"的方式来创建,js的new与其他语言并不同,下面我会介绍一下new做了哪些事。
window对象
接下来简单说明一下window对象,它是一个顶层对象,表示当前页面对象。
-
JS 代码还没运行的时候,JS 环境里已经有一个 window 对象了,这个window囊括了各种方法和属性在里面
所有 JavaScript 全局对象、函数以及变量均自动成为 window 对象的成员。
全局变量是 window 对象的属性,全局函数是 window 对象的方法。即通过var声明的变量,都可以在window对象中取到。
- window 对象有一个 Object 属性,window.Object 是一个函数对象,一切对象的
__proto__
都能用原型链来追溯到这个Object上。
我们平常使用的Object.defineProperty,Array.prototype等等,都是可以从window中找到的。
- window.Object 这个函数对象有一个重要属性是 prototype
- window.Object.prototype 里面有一些属性,例如:toString(函数)、valueOf(函数)等
看完这些,相信你们对window,prototype和__proto__
差不多有一个大概模糊的认识了。
ECMAScript
中5有种简单数据类型(也称为基本数据类型): Undefined
、Null
、Boolean
、Number
和String
。还有1中复杂的数据类型————Object
。js也是一门面向对象的语言,js代码中各种类和方法都与Object相关,Object机制是js实现面对对象编程的基础。
new背后的原理
new操作符将函数作为构造器进行调用时的过程:
- 新生成了一个对象newObj
- 将
newObj.__proto__
链接到TargetClass原型上 - newObj绑定 this到目标类TargetClass上
- 返回新对象newObj
执行这段代码后,会发生什么?
function Person(name){
this.name = name;
this.type = 'person'
}
let sxc = new Person("孙笑川")
Person与Person.prototype与sxc之间的关系如下图。
现在继续往下深究,对下面这段代码进行分析。
function Foo() {};
var f1 = new Foo();
根据对背后底层原理的理解,可以画出这样的一张图。
这张图乍一看挺眼花缭乱的,接下来我会对其按constructor、__proto__
和prototype三部分来进行分析,希望能尽量帮助自己和读者把这块吃透。
__proto__
属性
遵循ECMAScript标准,someObject.[[Prototype]]
符号是用于指向 someObject
的原型。从 ECMAScript 6 开始,[[Prototype]]
可以通过 Object.getPrototypeOf()
和 Object.setPrototypeOf()
访问器来访问。这个等同于 JavaScript 的非标准但许多浏览器实现的属性 __proto__
。
这是每个对象都有的隐式原型属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 [[prototype]],但是 [[prototype]] 是内部属性,我们并不能访问到,所以使用 _proto_
来访问。
因为在 JS 中是没有类的概念的,为了实现类似继承的方式,通过 _proto_
将对象和原型联系起来组成原型链,得以让对象可以访问到不属于自己的属性。
分析
从左往右,从上往下,__proto__
链分别如下。
1、f1.__proto__ ---------->Foo.prototype
2、Foo.prototype.__proto__---------->Object.protoype
3、Object.protoype.__proto__ ----------> NULL
4、Function.protoype.__proto__ ----------> Object.protoype
5、Foo.__proto__ ----------> Object.protoype
6、Object().__proto__ ----------> Function.protoype
7、Function().__proto__ ----------> Function.protoype
通过观察,可以发现以下几点规律。
-
__proto__
都是由对象指向对象(函数本身的本质也是对象,这是由js数据类型决定的) -
所有函数的
__proto__
都会指向Function.prototype -
一切对象的
__proto__
都能追溯到Object.prototype,再往下追溯就为null了 -
如果一个对象没有constructor,它将通过
__proto__
来向上(从父对象)获取constructor,其它属性和方法同理。 -
构造函数也好,prototype也罢,包括Object自身,本质上都是对象!(这很套娃)
作用
到这里,__proto__
的作用就显而易见了。
总结一下,__proto__
作用就是当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的__proto__
属性所指向的那个对象(可以理解为父对象)里找。
如果父对象也不存在这个属性,则继续往父对象的__proto__
属性所指向的那个对象(可以理解为爷爷对象)里找,如果还没找到,则继续往上找…直到原型链顶端null,再往上找就相当于在null上取值,会报错,至此,null就是原型链的终点。
由以上这种通过__proto__
属性来连接对象直到null
的一条链即为平时经常听到的原型链。
js正是基于原型链的机制来实现继承的。
Object.prototype
那问题来了,为什么源头是Object.prototype呢?
Object.prototype本身就是一个object,里面封装了一些object最最最基础的操作,这是每个对象都能用到的基础操作,例如toString()、valueOf()、get()、set()等等。
例如数组Array,就是在Object的基础上,封装了pop、push、slice等方法和length等属性。
用继承的思想来讲,就是Object是父类(基类),Array和Function继承了它,并通过__proto__
调用父类中的属性和方法。
Array.__proto__ === Function.__proto__
输出为true
prototype属性
prototype属性是一个显式原型属性,是函数所独有的,它是从一个函数指向一个对象。
基本上所有函数都有prototype属性,但是也有例外。
let fun = Function.prototype.bind()
// 如果这样创建一个函数,那么这个函数是不具有 `prototype `属性的。
分析
从左往右,从上往下,prototype
链分别如下。
1、Foo.prototype ----------> Foo.prototype
2、Object.protoype ----------> Object.protoype
3、Function.protoype ----------> Function.protoype
通过观察,可以发现以下几点规律。
-
prototype
属性是函数所独有的,且方向为从函数指向对象 -
通过改写prototype,可以影响到
__proto__
链上往后的对象
prototype
属性是函数所独有的。但由于JS中函数也是一种对象,所以函数也拥有__proto__
和constructor
属性,这点是致使我们产生困惑的很大原因之一。
作用
作用:包含可以由特定类型的所有实例共享的属性和方法,也就是让该函数所实例化的对象们都可以找到公用的属性和方法。任何函数在创建的时候,其实会默认同时创建该函数的prototype对象。
这里可以类比java里的public。
在使用vue的时候,我们经常会在main.js中使用到如下语句:
Vue.prototype.$bus = new Vue();
通过上面这条语句,我们可以在vue的任何一个组件中都能获取到这个bus(事件总线),这样就是通过改变原型链继而影响后面所有的子类的例子。
prototype 如何产生
当我们声明一个函数时,这个属性就被自动创建了。
function Foo() {}
并且这个属性的值是一个对象(也就是原型),只有一个属性 constructor
修改一个对象的prototype会发生什么?
// 定义一个Biology(生物)构造函数,作为 Person的父类
function Biology(){
this.superType = 'Biology';
}
function Person(name){
this.name = name;
this.type = 'Person';
}
定义了两个function来模拟类,一个是生物类,一个是人类,很明显,生物类应该是父类,那应该如何实现继承呢?
现在运行下面的语句,会发生什么?
// 改变Person的protype指针,指向一个Biology实例
Person.prototype = new Biology()
sxc = new Person("孙笑川")
new做了以下四件事。
- 新生成了一个对象newObj
- 将
newObj.__proto__
链接到TargetClass原型上 - newObj绑定 this到目标类TargetClass上
- 返回新对象newObj
如图所示,原本指向Person.prototype的指针都指向了新的生物实例上,而Person的constructor将会丢失。
现在new出来的person调用的构造函数则从原型链中找到Biology(),之后Person(“孙笑川”)也会被执行。可以实现子类对父类的覆写。
这便是js中最经典的继承方式——原型链继承。但这种继承机制也有不足:
-
当原型链中包含引用类型值的原型时,该引用类型值会被所有实例共享;
-
在创建子类型(例如创建Son的实例)时,不能向超类型(例如Father)的构造函数中传递参数.
constructor属性
constructor本质上是一个函数。
constructor
是一个公有且不可枚举的属性。一旦我们改变了函数的 prototype
,那么新对象就没有这个属性了(当然可以通过原型链取到 constructor
)。
分析
可以看到有如下规律
- constructor都是对象指向函数
- 如果一个对象(函数也是对象)本身没有constuctor,那么它将通过原型链获取到最近的constructor
作用
一般来说,我们是不会去动consructor的,
- 让实例对象知道是什么函数构造了它
- 如果想给某些类库中的构造函数增加一些自定义的方法,可以通过
xx.constructor.method
来扩展
总结
要想真正理解,还需要对window对象有一个深入的了解。这个window的设计理念是让它成为一个顶层对象,将其设计模式和思路理清楚了,对prototype、proto、constructor这三个东西自然是张口就来。