JavaScript高级程序设计之面向对象程序设计

JavaScript不是纯粹的面向对象编程语言,因此用面向对象编程,对与一些初学者是比较难的,甚至对一些中级的前端工程师来说也是一种考验。既然用JavaScript面向对象编程,如此困难,为何我们还要用面向对象呢?下边我们去揭开面向对象的层层面纱,去寻找其中的原因。

面向对象简称OO编程,是目前比较盛行的编程方法。在面向对象中,推崇一切皆对象的理念,这些与现实世界的事物更加的贴合。与面向对象相对的编程方式,是面向过程。面向过程编程方式,注重的是功能的实现过程,而不注重功能本身。在编程界中,有的推崇面向对象,有的推崇面向动作,也有的人,认为两种方式是互补的,不过这种人比较少。

理解对象

在JS中,创建对象的最简单方式,就是直接声明一个对象字面量,然后在给对象添加对应的属性和方法。例:const persion = {}; persion.name = "xyq";。这是一个给对象赋属性的例子,那什么是属性呢?属性是用于描述对象特征的值。例如创建一个人类,人类有性别、年龄等属性,来描述人类的特征。JS中属性分两种,数据属性和访问器属性。

数据属性是包含数据的值,我们可以对这个值,进行读取和写入的操作。数据属性拥有四个描述其行为的特性:[[Configurable]]:是否可配置,表示属性能否用delete删除,重新定义,能否修改属性的特性或能否把属性修改为访问器属性。默认值为true。[[Enumerable]]:是否可枚举,表示是否能通过for in 、for of循环返回属性。默认值true。[[Writable]]:是否可写,标示是否可以修改属性值,默认为true。[[value]]:用来存储属性的值,默认值为undefined。

可以通过Object的defineProperty和defineProperties方法来修改属性的描述符,例:

 1   let obj = {
 2             age:18
 3         }
 4         //defineProperty方法
 5         //第一个参数为要设置的对象 第二个参数要设置的属性 第三个参数一个对象
 6         //具体要设置的属性描述符
 7         Object.defineProperty(obj,"age",{
 8             writable:false,
 9             configurable:false,
10         })
11 
12         obj.age = 35 //报错
13 
14         //defineProperties方法
15         //第一个参数为要设置的对象 第二个参数为属性对象
16         Object.defineProperties(obj,{
17             "age":{writable:true}
18         })
19 
20         obj.age = 35 //不在报错
21 
22         //获取指定属性的描述符
23         //第一个参数为要获取属性描述符的对象 第二个参数为要获取属性描述符的属性
24         Object.getOwnPropertyDescriptor(obj,"age");
View Code

访问器属性不包含数据值,它们包含一对getter和setter函数,当然它们并不是必须的。读取属性的时候,会调用getter函数,设置属性的时候会调用setter函数。访问器属性也有四个特性:[[Configrable]]:能否通过delete删除、能否修改属性特性、能否把属性改为数据属性。默认true。[[Enmerable]]:能否通过for in 或 for of循环,默认true。[[Get]]:读取属性调用的函数,默认值为undefined。[[Set]]:在写入属性时,调用的函数,默认值为undefined。

必须通过Object的defineProperty或defineProperties方法来修改。例:

 1 //访问器属性
 2         Object.defineProperty(obj,"name",{
 3             get:function(){
 4                 return this._name
 5             },
 6             set:function(value){
 7                 if(value == "c") value = "b";
 8 
 9                 this._name = value;
10             }
11         })
12 
13         obj.name = "c";
14 
15         console.log(obj.name);
View Code

 创建对象

工厂创建模式

1  // 工厂创建
2 
3     function factory(name,age){
4         const f = {};
5 
6         f.name = name,f.age = age;
7 
8         return f;
9     }
View Code

构造函数创建模式

与工厂创建模式的不同:1、没有显式地创建对象。2、直接将属性和方法赋给了this对象。3、没有return语句。

构造函数需要通过new操作符去创建对象,这样创建出来的对象都是构造函数的实例,都会有一个constructor属性,指向构造函数。使用构造函数创建对象,有一个很明显的优点,就是可以把它的实例标为特定的类型。

构造函数也是函数,它与普通函数除了调用方式不同之外,并无其它不同,也就是说,任何函数,通过new调用,都可以称为构造函数。

缺点:每次创建实例,方法都会被重新创建一遍,而这会引起不必要的性能消耗。

 1 //构造函数创建
 2     function Persion(name,age){
 3         this.name = name;
 4         this.age = age;
 5 
 6         this.getName = function(){
 7             return this.name;
 8         }
 9     }
10 
11     const persion1 = new Persion("ww",15);
12     
13     persion1.getName();
View Code

原型模式

构造函数创建对象,存在方法不能共享的问题。原型模式的出现,解决了此问题。原型模式让构造函数有一个原型属性,这个原型属性指向构造函数的原型对象,因此可以在其身上添加属性和方法,而且添加的属性和方法,是所有实例共享的。

原型是js面向对象中,十分重要的一环,因此理解原型是十分必要的。原型属性有个constructor属性,这个属性存储的是指向构造函数的一个标示,我们可以通过这个属性来访问构造函数。创建的构造函数,初始的原型只有constructor一个属性,其它方法都是继承自Object。当我们创建构造函数的时候,实例内部会有一个指向原型对象的指针,ES5中管这个指针叫做[prototype],不过并没有标准的方式访问这个指针,但是在主流的浏览器中,支持用__proto__来访问该指针。(注:实例的[prototype]指针是指向构造函数的原型对象的,这点一定不要弄混)。虽然并没有一个标准的方式来访问[prototype],但是可以用isPrototypeOf方法来确定对象之间是否存在次关系。例:Persion.prototype.isPrototypeOf(persion) 返回true。还可以用新增的getPrototypeOf方法获取[prototype]的值。例:Persion.prototype.getPrototypeOf(persion) == Persion.prototype 返回true。

每一次对原型属性的访问,都会进行一次查找操作。查找会首先查找对象自身,然后在查找原型,如果差找不到,会继续查找原型的原型,知道查找到属性为止。所以实例只能访问原型的属性,无法修改原型的属性。如果在实例上添加一个属性的话,原型中的属性并不会改变,它只是纯粹的给实例加了一个属性,但是你访问的时候,根据属性的查找原理,似乎的确是修改成功了,但是,当你实例化另一个实例时,通过这个实例,你去访问属性,你会发现,修改并没有成功,值并没有发生变化。之前添加的实例属性,只不过是把访问原型属性的路给堵住了,造成了修改成功的假象。而这也就是说,只要我们把路疏通,仍然可以访问原型中的属性。如何疏通呢?可以用delete操作符,把实例属性删除掉,路就被疏通了。

有一个hasOwnProperty方法,此方法会判断属性是否是对象本身的,如果是返回true,如果不是返回false。方法接收一个属性名作为参数,通过实例来调用。

原型模式很好用,但并不是没有缺点。原型模式最大的一个缺点,就是如果有个属性它的值是引用类型的话 ,修改这个值里的某个属性,在其它拥有此原型属性的对象上,修改也会生效。为了弥补这个缺点,就出现了构造函数和原型合用的模式。这个模式,在构造函数中定义属性,在原型中定义方法。用构造函数的优点来弥足原型的缺点。

动态原型模式,是另一种创建原型的模式,它把原型写在构造函数中,只在构造函数第一次实例化的时候,创建原型,之后的使用中,不在创建。该模式,会在构造函数中添加一个判断,判断原型方法是否存在,不存在的话,则创建这个方法。存在的话则不进行任何操作。

寄生构造函数模式采用的是借鸡生蛋的原理。创建一个普通的函数,把创建对象的代码封装到函数里。使用上,既可以当做普通工厂函数使用,也可以用new操作符,当做构造函数使用。

稳妥构造函数模式其实就是寄生构造函数的升级版,让其变得更加的安全。该模式,不在提供直接访问属性的途径,需要通过调用方法来获取属性存储的值,而且调用函数时只使用函数调用方式,不再使用new。

继承

JS对继承的实现和其它语言不同,它通过原型来实现继承。

原型链

原型链是个比较重要的概念,js的继承就是通过原型链来实现的。原型链其实并不是很难,其原理就是一个对象的原型指向了另一个对象,而另一个对象的原型又指向了第三个对象,这时三个对象之间通过原型这个链条彼此互相链接,而这就是原型链。在JS中,所有的对象都默认继承Object类型,它们的默认原型都是Object的实例,也就是说其内部有个指针指向Object的原型。

原型链的确是个好东西,但实践中并不是很常用,因为它还存在一些问题,为了解决这些问题,出现了一种继承方法,名字叫做借用构造函数。这种方式的思想比较简单,就是在子类的构造函数中,借用call和apply来调用父类的构造函数,绑定子类的环境对象。这样子,子类会生成父类属性的副本,不会存在引用值共享的问题,而且可以传递参数,不过因为使用的是构造函数,因此构造函数模式的问题,也会在其身上体现出来。

为了解决借用构造函数的问题,又出现了另一种继承方式。组合继承其实就是构造函数和原型组合模式,只不过这里用来实现继承。这种方式把构造函数和原型合二为一,各取其长。通过构造函数来实现属性的继承,通过原型来实现方法的继承。看上去是个比较完美的继承方式。

借于寄生构造函数的理念,出现了原型式继承的方式。这种方式通过借用第三方的函数,来封装具体的继承实现。而在具体实现中,还会创建一个函数,用来通过其原型来实现继承,然后在把函数实例化后返回出去。这种方式的确很好,以至于JS在es5中推出了Object.create方法。这个方法就是对原型式继承方式的一个规范,通过这个方法可以很轻松的实现原型式继承。方法接收两个参数,第一个父类对象,第二参数为可选,是用来给新对象定义额外属性和方法的对象。

寄生式继承更加的接近了寄生构造函数,它可以说是原型式继承的一种提升。它和原型继承的区别,就在与函数的内部,不仅仅是实现继承,它还会对实例对象进行一些扩充,然后再把实例对象返回出去。

前边说的组合继承,虽然很常用,但存在问题。它最大的问题就是,无论什么情况下,都会调用两次超类型构造函数,一次是在构造函数内部继承属性的时候,另一次是继承原型方法的时候。为了解决这个问题,出现了一种更完善的继承方式,寄生组合式继承保留了组合式继承的思想,并在其上加入了寄生式继承的思想,两者混合,实现了该继承方式。基本原理为属性的继承依然在构造函数中用call,然后原型的继承通过封装的第三方函数来实现。该函数接收两个参数,父类和子类构造函数。函数中会调用Object()创建一个父类原型的副本,然后把副本的constructor属性指向子类构造函数,最后把副本赋给子类的原型。

 

posted on 2017-11-25 13:01  木森焱  阅读(249)  评论(0编辑  收藏  举报

导航