浅析prototype、__protp__和constructor

谈谈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囊括了各种方法和属性在里面

    img

所有 JavaScript 全局对象、函数以及变量均自动成为 window 对象的成员。

全局变量是 window 对象的属性,全局函数是 window 对象的方法。即通过var声明的变量,都可以在window对象中取到。

img

  • window 对象有一个 Object 属性,window.Object 是一个函数对象,一切对象的__proto__都能用原型链来追溯到这个Object上。

我们平常使用的Object.defineProperty,Array.prototype等等,都是可以从window中找到的。

img

  • window.Object 这个函数对象有一个重要属性是 prototype

img

  • window.Object.prototype 里面有一些属性,例如:toString(函数)、valueOf(函数)等

img

看完这些,相信你们对window,prototype和__proto__差不多有一个大概模糊的认识了。

ECMAScript中5有种简单数据类型(也称为基本数据类型): UndefinedNullBooleanNumberString。还有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之间的关系如下图。

img


现在继续往下深究,对下面这段代码进行分析。

function Foo() {};
var f1 = new Foo();

根据对背后底层原理的理解,可以画出这样的一张图。

img

这张图乍一看挺眼花缭乱的,接下来我会对其按constructor、__proto__和prototype三部分来进行分析,希望能尽量帮助自己和读者把这块吃透。




__proto__属性

遵循ECMAScript标准,someObject.[[Prototype]] 符号是用于指向 someObject 的原型。从 ECMAScript 6 开始,[[Prototype]] 可以通过 Object.getPrototypeOf()Object.setPrototypeOf() 访问器来访问。这个等同于 JavaScript 的非标准但许多浏览器实现的属性 __proto__

这是每个对象都有的隐式原型属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 [[prototype]],但是 [[prototype]] 是内部属性,我们并不能访问到,所以使用 _proto_ 来访问。

因为在 JS 中是没有类的概念的,为了实现类似继承的方式,通过 _proto_将对象和原型联系起来组成原型链,得以让对象可以访问到不属于自己的属性。

分析

img

从左往右,从上往下,__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()等等。

img

例如数组Array,就是在Object的基础上,封装了pop、push、slice等方法和length等属性。

用继承的思想来讲,就是Object是父类(基类),Array和Function继承了它,并通过__proto__调用父类中的属性和方法。

Array.__proto__ === Function.__proto__
输出为true



prototype属性

prototype属性是一个显式原型属性,是函数所独有的,它是从一个函数指向一个对象

基本上所有函数都有prototype属性,但是也有例外。

let fun = Function.prototype.bind()
// 如果这样创建一个函数,那么这个函数是不具有 `prototype `属性的。

分析

img

从左往右,从上往下,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

img

如图所示,原本指向Person.prototype的指针都指向了新的生物实例上,而Person的constructor将会丢失。

现在new出来的person调用的构造函数则从原型链中找到Biology(),之后Person(“孙笑川”)也会被执行。可以实现子类对父类的覆写。

img

这便是js中最经典的继承方式——原型链继承。但这种继承机制也有不足:

  • 当原型链中包含引用类型值的原型时,该引用类型值会被所有实例共享;

  • 在创建子类型(例如创建Son的实例)时,不能向超类型(例如Father)的构造函数中传递参数.




constructor属性

constructor本质上是一个函数。

constructor 是一个公有且不可枚举的属性。一旦我们改变了函数的 prototype ,那么新对象就没有这个属性了(当然可以通过原型链取到 constructor)。

img

分析

img

可以看到有如下规律

  • constructor都是对象指向函数
  • 如果一个对象(函数也是对象)本身没有constuctor,那么它将通过原型链获取到最近的constructor

作用

一般来说,我们是不会去动consructor的,

  • 让实例对象知道是什么函数构造了它
  • 如果想给某些类库中的构造函数增加一些自定义的方法,可以通过 xx.constructor.method 来扩展

总结

要想真正理解,还需要对window对象有一个深入的了解。这个window的设计理念是让它成为一个顶层对象,将其设计模式和思路理清楚了,对prototype、proto、constructor这三个东西自然是张口就来。

参考资料

《帮你彻底搞懂JS中的prototype、__proto__与constructor(图解)》

《前端进阶之道》

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JavaScript 中,每个对象都有一个 __proto__ 属性,指向其构造函数的原型对象。而每个函数都有一个 prototype 属性,指向该函数实例化出来的对象的原型对象。 __proto__ 属性是一个指针,指向该对象的构造函数的原型对象。通过 __proto__ 属性可以访问原型对象中的属性和方法。这个属性在 ES6 中已经被标准化,可以用 Object.getPrototypeOf() 来获取对象的原型。 prototype 属性是函数的一个特殊属性,指向一个对象。当函数用作构造函数创建实例对象时,该对象的原型会指向构造函数的 prototype 属性指向的对象。也就是说,该对象可以访问构造函数原型对象中的属性和方法。 举个例子: ``` function Person(name) { this.name = name; } Person.prototype.sayHello = function() { console.log(`Hello, my name is ${this.name}`); } const p = new Person('Tom'); console.log(p.__proto__ === Person.prototype); // true console.log(Person.prototype.constructor === Person); // true console.log(p.constructor === Person); // true ``` 在这个例子中,我们定义了一个构造函数 `Person`,并给其原型对象添加了一个 `sayHello` 方法。我们通过 `new` 关键字实例化了一个 `Person` 对象 `p`。这个对象的 `__proto__` 属性指向了 `Person.prototype`,因此我们可以通过 `p.__proto__.sayHello()` 或者 `Person.prototype.sayHello.call(p)` 来调用 `sayHello` 方法。同时,我们也可以通过 `Person.prototype` 来访问 `Person` 构造函数原型对象中的属性和方法。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值