JS面向对象——OOP

闭包

定义

  官方定义为,闭包即一个拥有许多变量和绑定了这些变量的环境的表达式(通常为函数)

原理

  实际上,不用解释那么费劲,闭包就的原理就是函数可以访问其外部的全局变量

  原本在函数外部访问不到函数里面的局部变量,为了可以访问函数内的局部变量,在函数中添加一个函数,并让函数的执行结果返回这个函数体

  接下来,把函数的执行结果赋值给一个全局变量,并再次执行,即可以让这个函数内的函数通过作用域链的方式,向上查找并访问到函数体内部的局部变量

 案例

  一个比较经典的例子,先把案例放上来,最后再放执行的结果

  

  执行的结果如下:

  

  整个执行过程解析如下:

  1.第一步var c = f1(),把函数f1的执行结果返回给全局变量c,其实就是函数f2,此时c就是函数f2的函数体

  2.由于在执行函数f1过程中,函数f1内部声明了一个全局变量nAdd(实际上是方法)和i,所以可以在执行完f1后访问到这两个变量

  3.执行函数c,也就是执行函数f2,由于函数f2是在f1中声明的,虽然f2中没有局部变量n,但可以通过作用域链,访问到其上一级的f1的局部变量n,返回999

  4.由于f1执行时生成了一个函数作用域,n是这个作用域中的,可以理解成这个作用域中的全局变量,而nAdd虽然是全局的,但是声明时候是在f1内,因此可以访问到f1内的局部变量n,可以通过执行n+1让该环境中的局部变量值发生改变,那么下一次访问n的时候就变成1000了

 用途

  利用闭包,在函数外读取函数内部的局部变量,并让函数内部的局部变量保存在内存中

优缺点

  优点:封装性强,可访问局部变量

  缺点:局部变量长时间占用内存,容易产生内存泄漏

 

封装

定义

  官方解释(先放个再说,至于看不看得懂不重要):把对象内部数据和操作细节隐藏,只对外提供一个对象的专门访问的接口

  很多语言中有关键字来实现封装

原理

  接着上面的定义,很遗憾的是,JS中,没有关键字来实现封装,倒是可以利用闭包来实现封装的效果(注意是效果)

案例

案例1

  先上案例的代码,具体的执行结果和解析在下方(养成个先思考再看答案的好习惯)

  注意这里绿色的注释部分和test的效果是一样的,一个是用了字面量的声明方式,一个是用了构造函数的方式声明test方法

  

  执行结果如下

  

  整个执行过程如下:

  1.这里用了闭包的方法,在demo这个函数中创建了局部变量n方法test,并让函数的执行结果返回test(标准的闭包)

  2.这样一来,用一个全局变量at承接demo的执行结果,就得到了test这个函数的本体

  3.此时执行at函数,由于at里没有n这个局部变量,沿着作用域链向上,在demo函数中找到了demo的局部变量n,让其加1后返回为2,改变了这个局部变量的值,如果再次执行at函数则返回3

案例2

  再来一个案例,不过和上面的案例有所不同,全局下的函数还用原型模式来添加了一个普通方法

  先放上代码

  

  执行结果如下

  

  整个执行过程如下:

  1.用了混合模式创建了A这个对象,接着构造了一个A的实例a

  2.创建了一个全局变量b,用来接收a里的xx方法,而xx方法是返回a里的_xx方法的,因此实际上b接收的是a里的_xx方法

  3.调用了函数b,实际上调用了a里的_xx方法,输出11******

  4.调用a的oth方法,这个方法是通过原型模式添加给a的原型A的,因此输出“普通方法

特权方法

  第一个案例中,test这个方法值得注意,它是整个demo函数中唯一可以访问到其内部的局部变量n的方法

  鉴于其特殊性,我们叫test方法为特权方法

  而第二个案例主要是想说明,封装不影响原型方式添加的普通方法的访问,例如这里我们仍然可以访问到oth这个方法

缺点

  封装占用了内存,不利于继承

 

继承

  在说明什么是继承之前,先引入两个基本概念:原型原型链

原型

  官方解释为:用prototype给对象添加属性和方法

  前面创建对象的原型模式里就提及了,通过对象.prototype来给某个指定的对象添加属性和方法

原型链

  那原型知道了,原型又是如何构成一条“链”的呢?

  JS创建对象时,新对象内部有一个__proto__内置属性,指向创建其函数对象的原型对象prototype

  我们不妨了解一下用构造函数方式创建一个对象的具体过程,如下图

  

  分析一下,整个创建student对象的过程如下:

  1.var student = {},也就是让student这个变量指向一个空的对象,此时student就成为了对象

  2.student.__proto__ = person.prototype,这也是最关键的一步,让student的__proto__这个内置属性指向创建其的函数对象person的原型对象prototype

  3.创建对象,也叫做初始化对象,用person.call(student),也就是把student传入person的call方法里,完成这个对象的创建

  可以这么理解整个过程:

  首先,诞生了student这个宝宝,这个宝宝是一片空白的,不过可以确定是个人(对象);

  接下来,student认出了他的爹person,用自己的内置属性__proto__和他爹的原型对象prototype连接上,以后就可以从爹爹那里学东西了(原型继承)

  最后,他爹费了九牛二虎之力,用自己的方法让student得到了最最基本的生存能力

  

  创建对象的过程知道了,接下来这个例子就好理解了

  

  执行结果如下:

  

  整个过程的解析如下:

  1.首先,用原型模式声明了对象person,这个person原型中有salary属性500),以及say方法天气不错

  2.接着,用原型模式声明了对象programmer,这个programmer原型中有salary属性1000),以及wcd方法明天天气也不错

     此外,创建了一个person的实例,programmer的原型为person,也就是programmer.prototype.__proto__ = person.prototype

  3.最后,构建了一个programmer的实例p

  所以,调用say方法过程如下:

  由于p里没有say方法,p在自己的__proto__属性里找say方法,也就是p.__proto__,即programmer.prototype里查找,但是这里也没有say方法,继续沿着programmer.prototype.__proto__查找,也就是person.prototype里查找say方法,这里发现了say方法,于是调用

  而调用wcd方法过程如下:

  由于p里没有wcd方法,p在自己的__proto__属性里找say方法,也就是p.__proto__,即programmer.prototype里查找,这里有wcd方法,所以调用这个方法

  p的salary属性调用过程也类似:

  由于p自身没有salary属性,会在自己的__proto__属性里找say属性,也就是p.__proto__,即programmer.prototype里查找,而这里有salary属性,属性值为1000,返回并输出1000,所以输出不是500

 

  当使用对象的方法时,先在当前对象中查找,如果有则使用,没有就查找当前对象的__proto__属性中是否有方法,有则调用,无则继续向上查找,如此层层往上“递归”,形成一条无形的链子即为原型链

  

原型继承

  通过原型方法创建新对象时,如果子对象没有修改/重新定义同名方法,原型中有的方法会继承到子对象中

  子对象使用方法时也会沿着原型链逐步向上查找,只有在最顶层父元素都没有该方法时才报错

  我们把原型链中的案例画一个图说明

  

  这下我们可以清楚看到,p、programmer和person因为彼此之间的原型的关系,构成了一条无形的原型链

  所以这里的person爷爷辈的,programmer父辈的,而p子辈

  当儿子没有家产时,可以名正言顺地从父亲那里继承家产,也就是父亲有啥儿子用啥(继承父一级的方法和属性

  如果父亲把家产变更了(例如家产败光了或者翻倍了),那就用父亲得到的家产,否则的话,父亲的家产就跟爷爷那里的一样,也就是实际上继承了爷爷的家产

  (也就是修改了方法或属性,子代用父辈的属性或方法)

  

  这里举一个例子说明子对象修改属性/方法时,后代是如何继承的

  先放上案例的代码

  

  执行结果如下

  

  sayhello方法的调用过程如下:

  1.对象s是student实例,调用s的sayhello方法时,会先在s对象自己内部找,发现没有,然后沿着s.__proto__找,也就是student.prototype查找sayhello方法,但是也没有

  2.不过,student的prototype是person的实例,这个实例里name为李四,age为18,继续查找的话,查的是student.prototype.__proto__,也就是person.prototype,在这里发现了sayhello方法,于是调用

  3.调用的时候,这里的nameperson的,输出person的name李四

  grade属性调用过程如下:

  1.s作为student的实例,会先在s里找grade这个属性,发现没有,于是沿着原型链向上查找s.__proto__,也就是student.prototype,在这里发现grade属性,值为3,调用

  这里person、student和s的关系如下图

  

 

构造继承

  在子类内部构造父类的对象实现继承

  详细的实现方法参加下面这个例子

  

  执行结果如下

  

  执行过程分析如下:

  1.先构建了一个parents的实例p,其name值为张三,p对象里有say方法,输出父亲的名字张三

  2.再构建一个child的实例c,其name传入李四age传入24(注意是传入,因为这里c没有name属性)

  3.传入这两个参数后,执行child这个函数,其中pObj这个属性指向parents这个函数,之后把name这个参数传入pObj,这里的this指向child

  4.调用parents函数,name李四传入其中,但由于也传入了child这个对象,这里thischild,也就是child.name李四,同时child也有了say方法

  5.调用child的sayC方法,这里child的name就是刚刚通过调用parents函数传入的李四,而age则是child这个函数本身就传入的24(并非parents里的age)

  关于age用24,原因有二

  1.child自身有age这个参数

  2.调用parents函数时,也没有给parents传入age参数

  (可以尝试给pObj传入age参数,并把child里的this.age=age提前到函数的最开始,输出结果就变成20了)

  

  其实也可以不用理解成父子的继承,因为个人感觉这里有点“偷师”的意思,也就是通过传入参数对象,达到给指定的对象添加一个属性,而不像原型链那样层层递推

 

call和apply继承

  call和apply都是对象的一个方法,第一个参数用来改变this的指向,而call和apply的区别在于第二个参数

  call第二个参数为独立的一个个参数,apply第二个参数为一个数组

   具体的用法看下面的这个案例

  

  执行结果如下

  

  执行过程的分析:

  1.构建stu这个student的实例时,接收了xm以及18两个参数,进入函数体

  2.函数体内,调用了person对象的call方法(所有对象都有的方法),回调person

  3.跳转到person函数中,把两个参数传递进去,这里的this代表当前对象,即stu,然后person函数中使用的this.name等,因为传入stu这个对象,this指向stu,即stu的name、age等接收传入的参数,同时stu也有了say方法,其中的this统统都是stu

  tea的构建方式类似

  不过这么看来,与其说是继承了方法,不如说是“获得”,可以看做stu调用了person函数,获得了person里的所有属性和方法。当然,获得的属性被赋值给stu自己了

  结合上面构造继承的方法,我们可以理解为,下图所示的两行代码的效果,等同于apply/call方法一行代码的效果

  

 

  好了,终于彻底理解这里的闭包、封装和继承了

posted @ 2019-08-06 21:47  且听风吟720  阅读(538)  评论(0编辑  收藏  举报