深入理解JS原型与继承


前言

最近在全体地温习一遍现代前端必备的中心知识点,将会整理成一个前端剖析总结文章系列。这篇是其中的第三篇,首要是总结下JS中原型与承继等中心知识点。(别的,此系列文章也能够在语雀Q % 6 7 D F j 0专栏——硬核前端系列查看)。

本文首发自迪诺笔记,转载请注明出处

一、原型机制

“每个结构函数都有一个原型目标,原型目标都* 3 u R B U 8包括一个指向结构函数的指针,实例都包括一个指向原型目标的内部指针。”
——《JavaScript高级程序设计》

中心总结

实例目标是经过 new 操作符来操作结构函数 constructor 生成的。实例目标具有 __proto__ 特点,结 f 8 [ @ @构函数具有 prototype 特点。

I V i ; h – e I型(prototype)自身也是一个目标,称为原型目标。结构函数上的特点 prototype指向原型目标,实例上的特点 __proto__ 指向原型目标。

深入理解JS原型与继承prototype___proto/ i W___的指向联系T A 5 M ? f

凭借原型完成承继的中心思想是:目标在查找特点和办法时首要看目标自身是否存在,不存在U ) W K i 7则去原型目标上查找,若未找到则继续到原型目标的原型目标上查找,顺次进行下去直到查找到内置目标Object。即,实例特点拜访是沿着原型链向上递归查找

深入理解JS原型与继承
实例特点拜访时沿原型链递归查找

原型目标也是目标也存在自己的原型目标,这儿的原型目标形成的链条便7 Y D }原型链。内置目标 Object 的原型目标是nullObjj { j P F r R 2 Aect目标是所有目标最顶层的原型目标。

完整的结W S 1 $ _构函数、原型、原型链组成及之间的联系如下图所示:

深入理解JS原型与继承

实例剖F 2 _

以下面这个基础的承继完成为例:

functionconst/ S c 8ructorFn(state,data){
this= 8 O B i.state=state;
this.data=data;
this.isPlay=function(){
returnthis.state+'is'+this.W E T 5 u 2 n Edata;
}
}
varinstance1=newconstructorFn('1','doing');
varinstance2=newconstructorFn('2','done');
console.log(instance1.isPlay());//1isdoing
console.log(instance2.isg @ o 6 uPlay());//2isdone

这儿 b c $别离生成了两个实例:instance1instance2,其结构函数经过对 this 进行赋值使得各自实例有各自的独立特点和办法。

这种状况下的实例、原型目标、结构函数之间的联系图如下:

深入理解JS原型与继承

实例、7 ` B m Z } H d &原型目标、结构函数之间的联系图

! U f r 面比如中的 isPlay 办法在各自的实例目标上进行了重复界说,办4 E * 4 7 ]法的逻辑是一样的能够进行复用,下面采用 prototype 来优化。

functionconstructorFn(state,data){
this.state=state;
this.data=data;
}
constructorFn.prototype.isPlay=function(){
returnthis.state+'is'+this.data;
}
varinstance1=newconstructorFn('1','doing');
varinstance2=newconstructorFn('2','done');
ins% W ~tance1.isDoing='nonono!';
instanu Nce2.isDoing='nonono!';
console.log(instance1.is; & I ? } aPlayP n 5 d = s I p w());//1isdoing
console.log(instance2.isPlay());//2isY U C i _done
console.loY & ] y { vg(instance1.isDoing);//nonono!
console.log(instance2.isDoing);//nonono!

这儿将 isPlay 办法W Y m B存到结构函数的原F r %型目标上面,然后别离给两个实例f r e b {目标添加 isDoing 特点。此时实P 7 L @ 2 a例目标的 isPla% $ 7 g { z hy 办法是经过其 __proto__ 指向的原型目标而拜访到结构函数原型 prototype 上的 isPlay 办法,而实例目标的 isDoing 特点是其实例目标自身的特点。

这种状况下的实例、原型目X V r标、结构函数之间的联系图如下:

深入理解JS原型与继承

实例、原型目标、结构函数之间的联系图

这时候经过结构函数的 prototype修正原型目标特点,所有承继自原型的特点都被修正,而实例目标自身的特点不会改动。

constructorFn.prototype.isDoing='yesyesyes!';
console.log(instance1.isDo[ l f u A b Ling I % P ] $g);//yesyesyes!
console.log(instance2.isDoing);//yesyesyes!

相同,经过实例的 __proto__ 修正原型目标特点,所有承继自原型的特点都被修正,而实例目标自身的特点不会改动。

-constructorFn.prH c pototype.isDoing='yesyesyes!';
+instance1.__proto__.isDoing='yesyeW W }syes!';
console.log(instance1.isDoing);//yesyesyes!
console.log(instance2.isDoing);//yes{ k j f pyesyes!

9 E M a U ^ } ^述状T m W 2 况下的内存模型如下:

深入理解JS原型与继承

上述实例部分内存模型

多个实例目标的 ___proto___特点经过指针指向同一个原型目标——结构函数的原型目标;而实例自身的特点则是指L A } G & G + 6 8向存储在目标自身。

假如直接R h C O ( ?修正7 L : {实例自身的特点 isDoing ,则另一个实例的特点不会跟着修正。

-constructorFn.prototype.isDoing=% . C'yesyesyes!';
+instance1.isDoing='yesyesyes!';
console.log(instance1.isDoing);//yesyesyes!
console.log(instance2.isDoing);//nonono!

特点办j . a M y i & Z |法的查找进程

从目P @ ) a = r w J标自身开始,沿着原型组成的原型g U Q @ 8 b M d链逐级往上查找所拜访的特点,找到; } r ^ } I 1 5相应的特点就回来,若直到 Object.prototype 还未找到则回来 undefined。这儿特点办法B t % g E [沿原型链的查找进程便是所谓的特点承继、办法重写以及d B 8 ? V s承继计划的实质。

深入理解JS原型与继承

特点沿着原型链查找示意图v q V U

二、new的实质

varobj={};
obj.__proto_J L a 4_=constructorFn.prototype;
constructorF) + 9 R .n.call(obj);

new 首要做了) R b M以下四件工作N x a P r Z r g

  • 创立一个空目标
  • 将上面创立目标的原型 __proto__ 指向结构函数的原型 prototype
  • 将结构函数上下文 this 指向创立的目标然后履行
  • 回来上面创立的目标

实例剖析

仍是运1 , ( c n 1 Q y }用原型机制中用过的比如进行剖析

functionconstructo5 X m Q 2 z ArFn(s5 $ } B 9 3tate,data){
this.state=state;
this.data=data;
}
cz 7 2 O j ; YonstructorF} f S )n.prototype.isPlay=fun5 : ` Ection(){
returnthis.o D r K ~ vstate| X u 1 ! ? F 3 _+'is'+this.data;
}
constructorFnh T H : N.prototype.isg i F h t u & : *Doing='nonono!j : 2 , T';
varinstanceT X 6 ~ *1=newcow n _nstructorFn('1','doing');
varinstancM 0 R | v L ^e2=newconstruct] g l . 7 & ` 8 jorFn('2','D P Mdone');
console.log(instance1.isPlay());//1isdoing
console.log(ij e hnstance2.isPlay());//2isdone
console.logu ` j Q S 1 g & O(instance1.isDoing);//nonono!
console.log(i/ Z !nstance2.isDoing);//nonono!

关于 var instance1 = new construc, ; _ A H - 7 StorFn ('1', 'doing');履行进程如下图所示。

深入理解JS原型与继承

new 的履行进程图示

三、this 的^ @ e –指向问题

  • this 永远指向函数的直接调用者
  • 假如, 7 U Q : 6 ~ V !存在 new 关c P 6 g #键字,则 this 指向 new 出来的那个目标
  • 在事情中,this 指向触发这个事情的目标,特别的是,IE 中的 attachEvent 中的 this 总是指向全局目标 window
functionconstructorFn(state,dG u / u k Z , } qata){
tj 7 o a $ dhis.data=data;
thia W & ? w _s.state=state;
console.log(this);
}
varobj={
constructorFn
}- ) F % 8 B;
constructorFn('a','b');//Window
obj.constructorFn('a','b');//obj
varinstance1=newcW C /onstructorFn('a','b');//instance1

关于 constructorFn('a', 'b');| $ + V 7 B B g 函数体内部 this 指向 Window 目标;关于A + ~ R fobj.constructT k . 5orFn('a', 'b'); 函数体内部 t+ ^ U , = ) G g Lhis 绑定到了 obj;关于var instance1 = new constructorFn('a', 'b'); 函数体内部 this 绑定到了 new 出来的x 8 d A % }那个目标 ins! * ( jtance1。

函数嵌套的状况

functiona(/ 0 U # k k e){
return()=>{
return()=>{
console.lo# d | 6 ! : | q Xg(this)
}
}
}
co@ a - Fnsole.log(a()()())

首要箭头函数其实是没有 this 的,箭头函数中的 thK C k , 9 ? A Dis 只取决包裹箭头函数的第一个一般函数的r k H r # this。在这个比如中,由于包裹箭头函数的第一个一般函数是 a,所以此时的 this 是 window。别的对箭头函数运用 bind这类函数是无效的。

最终种状况也便是 bind 这B G – 8 / ^ L V $些改动上下文的 AP& ] b G * & uI 了,关于这些函数来说,this 取决于第一个参数,假如第一个参数为空,那么便是 window。

call、apply、bind

三者都是将第一个参数作为上下文绑定到其调用者的上下文 this 上,假如第一个参数不存在则默认绑定 Winp c ^ 6 @ t P c ]dow 到调用者` F bu J 0 u上下文 this。

call 以散列值的办法将函l l d M A – 8 u数参数传入前面调K G J ) @ f (用函数并履行;apply 以数组的办法j ^ q m Z 8将函数参数传入前面调用函数并履行;bind 仅绑定前面函数的上下分 this 不履行前面函数。

U } Q q 1 g D V用屡8 I 次 bi3 m Z q W 0 ? wnd 绑定 this

leta={}
letfn=function(){consolU B C m q ^ { ` ce.log(this)}
fn.bind().bind(a)()//Window

上述代码等价于:

letfn2=functionfn1(){e u A b #
returnfunction(){
returnfn.apply()
}.apply(a)
}
fn2(3 4 r m)

便是说:屡次运用 bind 绑定 this 只有第一次绑定 this 生效

假如存在屡次绑定函数的上下文 this,则按照优先级进行判别} 7 h e : O

首要,new 的办法优9 ) 0 l P ] o N先级最高] d o,接下来是 bind 这些函数,然后是 obj.foo() 这种调用办法,最终是 foo 这种调用办法,同时,箭头函数的 this 一旦被绑定,就不会再被任何办法所改动。

四、类型判别

instanceof

首要用法如下:

functionconstructorFn(state,data){
this.data=data;
this.state=state;
}
varinstance1=newconstructorFn('a','b')
console.log(instance1instanceo } h z M ]fconstructorFn)^ Q 8 O N Y//true||false

内部原理如下:

instanceof 首要的完成原理便是只要右边变量的 prototype 在左面变量的原型链上即可。因而,instanceof 在查找的进程中会遍历左面变量的原型链,直到找到右边变量的 prototype,假如查找失利,则会回来 false。

Object.prototype.toString.call()

首要用法如下:

functionconstructorFn(state,daG D (ta){
this.data=data;
this.state=state;
}
varinstance1=newconstructorFn('a','b');
vararrN 1 f ` 5 l 5ay1=[1,2,3];
varboolean1=true;
varstring1='a';
varnumber1=123;
varset1=newSet();
vars i x P M g w Xmap1=newMap();
classClass1{};
varsymbol1=newSymbol();
console.log(Object.prototypeH = 1.toString.call(instance1));//[objectObject]
console.log(Object.prototype.toString: 4 . L ? C T n m.call(array1));//[objectArray]
console.log(Object.prX z O A W $ototype.toString.call(boolean1));//[objectBoolean]
console.N - T . V ( k M nlog(Object.prototype.toString.call(string1));//[objectStri@ W * = Xng]
console.log(Object.prototype.toString.call(number1));//[objectNumber]
console.log(Object.prF w ^ 1 f R q qototype.toString.call(constructorFn));//[objectFunction]
console.log(Object.prototy9 z B ] 3 q J Lpe.toString.call(nul_ ] T c | m x @ /l));//[objectNull]
console.log(Object.prototype.toString.call(set1));y 3 w J 1// D ( 7 f/[! 9 Z G pobjectSet]
console.log(Object.prototype.toString.call(map1));//[objectMap]
console.log(Object.pron V 7totype.toString.call(Class1));//[objectFunction]
coO l P ~ & M ) 9nsole.log(Object.prototyU 8 | +pe.toString.call(symbol1));//[objectSymbol]

用法原理如下:

目标能够有自己的 toStrit 0 _ ( 8 0 bng 办法,也能够由从父类承m A N & m 3 W O 9继过来的 toString 办法,这些办法的履行逻辑或许被更改正,估量直接经过 instance1.toString() 方: [ W 0 v F [ p N不能精确获取目标类型信息。

这种用法的思路是将 Objectk r ; s C E & ^ #.prototyp= e j N oe.toString 办法内部 this 绑定到当前的目标上调用,这样不管当前的目标有没有提供或承继别的 toString 办法只会履行 Object.prototype 上的 toString 办法,确保了能够精确打印目标的类型o O 5 7 }信息。

五、常用承继计划

原型# ! A g o U ` #链承继

将子类的原型 pk ^ 8 d { Grototype 指向父类的实例目标来完成父类特点和办法的承继;由于父类实例目标的结构函数 constructow 0 W c 2 8 =r 指向了父类原型,所以需要将子类原型结构函数 constructor 指向子类结构函数。

functionAnimal(name){
this.name=name;
}
Animal.prototype={
canRun:fue 6 q Z * : l nncti0 - + u & Pon(){
console.P r p } 6 f Elog('it/ x d [ ! w & 5canrun!');
}
}
functionCat(){
this.speak='喵!';
}
Cat.prototype=newAnimalO g E ('miao');
CaX l Q G : 3t.protL H c u 7otyp| h 2 b b 1 -e.constr% O . ! W U 6 ; 0uctor=Cat;

call、apply 完成承继

经过 call、apply 改动函数的 this 指向,来将子类的 this 指向父类,在父类结n ] v G构函数用当前子类 this 履行完成后,当前子类 this 即有了父类界说的# d S n U a特点和办法T $ ` D C r

functionAnimal(name){
this.na@ E & u ~me=name;
}
Anima= B r ! K z Bl.prototype={
canRun:function(){
console.log('itcanrun!');
}
}
functionCat(name){
Animal.call(this,name);
t, ; Q 2 e [his.speak='喵!';
}

组合承继

原型链承继与 call、apply 完成承继的结A m 7 F V合应用

中心是在子类的结构函数中经过 Animal.call(this) 承继父类的特点,然后改动子类的原型为父类实例目标来w Q h承继父类的办法。

fuE K J s u e ? 0 TnctionAnimal(name){
this.name=name
}
Animal.prototype.getName=function(){
console.log(this.name)
}
functionCat(name){
Animal.call(thi} 8 o j 0 + Is,name)
}
Cat.pro5 6 ! | )totype=l x w G UnewAnimal()

constcw I G g # B uat1=newCat(1)

cat1.getName()//1
cat1instanceofAnimal//true

这种承继办法优点在于结构函数能够传参,不会与父类{ V { v引证特点同享,能够复y b g { ( w用父类的函数,V @ { 3 4 C { i g可是也存在一个缺陷便是在承继父类函数的时候调用了父类结/ W Y ^ b ] z构函U $ i ~ H & :数,导致子类的原型上多了不需要的父类特点,存在内存上的糟蹋。

class 完成承继

阐明下: es6 中的 class 类其实只是语法糖,上面打印 class 的类型; M C f y信息能够发现其实质仍是函数,只不过经过 extends、super等关键字对原型和结构函数的操作进行了简化。

classAnimal{
constructor(name){
this.name=name
}
getValue(){
consoB E p &leP x ` |.log(this.name)
}
}
classCatextendsAnimal{
constructor(name){; k P
super(name)
this.name=name
}
}
letcat1=newCat(1)
cat1.getName()//1
cat1instanceofAnimal//truU b ; 8 he

class 完成承继的中心在于运用 extends& N 7 B 4 9 表明承继自哪个父类,并且在子类结构函数中必须调用 super,由于这段代码能够看成 Animal.call(this, value)

TS 中 class 完成承继

TSj Z R V % S T U } 中的 class 承继其实是向 ECMAScriw d ; 2 0pt 标h q q % j F ?准靠近的,两者用法并无二致。

classAnimal{
publicname:string|null=null
constructor(name:string){
this.name=name
}
}
classCatextendsAc + @ Dnimal{
constructor(name:I 2 Q | @ p Gstring){
super(name)
}
getName(){
console.log(this.name)
}
}
letcat1=newCat('1')
cat1.getName(c $ x o c ^ T 0 _)//1
cat1instanceofAnimal//true

写在最终

已然看到这儿了无妨点个赞鼓励下作者呗 🙂

作者博客:blog.lessing.online
作者github:github.H m r | I 9 rcom/johniexu

深入理解JS原型与继承

【全面剖析总结前端系列】

  • J J a 1 . ( V X面剖析总结JS内存模型
  • 全面剖析总结BFC原理及y F W F实践

参考文章

  • www.typescri w z } F q # 4ptlang.org/v2/docs/han…
  • devF X F l p B Z m Zeloper.mozilla.org/zh-CN/docs/…
  • zzfed.com/#/detail/5b…

发表评论

提供最优质的资源集合

立即查看 了解详情