JavaScript中的面向对象思想

[size=medium][b]一.概述[/b][/size]
[size=small]
任何一门面向对象的编程语言都有如下三个特性:封装,继承和多态。封装的意思就是把相关的方和和变量封装在一起,并且赋予这些方法和变量一些访问的权限,简单的说就是明确了那些变量或者方法只能在对象内部或者派生类中使用,那些变量或者方法可以在类的外部使用,封装的好处可以不把接口的实现细节暴漏给使用者,比如开关电脑,我不知道里面的实现到底是什么样的,我只要按一下开关电脑就开了,点两下鼠标电脑就关了,使用者没有必要关心其实现也可以达到自己的目的。继承也是面向对象的一大特色,由于继承的存在,这样就可以在设计的站在一个更加抽象的层面去设计,比如看Spring的设计,我们看到都是一些interface以及一些抽象类,同时继承可以实现代码的复用,把一些公共的代码放在基类中。多态,简单的说就是同类型的对象在不同的环境或者时刻表现出不同的行为来。关于面向对象的思想说起来简单,但是要想真正把其理解掌握,的确需要花很大的功夫去理解去实践,我还没有理解其本质。下面我们就围绕着封转,继承和多态来看一下JS中的面向对象思想。
[/size]
[size=medium][b]二.JS之封装[/b][/size]
[size=medium]1.变量的封装[/size]
[b]变量封装的方式一[/b]

<script>
var o = new Object();
o.name = "alibaba";
console.log(o.name); //输出alibaba
</script>

[size=small]
这里直接创建一个Object类型的对象,Object是JS的内置函数(不是类,这个我们后面再解释),o.name相当于给这个对象增加了一个name的成员变量,增加一个变量的方式太简单了,不过个人觉得有点随意,这样我们可以到处给o增加成员变量了,今天A增加一个明天B增加一个,到后来我们自己都不知道o的成员变量到底有那些了。这里有个小细节,如果对象o已经拥有了成员变量name,那么o.name=”alibaba”就是对成员变量的复制了而不是创建成员变量了。
[/size]
[b]变量封装的方式二[/b]

<script>
var o = {
name:"alibaba",
age:10
}; // 这里的分号不是必须的,但是习惯加个分号

console.log(o.name); //输出alibaba
console.log(o.age); //输出10
</script>

[size=small]
这里没有使用new关键字,感觉是直接定义了一个对象,然后封装了name和age两个成员变量,我们可以在浏览器的Console中看到原来o也是一个Object类型的对象,不过这里的缺陷也很明显,对象成员变量的值怎么在定义的时候被缺省创建了,就是说我们在创建对象的时候无法干预成员变量的赋值,只有在对象创建结束后,我们才有修改成员变量值的机会。
[/size]
[b]变量封装的方式三[/b]

<script>
function createCompany(name, age) {
var o = new Object();
o.name = name;
o.age = age;
return o;
}

var o = createCompany("alibaba", 10);
console.log(o.name); //输出alibaba
console.log(o.age); //输出10
</script>

[size=small]
这里有点工厂设计模式的味道,其实说到这里,我也曾经在网上搜索过,怎么没有写JS设计模式的书,我觉得JS也应该有一些设计模式出来,模式其实就是一些通用问题的解决方案,感觉模式和算法发挥的作用有点像。这时候我们可以干预创建对象是成员变量的赋值了,遗憾的是,我们只知道从我们这个“伪工厂”中创建出来的对象其类型只能是Object,我们不知道其具体类型,感觉不好。
[/size]
[b]变量封装的方式四[/b]

<script>
function Company(name, age) {
this.name = name;
this.age = age;
}

var company = new Company("nuaa", 10);
console.log(company.name); //输出alibaba
console.log(company.age); //输出10
</script>

[size=small]
这里首先定义一个Company的构造函数,这个构造函数有两个参数,这样我们可以在创建对象的时候既可以控制成员变量的复制,也可以知道对象是什么类型的了,所以这是一个比较完美的解决方案。
[/size]
[size=medium]2.方法的封装[/size]
[size=small]在采用面向对象思想时,我们一边都把变量的访问权限设置为私有的,然后提公有的接口去获取成员变量的值,不过在JS中我们按照上面的方式定义的成员变量都是公有的,就是可以直接通过.的方式来访问。但是这并不能说我们就不需要去封装方法了,方法的封装还是必须的。下面我们就来看一下各种方法封转的方式,其实和上面变量的封装是一一对应的
[/size]
[b]方法封装方式一[/b]

<script>
var o = new Object();
o.name = "alibaba";
o.getName = function() {
console.log(this.name);
};

o.getName(); //输出alibaba
</script>

[size=small]
这里我们先创建一个Object类型的对象o,然后给这个对象增加一个方法属性getName,这里的问题还是一样的,方法的封装可能会零散的分布在程序的各个地方,导致最后我们自己都不知道到底有那些方法
[/size]
[b]方法的封装方式二[/b]

<script>
var o = {
name:"alibaba",
getName:function() {
console.log(this.name);
}
};

o.getName(); //输出alibaba
</script>

[size=small]
在定义变量o的时候定义其方法属性getName,缺点也是一样的,就是在定义对象的时候无法控制成员变量的复制
[/size]
[b]方法的封装方式三[/b]

<script>
function createCompany(name, age) {
var o = new Object();
o.name = name;
o.age = age;
o.getName = function() {
console.log(this.name);
};
o.getAge = function() {
console.log(this.age);
};
return o;
}

var o = createCompany("alibaba", 10);
o.getName();
o.getAge();
</script>

[size=small]
这里采用的是在“伪工厂”中创建对象的时候给创建对象的方法属性,其实和方式一的创建手段有点像,只是这里把对象方法属性的创建集中起来,但是我们还是没有办法知道所创建的对象类型。
[/size]
[b]方法的封装方式四[/b]

<script>
function Company(name, age) {
this.name = name;
this.age = age;

this.getName = function() {
console.log(this.name);
};

this.getAge = function() {
console.log(this.age);
};
}

var company = new Company("alibaba", 10);
company.getName();
company.getAge();
</script>

[size=small]
这里我们在构造函数中创建方法属性,这样上面的那些缺点就全部没有了,这种解决策略是不是一定就是完美的呢?其实未必,我们来看个例子,首先我们创建一个对象var c1 = new Company(“nuaa”, 60); 接下来我们再来创建一个对象var c2 = new Company(“alibaba”, 10);这时候我们有两个对象c1和c2,这两对象除了成员变量的值不一样,其方法属性是一样的,但是现在c1拥有的getName应用和c2拥有的getName引用是不一样的,在JS中,其实每个方法属性都是对一个变量的引用,我们可以在程序中验证一下c1的getName对应的值和c2的getName对应的值是不一样的,下面我们给出验证程序:
[/size]

<script>
function Company(name, age) {
this.name = name;
this.age = age;

this.getName = function() {
console.log(this.name);
};

this.getAge = function() {
console.log(this.age);
};
}

var c1 = new Company("alibaba", 10);
var c2 = new Company("nuaa", 60);
console.log(c1.getName == c2.getName); //输出为false
</script>

[size=small]
上述程序段说明c1的getName和c2的getName是不同的引用,好,问题来了,虽然c1和c2是不同的引用,但是它们指向的代码段是一样的,所以这里的不同引用有点不合理,因此就出现的原型的概念,下面我们将看看JS中的原型。
[/size]
[size=medium]3.JS中的原型[/size]
[size=small]
原型(prototype)是JS中每个函数的都有的属性,这里一定要理解清楚,原型是函数的属性,首先我们来看一段小程序
[/size]

<script>
function Person(){
}
</script>

[size=small]
这段JS程序只定义了一个函数Person(),我们可以在浏览器的Console查看函数的prototype属性
[/size]
[img]http://dl.iteye.com/upload/attachment/0080/9209/e4a68710-67a5-3405-8860-f2ce4d7eb56b.jpg[/img]
[size=small]
这时我们发现原来函数的prototype属性就是一个对象,这个对象有两个属性,分别是constructor属性和_proto_属性,其中构造函数属性指向了其本身,下面我们通过一个示意图来描述一下这种关系,方便理解,关于这里的_proto_我们后面再讨论,它是一个引用,指向了当前对象的父类。
[/size]
[img]http://dl.iteye.com/upload/attachment/0080/9211/c61993e1-d84c-3ef8-9bd0-62b8894fabe7.jpg[/img]
[size=small]
通过上面的示意图,我们很清晰的看到函数的prototype属性其实就是一个对象,关于对象的_proto_属性我们后面再介绍。其实原型有点类似我们经常所说的静态成员变量,为所有实例所共享。这里我们并没有显示的地去定义函数的原型属性,此时,原型属性的constructor属性就自动会获取到构造函数Person()。下面我们来看看在使用原型的使用:
[/size]

<script>
/*在构造函数中定义成员变量*/
function Company(name, age) {
this.name = name;
this.age = age;
}

/*在原型中定义方法*/
Company.prototype = {
getName:function() {
console.log(this.name);
},

getAge:function() {
console.log(this.age);
}
}

var c1 = new Company("NUAA", 60);
var c2 = new Company("alibab", 10);

console.log(c1.getName == c2.getName); //输出是true
console.log(c1.getAge == c2.getAge); //输出是true
</script>

[size=small]在chrome中的运行结果如下:[/size]
[img]http://dl.iteye.com/upload/attachment/0080/9214/d22d9468-59d6-3c51-abc2-8f263b38f974.jpg[/img]
[size=small]
这里我们首先来看1,此时原型的constructor属性已经不再是Company()了,就是说如果我们定义了函数的原型属性,那么原型属性的constructor属性要是我们不显示指定的话,就不会自动指向Company(),如果这个constructor属性对你来说比较重要的话,那么就应该在原型中指定一下这个属性,例如constructor:Company。接下来我们来看看原型属性,这个原型属性是一个Object类型的对象,同时具有getName()和getAge()两个方法,通过观察2出,我们发现原来getName()和getAge()也有原型属性,再一次验证了原型属性是每个方法固有的属性,同时方法也有__proto__属性,这个我们后面再研究。最后需要注意的是程序的输出,打印出两个true,这就说明c1和c2拥有函数引用是相同的。这样的写法更加合理一些,这也是我们在前端开发中经常用到的一种写法。
[/size]
[size=medium][b]三.JS之继承[/b][/size]
[size=medium]1.实例的_proto_属性学习[/size]
[size=small]
在前面我们就说过每个实例都有一个_proto_属性,那么这个_proto_属性到底是什么呢?其实就是实例对应构造函数的原型对象,在这个原型对象中有我们在原型属性中定义的一些方法。下面我们首先来看一段JS代码
[/size]

<script>
//构造函数的定义
function Company() {
this.name = "NUAA";
}

//构造函数原型属性的定义
Company.prototype = {
sayHi:function() {
console.log("Hello " + this.name);
}
}

var o = new Company();
o.sayHi();
console.log(o);
</script>

[size=small]
上述代码很简单,就是定义了一个构造函数,同时也定义了构造函数的原型属性,接下来把实例打印出来。这段程序的运行结果如下:
[/size]
[img]http://dl.iteye.com/upload/attachment/0080/9230/9ee2a6ad-d6fb-3458-81db-c9091dd45b47.jpg[/img]
[size=small]对于箭头(1),我们发现实例对象的_proto_属性,其实根据上面的信息,我们可以看到_proto_是一个实例,其实就是原型对象实例,在这个原型对象实例中有我们在原型属性中定义的sayHi()方法,这个我们可以看一下构造函数Company()的原型属性,箭头(3)所指。同时在原型对象中,我们又看到了_proto_属性,箭头(2)所指,原型对象的_proto_属性执行的是Object()构造函数的原型对象,和箭头(4)处我们打印的Object的原型对象是一样的,那么Object原型对象的_proto_属性呢?没有,这是原型链的终点,原型链到Object后就停止发展了。其实这里我们已经看清楚了JS的继承是如何实现的了,对象实例中拥有一个_proto_属性,这个属性也是一个对象实例,这个对象实例中有一系列方法,同时原型对象实例中也有自己的_proto_属性,这时候使用我们创建的实例调用一个方法后,首先就会在实例本身的_proto_属性中查找这个方法,如果找到,就会执行,如果找不到,就会到实例拥有的原型对象的_proto_属性中去找,这样一直按照原型链向上搜索,直至找到方法或者原型链到达终点。
[/size]
[size=medium]2.方法的_proto_属性学习[/size]
[size=small]
在JS中每个函数都是一个对象实例,既然函数也是对象实例,那么函数也应该有自己的_proto_属性,没错,函数也有自己的_proto_属性,函数的_proto_属性中保存是一些函数的内部属性,比如call()方法,用来调用函数自己等等。下面我们来看一个小例子。
[/size]

<script>
function Sum(num1, num2) {
this.num1 = num1;
this.num2 = num2;
}

Sum.prototype = {
add:function() {
console.log(this.num1 + this.num2);
}
}

var o = new Sum(1, 2);
//直接调用函数
o.add();
//使用函数的内部属性调用函数
o.add.call(o, 1, 2);
</script>

[size=small]
这段JS程序很简单,创建一个对象实例,然后调用对象实例的add方法,不过这里展示两种调用add方法的方式,第一种是通过对象实例来调用的,第二种时通过函数的内部属性来调用的,在使用call方法时,我们传递了一个o参数,就是把o传进去,这时候在add方法中的this引用就是o了。下面是这段JS程序的运行结果:
[/size]
[img]http://dl.iteye.com/upload/attachment/0080/9234/215638ac-5ff0-3e94-8327-92ad5d23435f.jpg[/img]
[size=small]
看看这段JS代码的运行结果,我们发现函数有自己的_proto_属性,上图B箭头所指,函数的_proto_属性指向了一个对象,这个对象包含了函数的一些内部属性,例如call方法等,同时每个函数都有自己的原型属性,上图A箭头所指就是函数的原型属性。
[/size]
[size=medium]3.继承的实现[/size]
[size=small]
在JS 继承是通过原型链来实现的,其基本思想是利用原型让一个引用类型继承另外一个引用类型的属性和方法。从这个基本思想出发,我们可以设计这样一个简单的基于继承的JS程序。
[/size]

<script>
function Parent() {
this.superName = "Parent";
}

Parent.prototype = {
getSuperName:function() {
console.log(this.superName);
}
}

function Child() {
this.subName = "Child";
}

//子引用的构造函数的原型属性指向父引用对象,这样子引用就继承了父引用中的方法
Child.prototype = new Parent();

//在继承父引用方法的同时又有自己不同的特点
Child.prototype.getSubName = function() {
console.log(this.subName);
}

var child = new Child();

//调用继承而来的方法
child.getSuperName();
//调用自己的方法
child.getSubName();

</script>

[size=small]
上述代码利用JS中的原型链实现了面向对象中的继承理念,子引用在继承父引用的中的方法后,又有自己的方法。下面我们来看一下上述代码在Chrome中的运行结果如下:
[/size]
[img]http://dl.iteye.com/upload/attachment/0080/9236/23a68857-29ba-30a0-a360-445366a9b780.jpg[/img]
[size=small]
通过上面的运行结果,我们可以看到child实例有一个subName的成员变量,它的_proto_属性(箭头A所指)指向了一个Parent类型的实例(如箭头B所指),这个Parent类型的实例中首先有我们给Child增加的一个getSubName方法,其次,因为这个实例是Parent类型的,那么它就拥有Parent类型中定义的成员变量superName,其次,它(child的_proto_所指的引用)是一个实例,那么就应该有自己的_proto_属性,这个_proto_属性就指向了Parent类型的原型对应的一个实例,这里面就包含了Parent原型属性中定义的getSuperName方法了,从而形成一个原型继承链,当然箭头B所指的实例也有自己的_proto_属性了,这个属性其实就是原型链的终点了,即Object类型的实例。接下来,我们来按照这个原理来分析一下child.getSuperName()方法的调用过程,首先会在child的_proto_属性中查找这个方法,发现child的_proto_属性中没有这个方法,这时候child的_proto_属性指向了一个引用,这个引用也有自己的_proto_属性,这时候就会在这个引用的_proto_属性中查找getSuperName()方法,结果找到了这个方法,因为这个引用的类型是Parent类型的,那么它的_proto_属性一定指向Parent的原型对象了,这时候肯定就会找到getSuperName()方法,如果找不到,就沿着继承链继续向前找找到Object中,如果在Object中还是找不到的话,那么就报错没有相关的方法。如果我们在child中重写了父类的一个方法,那么使用child来调用这个方法的时候,调用是Child原型中的方法,而不是Parent原型中的方法,这可以理解为是JS继承链中的就近原则。
[/size]
[size=meduim][b]三.关于多态[/b][/size]
[size=small]找了一些资料看了看,觉得JS中多态的研究没有太大的意义,所以就没有继续往下纠结。
[/size]
[size=medium][b]四.JS&&JQuery实践分享[/b][/size]
[size=small]前些日子利用晚上的时间折腾了一个表达式计算器,利用数据结构中堆栈思想实现的,有一个操作符栈和一个操作数栈,通过扫描表达式以及比较算符优先级来实现,这个算法大家都学过,我就不详细介绍了,下面就是最终的Demo:
[/size]
[img]http://dl.iteye.com/upload/attachment/0080/9238/01caaba0-dae6-391e-8599-35df18a49a73.jpg[/img]
[size=small]注意:1.JS中浮点数的计算有点问题[/size]
[img]http://dl.iteye.com/upload/attachment/0080/9240/f2330d4e-c7ee-3c8a-a726-7648919a76a4.jpg[/img]
[size=small]2.源代码[url]https://github.com/yangbolin/web-calculator.git[/url][/size]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值