首页IT科技js原型概念和作用(JavaScript:原型(prototype))

js原型概念和作用(JavaScript:原型(prototype))

时间2025-08-04 23:34:07分类IT科技浏览4451
导读:面向对象有一个特征是继承,即重用某个已有类的代码,在其基础上建立新的类,而无需重新编写对应的属性和方法,继承之后拿来即用;...

面向对象有一个特征是继承              ,即重用某个已有类的代码                     ,在其基础上建立新的类       ,而无需重新编写对应的属性和方法       ,继承之后拿来即用;

在其他的面向对象编程语言比如Java中                     ,通常是指              ,子类继承父类的属性和方法;

我们现在来看看       ,JS是如何实现继承这一个特征的;

要说明这个                     ,我们首先要看看              ,每个对象都有的一个隐藏属性[[Prototype]];

对象的隐藏属性[[Prototype]]

在JS中,每个对象obj                     ,都有这样一个隐藏属性[[Prototype]]                     ,它的值要么是null,要么是对另一个对象anotherObj的引用(不可以赋值为其他类型值)              ,这另一个对象anotherObj                     ,就叫做对象obj的原型;

通常说一个对象的原型       ,就是在说这个隐藏属性[[Prototype]]              ,也是在说它引用的那个对象                     ,毕竟二者一致;

现在来创建一个非常简单的字面量对象       ,来查看一下这个属性:

可以看到       ,对象obj没有自己的属性和方法                     ,但是它还有一个隐藏属性[[Prototype]]              ,数据类型是Object       ,说明它指向了一个对象(即原型)                     ,这个原型对象里面              ,有很多方法和一个属性;

其他的暂且不论,我们先重点看一下                     ,红框的constructor()方法和__proto__属性;

访问器属性(__proto__)

访问[[Prototype]]

从红框可以看到                     ,属性__proto__是一个访问器属性,有getter/setter特性(这个属性名前后各两个下划线);

问题是              ,它是用来访问哪个属性的?

我们来调用一下看看:

可以看到                     ,__proto__访问器属性       ,访问的正是隐藏属性[[Prototype]]              ,或者说                     ,它指向的正是原型对象;

值得一提的是       ,这是一个老式的访问原型对象的方法       ,现代编程语言建议使用Object.getPrototypeOf/setPrototypeOf来访问原型对象;

但是考虑兼容性                     ,使用__proto__也是可以的;

请注意              ,__proto__不能代表[[Prototype]]本身       ,它只是其一个访问器属性;

设置[[Prototype]]

正因为它是访问器属性                     ,也即具有getter和setter功能              ,我们现在可以控制对象的原型对象的指向了(并不建议这样做):

如上图,现在将其赋值为null                     ,好了                     ,现在obj对象没有原型了;

如上图,创建了两个对象              ,并且让obj1没有了原型                     ,让obj2的原型是obj1;

看看       ,此时obj2.name读取到obj1的属性name了              ,首先obj2在自身属性里找name没有找到                     ,于是去原型上去找       ,于是找到了obj1的name属性了       ,换句话说                     ,obj2继承了obj1的属性了;

这就是JS实现继承的方式              ,通过原型这种机制;

让我们看看下面的代码:

正常的obj2.name = Jerry的添加属性的语句       ,会成为obj2对象自己的属性                     ,而不会去覆盖原型的同名属性              ,这是再正常不过了,继承得来的东西              。只能读取                     ,不能修改(访问器属性__proto__除外);

现在的问题是                     ,为什么obj2.__proto__是undefined?上面不是刚刚赋值为obj1了吗?

原因就在于__proto__是访问器属性,我们读取它实际上是在调用对应的getter/setter方法              ,而现在obj2的原型(即obj1)并没有对应的getter/setter方法                     ,自然是undefined了;

现在综合一下       ,看下面代码:

为什么最后obj2.__proto__输出的是hello world              ,为什么__proto__成了obj2自己的属性了?

关键就在于红框的三句代码:

第一句let obj2 = {}                     ,此时obj2有原型       ,有访问器属性__proto__       ,一切正常;

第二句obj2.__proto__ = obj1                     ,这句调用__proto__的setter方法              ,将[[Prototype]]的引用指向了obj1;

这一句完成以后       ,obj2因为obj1这个原型而没有访问器属性__proto__了;

所以第三句obj2.__proto__ = hello world的__proto__已经不再是访问器属性了                     ,而是一个普通的属性名了              ,所以这句就是一个普通的添加属性的语句了;

构造器(constructor)

在隐藏属性[[Prottotype]]那里,看到其有一个constructor()方法                     ,顾名思义                     ,这就是构造器了;

类对象与函数对象

类对象

在其他编程语言比如Java中,构造方法通常是和类名同名的函数              ,里面定义了对象的一些初始化代码;

当需要一个对象时                     ,就通过new关键字去调用构造方法创建一个对象;

那在JS中       ,当我们let obj = {}去创建一个字面量对象的时候              ,发生了什么?

上面这句代码                     ,其实就是let obj = new Object()的简写       ,也是通过new关键字去调用一个和类名同名的构造方法去创建一个对象       ,在这里就是构造方法Object();

这种通过new className()调用构造方法创造的对象                     ,称为类对象;

函数对象

但是              ,再等一下       ,JS早期是没有类的概念的                     ,那个时候大家又是怎么去创建对象的呢?

想一下              ,创建对象是不是需要一个构造方法(即一个函数),本质上是不是new Function()的形式去创建对象?

对咯                     ,早期就是new Function()去创建对象的                     ,这个Function就叫做构造函数;

这种通过new Function()调用构造函数创造的对象,称为函数对象;

构造函数和普通函数又有什么区别呢?除了要求是用function关键字声明的函数              ,并且命名建议大驼峰以外                     ,几乎是没有区别的:

看       ,我们声明了一个构造函数Cat()              ,并通过new Cat()创造了一个对象tom;

打印tom发现                     ,它有一个原型       ,这个原型和字面量对象的原型不一样       ,它有一个方法一个属性;

方法是constructor()构造器                     ,指向的正是Cat()函数;

属性是另一个隐藏属性[[Prototype]]              ,暂时不去探究它是谁;

也就是说       ,函数对象的原型                     ,是由另一个原型和constructor()方法组成的对象;

我们可以用代码来验证一下              ,类对象和函数对象的原型的异同点:

如上所示,创建了一个函数对象tom和一个类对象obj;

可以看出:

函数对象的原型的方法constructor()指向构造函数本身;

函数对象的原型的隐藏属性[[Prototype]]和字面量对象(Object对象)的隐藏属性                     ,他们两的引用相同                     ,指向的是同一个对象,暂时不去探究这个对象是什么              ,就认为它是字面量对象的原型即可;

还可以看到                     ,无论是类对象       ,还是函数对象              ,其原型都有constructor()构造器;

这个构造器在创建对象的过程中                     ,具体起了什么样的作用呢?

让我们先看看函数对象tom的这个原型是怎么来的?我们之前一直都是在说对象有一个隐藏属性[[Prototype]]指向原型对象       ,究竟是哪一步       ,让这个隐藏属性指向了原型对象呢?

函数的普通属性prototype

事实上                     ,每个函数都有一个属性prototype              ,默认情况下       ,这个属性prototype是一个对象                     ,其中只含有一个方法constructor              ,而这个constructor指向函数本身(还有一个隐藏属性[[Prototype]],指向字面量对象的原型);

可以用代码佐证                     ,如下所示:

注意                     ,prototype要么是一个对象类型,要么是null              ,不可以是其他类型                     ,这听起来很像隐藏属性[[Prototype]]       ,不过prototype只是函数的一个普通属性              ,对象是没有这个属性的;

来看下这个属性的特性吧:

可以看到                     ,它不是一个访问器属性       ,只是一个普通属性       ,但是它不可配置不可枚举                     ,只能修改值;

它的value值              ,眼熟吗?正是构造函数创建的函数对象的原型啊;

它居然还有一个特性[[Prototype]]       ,不要把它和value值里面的属性[[Prototype]]弄混                     ,前者是prototype属性的特性              ,后者是prototype属性的一个隐藏属性,虽然此刻他们都指向字面量对象的原型                     ,但是前者始终指向字面量对象的原型                     ,后者则始终指向原型(而原型是会变的);

这里也不再去追究为什么它会有这样一个特性了,让我们把重点放在prototype属性本身;

new Function()的时候发生了什么

事实上              ,只有在调用new Function()作为构造函数的时候                     ,才会使用到这个prototype属性;

我们来仔细分析一下上面代码具体发生了什么:

let tom = new Cat()这句代码的执行流程如下:

先调用Cat.prototype属性的特性[[Prototype]](我们知道它指向字面量对象的原型)里面的constructor()构造器       ,创建一个字面量空对象              ,当然此时这个对象的隐藏属性[[Prototype]]也都已经存在了                     ,将这个对象分配给this指针; 然后返回this指针给tom       ,即tom引用了这个字面量空对象       ,同时this指向了tom; 然后执行构造函数Cat()本身的语句                     ,即this.name = "Tom"              ,于是tom就有了一个属性name; 然后将Cat.prototype属性值value       ,复制(注意                     ,这里是复制              ,不是赋值,这意味着这里不是传引用                     ,而是传值)给tom的隐藏属性[[Prototype]]                     ,即tom.__proto__ = Cat.prototype;

如果我们用代码去描述上面整个过程,就类似于下面这样:

// let tom = new Cat()的整个具体流程              ,类似于下面这样 let tom = {}; //创建字面量对象                     ,并赋值给变量tom tom.name = "Tom"; // 执行Cat()函数 tom.__proto__ = Cat.prototype; // 将Cat的prototype的属性值赋值给tom的隐藏属性[[Prototype]]

现在已经说清楚了new Function()发生的具体过程       ,上面代码的输出结果也佐证了我们所说的:

函数对象tom的原型正是Cat函数的属性prototype的值value              ,可以看到他们的constructor()构造器都指向Cat函数本身                     ,并且tom.name的值Tom;

然后我们修改了Cat函数的prototype的值value       ,Cat.prototype = Dog.prototype语句将其设置成了Dog函数的prototype的值value;

让我们顺着刚刚说的流程       ,看看let newTom = new Cat()的执行过程:

先创建字面量空对象; 然后赋值给newTom; 然后调用Cat()函数本身                     ,即newTom.name = "Tom"; 然后执行语句newTom.__proto__ = Cat.prototype              ,而Cat.prototype = Dog.prototype       ,所以newTom.__proto__ = Dog.prototype;

输出结果佐证了我们的执行过程                     ,函数newTom的原型正是Dog函数的属性prototype的值value              ,他们的constructor()构造器都指向了Dog函数本身,但是newTom.name的值依然是"Tom";

从上面前后两个输出结果也可以看出来                     ,最后一步的tom.__proto__ = Cat.prototype确实是复制而不是赋值                     ,否则在Cat.prototype = Dog.prototype语句之后,tom.__proto__ = Cat.prototype = Dog.prototype了              ,但是输出结果表面并没有改变;

现在我们已经明白了函数对象的原型为什么是这个样子的                     ,也明白了函数对象的constructor()构造器指向了构造函数本身;

现在让我们像下面这样       ,使用一下函数对象的constructor()构造器吧:

看上面的代码              ,我们现在已经知道let tom = new Cat()的时候都发生了什么                     ,也知道此时tom的原型的constructor()构造器指向的是Dog函数;

所以let spike = new tom.constructor()这句代码       ,当tom去自己的属性里没有找到constructor()方法的时候       ,就去原型里面去找                     ,于是找到了指向Dog函数的constructor()构造器              ,所以这句代码就等于let spike = new Dog();

通过这段代码       ,好好体会一下函数对象的构造器吧                     。

构造函数和普通函数的区别

其实从技术上来讲                     ,构造函数和普通函数没有区别;

只是默认构造函数采用大驼峰命名法              ,并通过new操作符去创建一个函数对象;

new.target

我们怎样去判断一个函数的调用是普通调用,还是new操作符调用的呢?

如上所示                     ,通过new.target                     ,可以判断该函数是被普通调用的还是通过new关键字调用的;

构造函数的返回值

构造函数从技术上说,就是一个普通函数              ,所以当然也可能有return返回值(通常构造函数于情于理都是不会有return语句的);

之前说过new Function()的时候的具体流程                     ,我们来看一下:

先创建一个字面量空对象;

将空对象赋值给tom;

执行Cat()函数       ,让tom有了属性name;

但是Cat()函数有return语句              ,返回了一个空对象{}                     ,由tom接收了       ,也就是说tom被覆盖赋值了;

所以最后tom指向的是return语句的空对象       ,而不是最开始创建的空对象;

字面量对象的原型

new Object()的时候发生了什么

我们刚刚说了new Function()创建函数对象的时候                     ,具体发生了什么              ,现在来看看创建类对象的时候       ,具体发生了什么;

以Object为例                     ,因为它是一个类              ,是JS其他所有类的祖先,这一点与Java类似;

我们先看一下Object的prototype属性吧                     ,是的                     ,类和函数一样,也有这个属性(注意              ,是类有这个属性                     ,而不是类的实例即对象有这个属性);

看上图       ,是不是很眼熟              ,这不就是字面量对象的原型吗?

是的                     ,如上图所示       ,就是它;

还记得原型链吧       ,那么这个原型对象还有原型吗?

如上所示                     ,没有了              ,指向null了       ,看样子我们已经走到了原型链的原点了                     ,为了方便              ,我们就称呼Object.prototype为原始原型吧;

看看它的特性吧:

和函数的prototype属性的特性,如出一辙                     ,但是注意                     ,它的writable属性是false了,这意味着我们再也无法对这个属性做任何操作了;

这是当然              ,它可是所有类的祖先                     ,怎么能随意更改呢;

这下我们就能明白new ClassName()的时候大概流程是什么样子了;

以let obj = {}为例(其实就是let obj = new Object()):

先调用Objecet.prototype属性的特性[[Prototype]]里面的constructor()构造器(不再继续深究这个构造器了)       ,创建一个字面量空对象              ,当然此时这个对象的隐藏属性[[Prototype]]也都已经存在了; 然后将这个对象赋值给obj                     ,即obj引用了这对象       ,同时this指针也就指向了obj; 然后执行构造方法Object()本身的语句       ,就不再进一步去研究这个构造方法了                     ,总之此时obj已经是一个有着很多内置方法的字面量对象了; 然后将Object.prototype属性值value              ,复制给obj的隐藏属性[[Prototype]]       ,即obj.__proto__ = Object.prototype;

注意                     ,其实流程不完全是上面这样子              ,与构造函数的流程还有一点点区别,主要是第三步                     ,还有一个构造器的执行                     ,这和类的继承有关系,详细的在后面new className()的时候发生了什么里面具体说明;

更改原始原型

我们刚刚说了              ,Object.prototype属性的所有特性都是false                     ,意味着我们对这个属性无法再做任何操作了;

这只是再说       ,我们不能对其本身做任何删改的操作了              ,但是它本身依然是一个对象                     ,这意味着我们可以正常的向其添加属性和方法;

如上图所示       ,我们向Object.prototype属性对象里添加了hello()方法       ,并且由obj对象通过原型调用了这个方法;

类对象的原型

我们已经了解了函数对象的原型                     ,和原始原型              ,再来看看类对象的原型;

我们把这三种放一起做个比较吧:

我们自定义了类classA       ,自定义了函数functionA                     ,并创建了类对象clsA和函数对象funcA              ,以及字面量对象;

可以看出,类对象与函数对象的原型的形式                     ,是一致的                     ,只是各自原型里的constructor()指向各自的类/函数,即红框部分不同;

而他们的原型的原型则是一致的              ,和字面量对象的原型一样                     ,都指向了原始原型       ,即绿框部分相同;

上面的输出结果佐证了这一点;

从这也可以看出来              ,其他类都是继承自原始类Object的                     ,只是原型链的长短罢了       ,最终都可以溯源到原始类Object;

很显然       ,类与构造函数                     ,很类似;

类与构造函数的区别

尽管类对象和函数对象有相似的原型              ,但是不代表类与构造函数就完全一样了       ,他们之间的区别还是很大的:

类型不同                     ,定义形式不同

类名后不需要括号              ,构造函数名后需要加括号;

类的方法声明形式和构造函数的方法不一样;

打印类和构造函数,类前的类型是class                     ,构造函数前的类型是f                     ,即function;

注意,不能使用typeof操作符              ,它会认为类和构造函数都是function

prototype不一样

如上所示                     ,类的方法       ,会成为prototype的方法              ,但是构造函数的方法不会成为prototype的方法;

也即构造函数的prototype始终由constructor()和原始原型组成                     ,函数对象无法通过原型去调用在构造函数里定义的方法;

函数对象如果想要调用method1()方法       ,就不能写成let method1 = function(){}       ,而是this.method1 = function(){}                     ,将其变为函数对象自己的方法;

prototype的特性不一样

类的prototype是不可写的              ,但是构造函数的prototype是可写的;

方法的特性不一样

由于函数对象不能通过原型继承方法       ,这里只展示类的方法的特性                     ,如上所示              ,类的方法,是不可枚举的                     ,也即不会被for-in语法遍历到;

模式不同

由于类是后来才有的概念                     ,所以类总是使用严格模式,即不需要显示使用use strict              ,类总是在严格模式下执行;

而构造函数则不同                     ,默认是普通模式       ,需要显示使用use strict才会在严格模式下执行;

[[IsClassConstructor]]

类有隐藏属性[[IsClassConstructor]]              ,其值为true;

这要求必须使用new关键字去调用它                     ,像普通函数一样调用会出错:

但是很显然       ,构造函数本身就是一个函数       ,是可以像普通函数一样去调用的;

构造器constructor

由于函数对象不能通过原型继承方法                     ,所以无法自定义构造器;

但是类对象可以继承啊              ,所以可以自定义构造器并在new的时候调用;

从图上可以看出       ,我们是无法去自定义构造函数的构造器的                     ,它依然还是按照我们所说的流程去创建函数对象的;

我们现在看看              ,类自定义构造器,是怎么按照我们的流程去创建类对象的:

先调用classA.prototype的特性[[Prototype]]里的构造器去创建一个字面量空对象;

将空对象赋值给变量clsA;

然后执行构造方法classA()本身的语句;

首先添加了属性outterName;

然后又遇到了constructor()方法(注意该构造器与classA.prototype.constructor不是同一个东西)                     ,于是又执行了这个构造器的语句                     ,添加了属性innerName;

由此我们可以得出,类在创建类对象的时候              ,流程依然是我们所述的流程;

但是在遇到类里面的同名方法constructor()时候                     ,不会将其作为原型方法       ,而是会立即运行该构造器;

另外              ,像outterName这样的属性                     ,不会成为prototype的属性       ,也就是说       ,类只有定义的方法(除了constructor构造器)会进入prototype的属性                     ,成为原型被继承;

new className()的时候发生了什么

上面刚刚描述了类自定义构造器之后              ,创建对象是一个什么样的流程;

现在来仔细理解一下类的构造器       ,事实上                     ,如果我们不显式自定义构造器              ,类也会默认提供一个下面这样的构造器:

constructor() { super(); }

这里的super()实际上就是在调用其父类的构造方法(注意不是指父类的构造器constructor(),而是指父类自身);

用代码来验证一下吧:

我们先来看一下let c = new classC()的时候                     ,具体流程是什么样的吧:

首先调用classC.prototype属性的特性[[Prototype]](它总是指向原始原型)                     ,创建一个字面量空对象; 然后将其赋值给变量c; 然后执行构造方法classC()的语句,通常会有添加对象的属性和方法的语句              ,这里没有; 接着查看是否显式声明了constructor()构造器(如果没有就提供一个默认的构造器)                     ,这里有       ,于是立即执行这个构造器; 首先是super()              ,实际上就是执行构造函数classA()的语句                     ,于是添加了属性nameA; 然后是this.nameB = C       ,于是添加了属性nameC; 最后       ,将classC.prototype的value值                     ,复制给c的隐藏属性[[Prototype]]              ,即c.__proto__ = classC.prototype;

整个完整流程如上所示;

现在来试着对着流程看看let b = new classB()吧:

首先创建字面量空对象; 赋值给变量b; 执行classB()的语句       ,添加了属性nameB; 没有构造器                     ,提供默认的构造器              ,执行super()即执行classA()的语句,于是添加了属性nameA; 最后                     ,复制b的原型为classB.prototype的value值;

输出结果也验证了我们所说的;

操作原型的现代方法

之前已经说过                     ,通过__proto__属性去操作原型的方法,是历史的过时的方法              ,实际上并不推荐;

现代JS有以下方法                     ,供我们去操作原型:

Object.getPrototypeOf(obj)

此方法       ,返回对象obj的隐藏属性[[Prototype]];

Object.setPrototypeOf(obj, proto)

此方法              ,将对象obj的隐藏属性[[Prototype]]指向新的对象proto;

Object.create(proto, descriptors)

此方法                     ,创建一个空对象       ,并将其隐藏属性[[Prototype]]指向proto;

同时       ,可选参数descriptors可以给空对象添加属性                     ,如下所示:

原型链与继承

现在应该已经理解了原型是一个什么样的概念              ,以及如何去访问原型;

正如继承有儿子继承父亲       ,父亲继承爷爷一样                     ,有这样一个往上溯源的关系              ,原型也可以这样往上溯源,这就是原型链的概念;

用代码去理解一下吧:

我们定义了三个对象A/B/C                     ,并且设置C的原型是B                     ,B的原型是A;

读取C.nameA的时候,首先在C自己的属性里去找              ,没有找到;

于是去原型B的属性里去找                     ,没有找到;

再去B的原型A的属性里去找       ,找到并输出;

可以看C展开的一层层结构              ,可以很清晰的看到原型链的存在;

由此也可以看出                     ,JS是单继承的       ,同Java一致;

但是正常的继承       ,肯定不是这样手动去设置对象的原型的                     ,而是自动去设置的;

在JS中              ,继承的关键字也是extends       ,也是描述类的父子关系的;

上面代码                     ,classC继承classB              ,而classB继承classA;

所以classC的对象,继承了他们的属性                     ,便有了三个属性nameA/nameB/nameC                     ,这也说明,属性是不放在原型里的              ,而是会在创建对象的时候                     ,直接成为classC的属性;

classC的原型       ,有一个属性一个方法              ,方法是constructor()构造器指向自己                     ,属性是另一个原型;

注意       ,打印出来的原型后面标注的classX       ,原型指的是对象                     ,不是类              ,所以classC的原型不是指classB这个类本身       ,而是指其来源于classB;

紫色框:对象c的原型                     ,即c.__proto__ == classC.prototype;

橘色框:classB.prototype              ,即对象c的原型的原型c.__proto__.__proto__ == classB.prototype;

绿色框:classA.prototype,即对象c的原型的原型的原型c.__proto__.__proto__.__proto__ == classA.prototype;

红色框:Object.prototype                     ,也即原始原型c.__proto__.__proto__.__proto__.__proto__ == Object.prototype;

这是一条完整的原型链                     ,从中也能看出继承是什么样的一个形式;

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
二叉树左右子树交换利用什么遍历算法(二叉树交换左右子树递归以及非递归算法)