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.调用的时候,这里的name是person的,输出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这个对象,这里this为child,也就是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方法一行代码的效果
好了,终于彻底理解这里的闭包、封装和继承了