JavaScript面向对象思想详解

来源:互联网 发布:旅游cms系统 编辑:程序博客网 时间:2024/05/17 03:54

JavaScript面向对象思想


1.序言

在学习JavaScript的过程中,我认为,JavaScript勉强具备面向对象的能力,首先比较JavaScript是基于对象的,要像高级语言(java)那样完全面向对象是不可能的,因为面向对象的三大特征,封装,继承,多态,但在JavaScript不存在多肽,因为如果在同一个JavaScript文件中定义2个方法名相同的方法,不论参数个数,参数类型(JavaScript中参数不存在类型),是否相同,浏览器从上到先解析JavaScript文件,会已最后一个方法为准,最后一个方法(函数)会掩盖之前所有的同名的方法,

 

 往下我将主要介绍一些JavaScript面向对象的思想。本人知识水平有限,说的不对的地方,还望吝赐教,文辞粗浅,仅资参考。

 

声明:本文参考了阮一峰先生的相关博客资料。

2.JavaScript面向对象思想之封装

JavaScript是一种基于对象的语言,但是不同于Java这种高级语言,它的语法中没有class类(ECS6中已经提出了class类的思想,后面将展开介绍)。

 

对象是JavaScript的基本数据类型。对象可以看作是属性的无序集合,每个属性都是一个名/值对。属性名是字符串,因此我们可以对象看成是从字符串到值的映射,我们很多时候把它叫做哈希列表或散列表。JavaScript对象是动态的,可以很容易的进行新增或删除属性操作,相当于静态类型语言中的对象操作就显得十分轻松了。

 

那么我们应该如何在JavaScript中实现封装呢?具体的步骤是什么呢?

JavaScript的对象主要是通过prototype来模拟的。我们来看看如何进行封装。


2.1原始封装

假如我们把狗看成一个对象,给他赋予两个属性。

var Dog = {name =  “”,age =  “”}

我们便可以通过这样的格式来生成两个对象。


var dog = {};//创建一个对象Dog.name = “Jack”;//给它的属性赋值Dog.age = “9”;

这便是简单的最为简单的封装了,把属性封装在一个对象中,然后通过创建一个对象,再对象.属性来赋值。我们也可以很清楚的发现,这样创建对象使用起来很麻烦,所以还是需要改进。


2.2 改进原始封装--构造函数

我们可以考虑这样的生成对象的方式:var dog = Dog(“Jack”,”9”);像这么写我们就可以减少重复代码的使用。

 

那么我们上面定义对象的时候就可以做如下修改:

fuction Dog(name, age){    this.name = name,    this.age = age}

这便是构造函数模式,构造函数模式根据习惯首字母应该大写,在函数内部没有显示通过new去创建对象,将对象的属性直接赋值到this。构造函数本质上也是函数,因此不存在其他的语法。任何函数都可以通过new操作符来调用。


2.3构造函数的缺陷

构造函数虽然简洁好用,但是也存在缺点。在通过构造函数创建对象的时候,每个对象都要在实例上重新创建一遍,比如:
fuction Dog(name, age){    this.name = name,    this.age = age    this.bark = function(){    alert(“wow!”);};}

生成实例:

var dog1 = new Dog(“jack”,”10”);var dog2 = new Dog(“Tom”,”8”);

这时候,我们可以发现:当我们创建这两个实例对象的时候,有一个很大的弊端,每次生成一个实例,都会有一个bark()的方法,这些都是一模一样的内容,就会造成内存的浪费,没有达到代码共享的目的,我们可以这样测试:

alert(dog1.bark == dog2.bark);//false

为了解决这个问题,我们可以把函数定义也在构造函数外面:


fuction Dog(name, age){    this.name = name,    this.age = age}

function bark(){    alert(“wow!”);};

这样,当我们再次进行如上操作:


alert(dog1.bark == dog2.bark);//true

我们不禁好奇:能不能让bark()方法在内存中只生成一次,然后所有的实例都指向那个地址呢?答案是肯定的。这就是prototype模式。


2.4Prototype模式


2.4.1原型的概念

JavaScript规定,每一个构造函数都有一个prototype(原型)属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。这个对象可以为其他对象提供共享属性和方法。

       在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。JavaScript不包含传统的类继承模型,而是使用prototype原型模型。


2.4.2如何使用原型

如上例,我们可以做如下修改:
fuction Dog(name, age){    this.name = name,    this.age = age}Dog.prototype.bark = function(){     alert(“wow!”);};

然后我们生成两个实例


var dog1 = new Dog(“jack”,”10”);var dog2 = new Dog(“Tom”,”8”);

这时候所有实例的bark()方法指向的是同一个内存地址,指向prototype对象,因此提高了运行效率。

 

这时候我们可以测试一下:


alert(dog1.bark == dog2.bark);//true

2.4.3原型和原型链

每个由构造器创建的对象,都有一个隐式引用(对象的原型)链接到构造函数的“prototype”属性值。另外,原型可能有一个非空隐式引用链接到它自己的原型,以此类推,就构成了原型链。对象的原型决定了一个实例的类型。默认情况下,所有对象都是Object的实例,并继承了所有基本方法,如toString()。在原型链中可能存在这种情况,一个属性名既存在于实例对象中,又存在于其原型对象中,当访问这个属性时,根据就近原则,就会优先使用实例对象的属性值。如果这个属性不存在与实例对象中,则会根据原型链逐级向上找,直到prototype的引用为null。


3.JavaScript面向对象思想之继承

这一部分主要介绍了如何“封装”数据和方法,以及如何从原型对象生成实例。接下来介绍五种方法。

 

现在存在一个动物类对象的构造函数。


function Animal(){this.species = “动物”;}

还有一个“狗”对象的构造函数


fuction Dog(name,age){this.name = name;this.age = age;}

这时候我们会提出疑问,怎么样才能使”狗”继承“动物”?


3.1构造函数绑定

第一种方法也是最为简单的方法,即使用call或apply方法,将父对象的构造函数绑定在子对象上,即在子对象构造函数中加一行:

function Dog(name,age){Animal.apply(this,arguments);this.name = name;this.age = age;}var dog1 = new Dog(“Jack”,”18”);alert(dog1.species); //动物

3.2prototype模式继承

道格拉斯·克罗克福德在2006年写了一篇文章,题为Prototype Inheritance in JavaScript(JavaScript中的原型式继承)。在这篇文章中,他介绍了一种实现继承的方法,这种方法并没有使用严格意义上的构造函数。他的想法是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。为了达到这个目的,他给出了如下函数:


function object(o){function F(){F.prototype = o;return new F();    }}

在object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质上讲,object()对传入其中的对象执行了一次浅复制。

 

如果“狗”的prototype对象,指向一个Animal的实例,那么所有“猫”的实例,就能继承Animal了。


Dog.prototype = new Animal();Dog.prototype.constructoe = Dog;var dog1 = new Dog(“Tom”,”8”);alert(dog1.species);//动物

代码的第一行,我们将Dog的prototype对象指向一个Animal的实例。

Dog.prototype = new Animal();

它相当于完全删除了prototype对象原先的值,然后赋予一个新值。


第二行:

Dog.prototype.constructoe = Dog;

上文提及过,任何一个prototype对象都有一个constructor属性,指向它的构造函数。如果没有“Dog.prototype =new Animal();”这一行,Dog.prototype.constructor是指向Dog的,加了这一行以后,Dog.prototype.constructor指向Animal。

      

alert(Dog.prototye.constructor== Animal);//true

 而且因为每一个实例都有一个constructor属性,默认调用prototype对象的constructor属性。


alert(dog1.constructor== Dog.prototype.constructor); //true

 

所以我们在运行了”Dog.prototype = newAnimal();” 这一行之后,dog1.constructor也指向了Animal.

 

alert(dog1.constructor== Dog.prototype.constructor);//truealert(dog1.constructor== Animal);//true

 这样就会造成继承关系的混乱,明明dog1是用构造函数Dog生成的,因此我们必须手动纠正错误,将Dog.protype对象的constructor值改为Cat。也就是代码中的:

Dog.prototype.constructoe = Dog;

 如果替换了prototype对象,下一步必然是为新的prototype对象加上constructor属性,并将这个属性指回原来的构造函数.      

o.prototype.constructor= o;

3.3prototype模式直接继承

这一部分讲述的是对第二种方式的改进。由于Animal对象中,不变的属性都写进去prototype。所以,我们也可以让Dog()跳过Animal(),直接继承Animal.pototype。

首先,我们先改写Animal对象:

function Animal(){Animal.prototype.species = “动物”;}

然后,将Dog的prototype对象指向Animal的prototype对象,这样就完成了继承。


Dog.prototype = Animal.prototype;Dog.prototype.constructor = Dog;var dog1 = new Dog(“二狗子”,”18”);alert(dog1.spercies);//动物


与前面一种方法相,这样做的优点是效率比较高(不需要执行和建立Animal的实例了),比较省内存。缺点是Dog.prototype和Animal.prototype现在指向了同一个对象,那么任何对Cat.prototype的修改,都会反映到Animal.prototype。

 

原型链虽然很强大,可以用它来实现继承,但它也存在一些问题。其中,最主要的问题来自包含引用参数类型值的原型。想必大家还记得,我们前面介绍过包含引用类型值的原型属性会被所有的实例共享,如果某实例对象对该属性进行了操作,会影响到所有实例对象,所以尽量不要再原型对象中定义属性。原型链的第二个问题是:在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。因此,实践中很少会单独使用原型链。


3.4利用空对象作为中介

由于上面对于原型链的继承存在着上述问题。所以有了这种方法,利用一个空对象作为中介。

var F = function(){};F.prototype = Animal.prototype;Dog.rpototype = new F();Dog.prototype.constructor = Dog;


F是空对象,所以几乎不占据内存。这时,修改Dog的prototype对象就不会影响到Animal的prototype对象。

alert(Animal.prototype.constructor);//Animal

我们也可以将上面的方法,封装成一个函数,便于使用。

function extend(Child, Parent) {    var F = function(){};    F.prototype = Parent.prototype;    Child.prototype = new F();    Child.prototype.constructor = Child;}

使用的时候,方法如下:


extend(Dog,Animal);var dog1 = new Dog(“狗蛋”,”5”);alert(dog1.species); //动物

这个extend函数,就是YUI库如何实现继承的方法。


3.5拷贝复制

上面采用prototype对象,实现继承。我们也可以换一种思路,纯粹采用“拷贝”方法实现继承。简单说,如果把父对象的所有属性和方法,拷贝进子对象,不也能够实现继承吗?这样我们就有了第五种方法。

 

首先,还是将Animal的所有不变属性,都放到它的prototype对象上。

function Animal(){}Animal.prototype.species = “动物”;

然后,再写一个函数,实现属性拷贝的目的。


function extend2(Child, Parent){var p = Parent.prototype;var c = Child.prototype;for(var i in p){c[i] = p[i];        }}

这个函数的作用,就是将父对象的prototype对象中的属性,全部拷贝给Child对象的prototype对象。

 

使用的时候,这样写:


extend2(Dog, Animal);var dog1 = new Dog(“狗剩”,”4”);alert(dog1.species);//动物


4.JavaScript面向对象思想之闭包


4.4闭包与变量

JavaScript中的作用域链的机制引出了一个副作用,即闭包只能取得包含函数中任何变量的最后一个值。闭包所保存的是整个变量对象,而不是某个特殊的值。

 

闭包就是能够读取其他函数内部变量的函数,只有函数内部的子函数才能够读取局部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数,闭包就是将函数内部和外部连接起来的一座桥梁”

 

众所周知,变量的作用域无非就是两种:全局变量和局部变量。

 

JavaScript的特殊之处就在于函数的内部可以读取全局变量。

var n = 999;function f1(){    alert(n);}f1();//999


另一方面,在函数外部自然无法读取函数内的局部变量。

fuction f1(){    var n = 999;}alert(n); // error


这里有一个地方需要注意!函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明了一个全局变量!


4.2如何从外部读取局部变量

很多种情况下,我们有时候需要得到函数内的局部变量。我们为了获取可以做如下变通:


function f1(){    var n = 999;    function f2(){    alert(n); //999}}


在上述代码中,函数f2就被包含在f1内部,这时候f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。这就是JavaScript语言特有的“链式作用域”,子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

 

既然f2可以读取到f1中的局部变量,我们就可以把f2作为返回值,这样在外部我们就可以读取到f1中的局部变量了,这也就等同于java语言中的get方法。


function f1(){var n = 999;function f2(){alert(n);}return f2;}var result = f1();result(); // 999


4.3闭包的作用

闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值时钟保存在内存中。

function f1(){    var n = 999;    nAdd = function(){    n += 1;}    function f2(){        alert(n);    }    return f2;}var result = f1();result();//999nAdd();result();//1000



在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的确是999,第二次的值是1000.这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。

 

原因在于:f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终存在内存中,而f2的存在依赖f1,因此f1也始终存在内存中,不会在嗲用结束后,被垃圾回收机制回收。

 

这段代码中另一个值得注意的地方,就是”nAdd = function(){n+1}”这一行,首先在nAdd前面没有使用var关键字,因此nAdd是一个全局变量而不是局部变量。其次,nAdd的值是一个匿名函数,而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。



4.4闭包的注意点

1.由于闭包会使得函数中的变量被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成顽固额的性能问题,在IE中可能会导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

 

2.闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当做对象(object)使用,把闭包当做它的公用方法,把内部变量当作它的私有属性,这时一定要小心,不要随便改变父函数内部变量的值。


4.5内存泄漏

所谓内存泄露就是内存空间使用完毕之后未回收。在过去Web开发人员并没有太多的去关注内存泄露问题,那时的页面间联系大都比较简单,JavaScript使用的功能主要是表单校验,不会有太多关于特效以及业务等方面上的拓展,而进入WEB2.0时代,js人们对Web应用有了更高的要求。一个页面很可能发生:URL跳转,同时通过Web服务动态的更新页面内容。复杂的事件关联设计、基于对象的Jscript和DHTML技术的广泛应用,使得代码的能力达到了其承受的极限。

 

       标记清除

       JavaScript最常用的垃圾回收方式是“标记清除”。当变量进入环境(如在函数中声明一个变量),,则为其标记为“进入环境”;当变量离开环境,则为其标记为“离开环境”。垃圾回收器会定时扫描那些“离开环境”的变量,销毁那些被标记的值并回收他们所占的内存。

 

 

常见的内存泄露及解决

循环引用---IE浏览器的COM组件产生的对象实例和网页脚本引擎产生的对象实例相互利用,就会造成内存的泄露。这也是Web页面中我们最常见和最主要的泄露方式。

 

例:

var element = document.getElementById(“some-element”);var myobject = {};myobject.element = element;element.someElement = myobject;

在此例中DOM元素与一个原生JavaScript对象形成循环引用,其中myobject.element指向element元素,element.someElement指向element对象,由于存在这个循环引用,即使将改DOM从页面中移除,它也永远不会被回收。为了避免该问题,最好在使用完毕后手动将其删除:

myobject.element = null;element.someElement = null;

为了解决上述问题,IE9以后将所有的BOM和DOM都转化成了JavaScript对象,这样就避免了两种垃圾回收机制都存在的问题,也就消除了常见的内存泄露问题。

 

内部函数引用---Closures也就是闭包,外部变量可以应用内部函数的局部变量,当外部函数一直引用,那么该内部函数会在内存中一直存在,如果有大量这样的情况,则可能会出现内存泄漏。例如:


function closures(){    var a = 10;    return function(){        return a;    }}var b = closures()();


在此例中,closures函数下有个闭包,返回了改函数的局部变量a,外部有一个变量b引用了a,则如果b不释放a,a会一直存在与内存中,解决方法就是在b使用完后,主动释放b。



5.ECS6中的Class类

ES6 中有 class 语法。值得注意是,这里的 class不是新的对象继承模型,它只是原型链的语法糖表现形式。

函数中使用 static 关键词定义构造函数的方法和属性:


class Task {  constructor() {    console.log("task instantiated!");  }  showId() {    console.log(23);  }  static loadAll() {    console.log("Loading all tasks..");  }}console.log(typeof Task); // functionlet task = new Task(); // "task instantiated!"task.showId(); // 23Task.loadAll(); // "Loading all tasks.."

类中的继承和超集:

class Car {  constructor() {    console.log("Creating a new car");  }}class Porsche extends Car {  constructor() {    super();    console.log("Creating Porsche");  }}let c = new Porsche();// Creating a new car// Creating Porsche


extends 允许一个子类继承父类,需要注意的是,子类的 constructor 函数中需要执行 super() 函数。

当然,你也可以在子类方法中调用父类的方法,如 super.parentMethodName()。

在 这里 阅读更多关于类的介绍。

有几点值得注意的是:

·        类的声明不会提升(hoisting),如果你要使用某个 Class,那你必须在使用之前定义它,否则会抛出一个 ReferenceError 的错误

·        在类中定义函数不需要使用 function 关键词

 


原创粉丝点击