理论汇总前端开发那些事儿

js原型和原型链(用代码理解代码)

2021-04-08  本文已影响0人  奔跑的痕迹

众所周知js原型及原型链是很多开发者的一个疼点(我也不例外),我也曾多次被问起,也问过不少其他人,如果在自己没有真正的去实践和理解过;那么突然之间要去用最简单的话语进行概述还真不是一件容易的事情;
其实工作中看似神秘的js原型也并不是那么难以理解,最终其目的无非是为了达到方法、属性共享代码重用的目的;在我所了解的编程语言中都会用到object这个最顶层对象,然而生命的法则最终是从无到有,就如同世界先有鸡还是先有蛋一样。

一、 Class

先来看一个简单的es6的例子吧

假如我们定义一个类


/*用于全局状态管理*/
class State{}

然后在某个页面为其State.draw = true 然后在项目的任何地方都能使用State.draw来获取其值;而且当你一个地方更改,其它页面也会同时获取到更改后的值


class State{
    constructor(draw){
        this.draw = draw;
    }
}

然而使用对象属性方式就不一样了;


var state1 = new State('bar');
var state2 = new State('pie');
state2.draw = 'hello'; // 不管state2 的draw 怎么改都不会影响到 state1 的属性值

为啥扯上了这么一大圈,这个和原型好像没关系,其实是有的,State.draw 就等同于 State.prototype.draw = true
this.draw = draw;就等同于 普通的函数赋值

 function Sate(){
       this.draw = draw;
}

二、普通 function


function Sate(draw){
    this.draw = draw;
}
var state1 = new State('bar');
var state2 = new State('pie');
state2.draw = 'hello'; // 不管state2 的draw 怎么改都不会影响到 state1 的属性值

使用node运行测试

C:\Users\Lenovo>node
> function State(draw){
... this.draw = draw;
... }
undefined
> var state1 = new State('bar');
undefined
> var state2 = new State('pie');
undefined
> console.log(state1.draw);
bar
undefined
> console.log(state2.draw);
pie
undefined
> state2.draw = 'circle';
'circle'
> console.log(state1.draw);
bar
undefined
> console.log(state2.draw);
circle
undefined
>                                                                                                                                 

柑橘和第一个Class案例是有点相同了,在题中state1 和 state2 完全是两个不同的对象,他们各自维护自己的属性和方法和其他人没得关系

> console.log(state1);
State { draw: 'bar' }
undefined
> console.log(state2);
State { draw: 'pie' }
undefined
>   

三 、原型(prototype)

function State(){}
State.prototype.draw = 'pie';

var state1 = new State();
var state2 = new State();

console.log(state1.draw);
console.log(state2.draw);

state2.draw = 'bar'; 

console.log(state1.draw);
console.log(state2.draw);

使用node运行测试

C:\Users\Lenovo>node
> function State(){}
undefined
> State.prototype.draw = 'pie';
'pie'
> var state1 = new State();
undefined
> var state2 = new State();
undefined
> console.log(state1.draw);
pie
undefined
> console.log(state2.draw);
pie
undefined
> console.log(state1); // 看:打印的 state1和 state2 是一个空的 State {} 那上面却能正常打印draw
State {}
undefined
> console.log(state2);
State {}
undefined
>     

看:打印的 state1和 state2 是一个空的 State {} 那上面却能正常打印draw;其实这里的state1 和state2 只是State 的一个引用,在实例本身是没有任何属性的,但是他可以通过自身的__proto__关联到Sate这个构造函数


> console.log(state1.__proto__);
State { draw: 'pie' }
undefined
> console.log(state2.__proto__);
State { draw: 'pie' }
undefined
>   
   

而注意的是这个属性并没有直接关联构造函数;只是关联了构造函数的prototype属性(原型对象)


> console.log(State);
[Function: State]
undefined
> console.log(State.prototype);
State { draw: 'pie' }
undefined
>               

从打印可以得出 State.prototype 就等同于了state2.__proto__

so state2.proto == State.prototype


> console.log(state2.__proto__ == State.prototype);
true
undefined
>      

四、原型的用途

然而说了半天,这个原型的用处究竟在哪里呢?
其实他的主要用途就是 继承(extends),代码重用
如:


function State(){}
State.prototype.draw = 'pie';
State.prototype.drawGraphic  = function(){
    let height = 100;
    let width = 100;
    // draw a graphics...
    console.log(`Draw a graphic with a height of ${height}px and a width of ${width}px`);
}

上面定义了绘图的构造函数和方法,现在就可以开始使用了


var state1 = new State();
var state2 = new State();
var state3 = new State();
var state4 = new State();

// 使用实例化的四个实例,调用drawGraphic进行图形绘制
state1.drawGraphic();
state2.drawGraphic();
state3.drawGraphic();
state4.drawGraphic();

如上:实例化的四个实例,调用drawGraphic进行图形绘制,
然而他们并没有去创建各自的方法,只是直接从原型引用了State 上的drawGraphic,这样就极大的节约了开销;
如果不使用原型的方式,这个四个对象将会创建四个对应的方法,这就是一种极大浪费。

如果不明白可以来看看看开始的例子

4.1 普通function, 实例化多个对象

C:\Users\Lenovo>node
> function State(draw){
... this.drawGraphic = function(){
..... let height = 100;
.....   let width = 100;
.....   // draw a graphics...
.....   console.log(`Draw a graphic with a height of ${height}px and a width of ${width}px`);
..... }
... };
undefined
> var state1 = new State('bar');
undefined
> var state2 = new State('pie');
undefined
> console.log(state1);
State { drawGraphic: [Function] }
undefined
> console.log(state2);
State { drawGraphic: [Function] }
undefined
>           

从运行可以看出每个new出来的实例都会创建属于自己的实例方法和属性

4.2 使用原型方式

只用从State 上面调用drawGraphic 方法,而不会自己再去创建,就同java 的继承一个意思,直接从父类继承方法,属性,然后使用。看:

C:\Users\Lenovo>node
> function State(){}
undefined
> State.prototype.drawGraphic  = function(){
...     let height = 100;
...     let width = 100;
...     // draw a graphics...
...     console.log(`Draw a graphic with a height of ${height}px and a width of ${width}px`);
... }
[Function]
> var state1 = new State();
undefined
> var state2 = new State();
undefined
> console.log(state1);
State {}
undefined
> console.log(state2);
State {}
undefined
> state1.drawGraphic();
Draw a graphic with a height of 100px and a width of 100px
undefined
> state2.drawGraphic();
Draw a graphic with a height of 100px and a width of 100px
undefined
>   

4.3 实例原型重写

那有的童鞋就会问了你这个只能调一个方法打印同样的图形,太死板了;其实不然,原型也支持重写(override)

改改刚才的案例

function State(){}
State.prototype.draw = 'pie';
State.prototype.drawGraphic  = function(){
    let height = 100;
    let width = 100;
    // draw a graphics...
    console.log(`Draw a ${this.draw} with a height of ${height}px and a width of ${width}px`);
}

使用node运行测试

C:\Users\Lenovo>node
> function State(){}
undefined
> State.prototype.draw = 'pie';
'pie'
> State.prototype.drawGraphic  = function(){
...     let height = 100;
...     let width = 100;
...     // draw a graphics...
...     console.log(`Draw a ${this.draw} with a height of ${height}px and a width of ${width}px`);
... };
[Function]
> var state1 = new State();
undefined
> var state2 = new State();
undefined
> var state3 = new State();
undefined
> state1.drawGraphic();
Draw a pie with a height of 100px and a width of 100px
undefined
> state2.drawGraphic();
Draw a pie with a height of 100px and a width of 100px
undefined
> state3.drawGraphic();
Draw a pie with a height of 100px and a width of 100px
undefined
> state2.draw = 'circle';
'circle'
> state1.drawGraphic();
Draw a pie with a height of 100px and a width of 100px
undefined
> state2.drawGraphic();
Draw a circle with a height of 100px and a width of 100px
undefined
> state3.drawGraphic();
Draw a pie with a height of 100px and a width of 100px
undefined
>   

看运行结果:state2将 draw 重新设置为了 circle ;再次调用打印 Draw a circle with a height of 100px and a width of 100px;然而他并没有影响到其他的实例(其实说白了就是在执行state2.draw = 'circle'; 在state2实例对象上新增了一个draw的属性)


为了能更好理解继承和重写:来用我们小学老师教我的语文解释一哈【

继承如同你父亲开了一家xxx公司,你就可以直接找财务开个20万,今天要去约个女朋友吃饭,然后财务一看原来是少爷呀,大笔一挥给你了,然后你再开上你父亲的法拉利愉快的约会去了。
当某一天你发现自己也该干一番事业了,于是开始模仿你父亲建起来了同样的公司和相同的经验模式,也许有人会想干嘛不直接把父亲公司改为自己的?
那肯定不行啦,因为那样老大,姥二,老三……不会把你打死呀。最后只好自己模拟了个和你父亲相同的xxx子公司,现在出去约会就直接叫自己财务开单了,然后开着自己的法拉利愉快的且。
假如哪天经验不当(女朋友太多,哈哈),又得去找你父亲的财务了,那就不一样了现在得明确指定是去父亲的财务那里(super.财务)还是自己的财务那里开支票;
否则你直接给你手下说去叫财务给我20万,他第一反应当然是去找你自己公司的财务了,啊哈哈。】


C:\Users\Lenovo>node
> console.log(state2);
State { draw: 'circle' }
undefined
> console.log(state2.__proto__);
State { draw: 'pie', drawGraphic: [Function] }
undefined
>         

如果上面啥子重写依然搞不清楚那可以把它看成(虽然不是太准确,为了理解还是可以的)
在执行state2.draw = 'circle';是为该state2实例对象新增了一个draw的属性,

那你可能会迷惑了打印是为啥会是circle 而不是pie呢,不是说好的state2 的原型指向State.prototype吗,
State中的draw并没有改变呀,其实问题在于js 属性查找机制(就近原则)

首先获取属性值会优先从自己对象上面查找,当对象没有该属性才会通过__proto__到原型对象上面去找那个属性;
假如该State.prototype原型对象上也没有该属性,他会再根据State.prototype.__proto__继续向上,直到Object;直达Object.protopyte.__proto__,最后如果还没有找到对于的属性;那就给你个undefined了

4.4 原型对象重写

那你说假如我就是想更改所有对象的draw咋办呢。当然有办法(你想翻天谁都拦不住,,哈哈)

> state2.__proto__.draw = 'trinagle';
'trinagle'
> console.log(state3.drawGraphic());
Draw a trinagle with a height of 100px and a width of 100px
undefined
undefined
> console.log(state2.drawGraphic());
Draw a circle with a height of 100px and a width of 100px
undefined
undefined
> console.log(state1.drawGraphic());
Draw a trinagle with a height of 100px and a width of 100px
undefined
undefined
>      

看到没有state2.__proto__.draw = 'trinagle'一执行,其他state1和state3就瞬间改变了打印为Draw a trinagle ...
state2没有遭更改因为最开始他就重写了一次(他自己有了自己的子公司)

五、 原型链

说了半天没讲到原型链,不地道,其实上面都已经出现过n次了,只是你没注意(世上最遥远的距离是:我就在你眼前你却不认识)哈哈扯远了。

想想一个实例对象和函数对象是怎么取得联系的,不就是通过__proto__这个属性(这个属性会在函数对象或是普通对象一降生就会自带而来)吗?

那就对了其实他作用就是建立实例与函数对象之间的一条链子简称原型链

注意这个__proto__ 是所有人都有(一视同仁);然而prototype这就就不一样了只有函数对象才具备(只有大佬才具备,哈哈)

C:\Users\Lenovo>node
> function State(){}
undefined
> var state1 = new State();
undefined
> console.log(State.prototype);
State {}
undefined
> console.log(state1.prototype);
undefined
undefined
>     

看到了吧。


5.1 函数对象

现在来揭开原型链的神秘面纱了
接着看

C:\Users\Lenovo>node
> function State(){}
undefined
> var state1 = new State();
undefined
> console.log(State.prototype);
State {}
undefined
> console.log(state1.__proto__);
State {}
undefined
> console.log(State.prototype == state1.__proto__);
true
undefined

从上得出State.prototype == state1.__proto__ 证明 State.prototype也就如同State的一个实例

佐证一下:每一个实例对象都会默认携带一个constructor属性指向其构造函数,那么原型对象为什么也会有一个constructor属性呢,其实他也是构造函数的一个实例

undefined
> console.log(State.prototype.constructor);
[Function: State]
undefined
> console.log(state1.constructor)
[Function: State]
undefined
> console.log(state1.constructor == State.prototype.constructor)
true
undefined
>        

ok State和他的实例之间的爱恨情仇算是基本清楚了吧

不清楚再简单画一哈:
State ----------prototype------------------->State.prototype
函数对象(State {})通过prototype属性指向它的原型对象(State.prototype

State.prototype ---------constructor----------------> State
state1---------------------constructor----------------> State
然而原型对象和实例对象都会有个一个constructor指向其构造函数([Function: State]
state1 -----------------__proto__·-----------------> State.prototype
实例对象会通过原型链属性__proto__指向其构造函数的原型对象


5.2 构造函数

因为State是通过new function来的所有他是一个构造函数,而State.prototype则是该State的一个实例对象,就是一个普通的函数State {},从运行可以看出

> console.log(State)
[Function: State]
> console.log(State.prototype);
State {}
> console.log(state1)
State {}

看看这个定一个匿名函数

> var fun3 = new Function();
undefined
> console.log(fun3);
[Function: anonymous]
undefined
>   

再回头去看看上面的 function State(){} 定义函数是否理解了呢

上面说State的实例对象就是一个普通对象,怎么理解,
在工作中是不是常常会var obj = {};这样来定义一个对象呢,应该都有过吧,这就是定于了一个普通对象,然而State.prototype的原型对象也是一个{}
这就和好的论证了他是一个普通对象

> var obj = {};
undefined
> console.log(obj.prototype);
undefined
undefined
> console.log(obj.__proto__);
{}
undefined
>   

这里的obj.prototype == undefined也充分证明其只是一个普通对象,只有函数对象才会有prototype

> console.log(State.prototype);
State {}
undefined
> console.log(State.prototype.__proto__);
{}

看到这个是否理解了上面说的【函数对象(State {})通过prototype属性指向它的原型对象(State.prototype)】

5.3 构造函数的由来

那么构造函数最终来之哪里呢

> console.log(State.__proto__);
[Function]
undefined
>       

他的原型对象是Function;那么就会问Function的原型对象又是谁呢?(Object)? no 他是他自己。。。

> console.log(Function.prototype);
[Function]
undefined
>                                                                                                                             

why?
再来回顾下
function State(){}的 State.prototypeState 的一个实例那就等同于
State.prototype = new State();

以此类推
Function.prototype 也是Function 的一个实例;那么
Function.prototype = new Function();

而 上面曾经定义匿名函数时提到使用new Function()创建的函数就是构造函数
那么Function.prototype 也是通过new Function()创建的,那么他是不是也该是个构造函数呢,也就是等于了他自己

那么Function的__proto__就很简单了,
Function.__proto__ == Function.prototype == Function

那最后Function.prototype 的原型链对象又指向了谁,不可能还是Function吧?
那倒不是了。那岂不成死循环了
所以它依然遵循了万物法则,一切皆从无中来

> console.log(Function.prototype.__proto__);
{}
undefined

也就是说

Function.prototype.__proto__ == {}
var obj = {};
var obj2 = new Object();

使用node运行测试

> var obj2 = new Object();
undefined
> console.log(obj2.prototype);
undefined
undefined
> console.log(obj2.__proto__);
{}
undefined
>    

是否圆满了所有对象都继承之Object的论题!

个人理解,有不正确之处,欢迎留言指出,共同学习一起进步

上一篇下一篇

猜你喜欢

热点阅读