JavaScript大杂烩3 - 理解JavaScript对象的封装性
JavaScript是面向对象的
JavaScript是一种基于对象的语言,你遇到的所有东西,包括字符串,数字,数组,函数等等,都是对象。
面向过程还是面向对象?
JavaScript同时兼有的面向过程(函数)的编程体验和面向对象的编程风格,其实对于很多人来说,完全的函数式编程可以很好的解决相关问题,比如荷兰的ppk(著有《ppk谈JavaScript》一书,是JavaScript经典的著作之一)就很擅长使用面向函数的方式解决问题,而对与面向对象编程,使用的反而少了一点;其实对于大多数的页面来说,它们的逻辑都比较简单,通常都是基于事件驱动的小段功能,短小精悍与高效向来就是页面逻辑追求的目标,从这一个方面来说,面向函数的编程确实也是JavaScript最主要的使用习惯。不过,对与很多大型的,逻辑来说比较复杂的JavaScript程序,还是需要从面向对象的角度去编程。总的来说,并不是说面向对象就比面向过程编程更为高级,必需要面向对象编程,这个还是要具体问题具体分析的,面向对象表示能力更强了,抽象等级也更高了,但是带来的复杂性也更多了,很多的场景可能还是面向函数编程更为简单直接。
下面就我自己的认识,总结一下JavaScript在对象方面的表现。
面向对象之封装性
对象是比函数更为高级的抽象,它体现了复用的知识不再像面向函数编程那样仅仅局限于一些动态的行为,而是把行为的作用数据,也提高到了与行为等高甚至是更高的抽象层次(数据驱动开发)。这些复用的数据和行为被封装到对象中,作为一个整体在不同的场景下完成比较独立的功能。所以封装性是面向对象最为根本的特性,是对象作为一个独立单元的表现。在面向对象编程中,对象是复用的基本单元。
对象的封装性在JavaScript中体现出来的就是作用域的概念,这个前面我们已经总结过函数的作用域的概念了,因为函数是对象,所以对象的作用域概念是一致的。基本上JavaScript的作用域分为两类:
第一类,就是在JavaScript中有全局作用域的概念,就是某个变量在全局都可以访问,所有直接在Script元素中定义的变量都有全局的作用域,除此之外,即使是在函数中不是使用var定义的变量都具有全局作用域(这一点最新的浏览器应该是处理了这种情况的,不过为了安全起见,不要出现不用var定义的变量)。其实我们说JavaScript是面向对象的,在这里也是有所体现的,全局作用域其实就是内置对象window的作用域,所以所有全局变量都是window对象的成员。简单看个例子:
var n = 10;
k = 20; // bad usage alert(n);
alert(k);
alert(window.n);
alert(window.k);
第二类,就是封装性的体现,局部作用域,也就是对象的内部作用域,像函数体内部定义的变量,形参等等。从上面全局作用域的表述上说,其实全局作用域只不过就是window对象的局部作用域。简单看个例子:
function M(age) {
var tmp = age;
alert(tmp);
alert(age);
} M(10);
alert(tmp);
alert(age);
最后的两个alert在最新的Chrome上都会抛异常并结束JavaScript的运行,这个行为我们要时刻注意,因为这是导致程序无端结束的常见原因之一。
封装是为了在保护细节的基础上安全的实现对象的功能,是维持接口的稳定性的基本方式。使用作用域可以很好隐藏对象的细节。在更详细总结封装性之前,我们先来看看如何创建和表示对象。
在面向对象编程中,类是对象的抽象,是对象的模板,类封装了特定的数据,定义了特定的行为。在JavaScript中,是没有类概念的,因为JavaScript是动态语言,是可以随时扩展的,所以不需要类作为对象模板,如果确实需要模板来创建新的对象,那就直接使用对象来做模板吧!JavaScript就是直接从对象开始的。
对象的表示方法
在JavaScript中,定义一个对象很简单,常见有两种方式:
var o = {};
var o1 = new Object();
我相信,大家使用的都是第一种,也就是对象字面量去定义一个对象。
有了对象,就是要添加成员了,添加成员和使用成员如下所示:
var o = {
name: 'Frank',
say: function (msg) {
alert(name + msg);
}
}; o.age = 10;
o.go = function() {
alert('go');
}; alert(o.name);
o.say('Hi');
alert(o.age);
o.go();
直接写到对象里面和动态给对象添加是常用的方式,这里定义的成员(数据和方法)在对象外部可以正常访问,这是其它语言中所说的公开成员。在上面的例子中,我定义了一个人,然后添加了姓名,年龄等,如果我再去定义另外一个人的时候,可以按照相似的逻辑去定义一个新的对象。
对于对象成员的访问,JavaScript也比较灵活,可以直接使用成员名字,字符串名字,变量访问,特别是后两种,对于不符合规范的成员名字访问非常有效,看个例子:
// 定义时也可以使用字符串命名成员
var foo = {name: 'frank', 'age': 10};
// 有效的方式:
foo.name; // frank
foo['name']; // frank
var get = 'name';
foo[get]; // frank
// 特殊的成员定义与访问方式,并不推荐
foo.1234; // 无效的方式,无效的名称
foo['1234']; // 有效的方式,并不推荐
这种直接使用对象的方式很简单,应用也很广泛,但是有一点小小的问题:
1. 所有成员都是公开的,这一点对于封装性来说不是太好的现象,因为细节全部暴露了。
2. 对象的成员不能够共享,每定义一个具有相同逻辑的对象,成员都是独立的一份,造成了空间浪费。
为了解决这些问题,我们可以使用函数来模拟对象。
解决封装性问题:使用函数的作用域与闭包来模拟对象的可访问性
对于上面的第一个封装性的问题,可以用函数的作用域与闭包机制来解决。我们可以这么模拟对象的工作过程:
function Person(name, age) {
var m_name = name;
var m_age = age; this.say = function(msg) {
alert(m_name + ':' + msg);
};
this.go = function() {
alert('go');
};
} var o = new Person('Frank', 20);
var o1 = new Person('Dong', 30);
o.say('Hi');
o1.say('Hi');
针对上面的代码,我们来分析面向对象封装性的各个要素:
1. 构造函数constructor
这个就是由Person函数来充当了,毋庸置疑。
2. 私有成员
在构造函数中使用var定义的成员都是私有成员,只工作在对象内部,对象构造函数中定义的所有成员都可以访问它们。
3. 公开成员
在构造函数中使用this定义的成员都是公开成员,对象外部可以访问这些方法,这里关键是this的使用,在JavaScript中,this指向当前函数的调用者,比如上面的例子中,this指向Person函数的调用对象,在接着的两次new操作中,this分别指向了o与o1,所以this这种方式就是利用了JavaScript的动态语言特性的给对象添加了方法(本质同上面的o.say = function(){},因为this就等同于o嘛),从这里可以看出,这些相同的方法还是没有得到共享。
4. 对象的创建与销毁
熟悉C#或者java的同学肯定对这个很熟悉,创建对象使用new操作符,对于拥有自动垃圾回收的语言来说,把对象设置为null就会在回收器工作的时候被销毁。
先看new操作,在JavaScript中,new操作完成下面的工作:
首先,创建一个空对象,然后使用apply方式去调用构造函数,将这个空对象传入作为apply的第一个参数,及上下文参数。这样函数内部的 this 将会被这个空的对象所替代:
var o = new Person('Frank', 20);
//上一句相当于下面的代码
var o = {};
Person.apply(o, ['Frank', 20]);
说白了也很简单,创建一个空对象,然后在这个对象上把指定函数的内容全部执行一遍,这样该有的成员就都有了,是不是实现了别的语言中new干的相同的事?!
销毁一个对象很简单,把它设为null就可以了。
o = null;
这样垃圾回收器会知道如何处理。
在JavaScript中还有一个delete操作,这个操作很神奇,有时候也会带来一些隐藏很深的bug,所以大部分情况下,确实没有必要使用这个操作。
delete操作用于删除对象的成员,操作以后访问这个成员就会返回undefined。看个例子:
function Person(name, age) {
var m_name = name; this.say = function(msg) {
alert(m_name + ':' + msg);
};
} var o = new Person('Frank', 20);
o.say('Hi');
delete o.say;
o.say('Hi');
运行一下就可以发现第二次的say调用没有输出。
delete的正确的用法就是这样,不过需要注意,使用var,function关键字定义的变量,内置对象的内置成员,原型(这个概念下面会讲到)上定义的成员都是无法delete掉的。更详细的细节可以参看下面两位同学的文章,这里不详述了:
http://m.oschina.net/blog/28926和http://blog.charlee.li/javascript-variables-and-delete-operator/。
5. 实例成员与静态成员
使用函数来传参数,使用var去限定作用域,使用this暴露公开成员,这就是使用函数来模拟对象的私有成员与公开成员的方式,这些定义的成员都是通常意义上的实例成员。由于JavaScript是纯粹的面向对象,所以理论上是没有静态成员的概念的,但是我们仍然可以使用函数模拟出静态成员的效果:
function Person(name, age) {
// 实例成员
} // 静态成员
Person.showType = function() {
alert("Animal");
}; Person.showType();
好了,至此封装性的问题以及常见的对象成员访问方式都解决了,下面看一下重用的问题。
解决重用性问题:使用原型来分享数据
既然JavaScript是面向对象的语言,那么继承的问题就无法避免。在JavaScript中,继承是通过原型来实现的,继承的方式下面会总结。这里只是说一下原型的概念,以及它如何解决成员的重用问题。
原型是一个对象,任何对象都有一个属性prototype(在Chrome中,通过__proto__访问,不详述了)指向它的原型对象,由于原型也是对象,所以原型也有这样的属性指向它的原型,这样这些原型对象就串起来了,形成了原型链,原型链的最末端就是Object的原型对象{},其实就是空对象。
到了这里我们再回头来看看new时,JavaScript做了哪些工作;其实除了上面所说的创建对象,调用构造函数,还有另外一件事就是把新对象的prototype设置为构造函数的prototype,而且把新对象的prototype属性的constructor设置为构造函数。这样所有new出来的对象的prototype都是指向同一个对象,就是构造函数的prototype对象,这样只要我们把需要共享的成员放到构造函数的prototype对象中,我们就能重用代码了,如下所示:
function Person(name, age) {
this.m_name = name;
var m_age = age;
// 实例成员放到构造函数中
this.showAge = function() {
alert(m_age);
};
}
// 实例成员放到原型对象中
Person.prototype.showName = function() {
alert(this.m_name);
}; var p1 = new Person("Frank", 10);
var p2 = new Person("Dong", 20); // 原型方法的调用与别的方法是一样的
p1.showName();
p2.showName(); // 比较方法的来源
alert(p1.showName === p2.showName); // 返回true
alert(p1.showAge === p2.showAge); // 返回false
这里需要注意一个问题,那就是原型对象不能访问构造函数里面的私有成员,这是肯定的,因为原型对象是新的对象,有自己的作用域了,它只能访问原来对象公开的成员,就是使用this暴露的成员。从这一点来说,定义一个实例方法的时候我们可以选择把它放到原型中,也可以把它放到构造函数中,这两种做法是各有优点,前者节省内存,后者能访问私有成员,是更完美的封装。
好了,到这里我们终于不太完美的解决了上面讨论的封装性的两个问题,在结束JavaScript对象的封装性之前还需要回顾两点:
1. JavaScript对象的动态性
上面的代码体现了动态语言最主要的一个特性:自由添加和删除成员。这个是JavaScript很重要的特性,有意识的使用的话可以灵活的解决很多问题。
2. 对象的成员访问问题
访问对象的成员时,JavaScript会首先在对象自身上查找,查找不到的话,会查找原型对象。后面再加上继承的知识,我们可以知道,在JavaScript中,继承是通过构造原型链实现的,于是访问对象成员时,JavaScript也会一直遍历原型链查找成员,当到达原型链末尾还找不到的话,就返回undefined。
这是一个相当费时的操作,尤其在遍历对象所有成员的时候,这个问题在后面会专门说明。
JavaScript大杂烩3 - 理解JavaScript对象的封装性的更多相关文章
- JavaScript大杂烩1 - 理解JavaScript的类型系统
随着硬件水平的逐渐提高,浏览器的处理能力越来越强大,本人坚信,客户端会越来越瘦,瘦到只用浏览器就够了,服务端会越来越丰满:虽然很多大型的程序,比如3D软件,客户端仍然会存在,但是未来的主流必将是浏览器 ...
- JavaScript大杂烩4 - 理解JavaScript对象的继承机制
JavaScript是单根的完全面向对象的语言 JavaScript是单根的面向对象语言,它只有单一的根Object,所有的其他对象都是直接或者间接的从Object对象继承.而在JavaScript的 ...
- JavaScript大杂烩6 - 理解JavaScript中的this
在JavaScript开发中,this是很常用的一个关键字,但同时也是一个很容易引入bug的一个关键字,在这里我们就专门总结一下页面中可能出现的this关键字(包括几种在其他页面文件中出现的this) ...
- JavaScript大杂烩2 - 理解JavaScript的函数
JavaScript中的字面量 书接上回,我们已经知道在JavaScript中存在轻量级的string,number,boolean与重量级的String,Number,Boolean,而且也知道了之 ...
- JavaScript大杂烩12 - 理解Ajax
AJAX缘由 再次谈起这个话题,我深深的记得就在前几年,AJAX被炒的如火如荼,就好像不懂AJAX,就不会Web开发一样.要理解AJAX为什么会出现,就要先了解Web开发面临的问题. 我们先来回忆一下 ...
- JavaScript大杂烩10 - 理解DOM
操作DOM 终于到了JavaScript最为核心的部分了,通常来说,操作DOM,为页面提供更为友好的行为是JavaScript根本目标. DOM树 - HTML结构的抽象 既然DOM是操纵HTML ...
- JavaScript大杂烩9 - 理解BOM
毫无疑问,我们学习JavaScript是为了完成特定的功能.在最初的JavaScript类型系统中,我们已经分析过JavaScript在页面开发中充当着添加逻辑的角色,而且我们知道JavaScript ...
- JavaScript大杂烩7 - 理解内置集合
JavaScript内置了很多对象,简单的类型如String,Number,Boolean (相应的"值类型"拥有相同的方法),复杂一点的如Function,Object,Arra ...
- 深入理解JavaScript系列+ 深入理解javascript之执行上下文
http://www.cnblogs.com/TomXu/archive/2011/12/15/2288411.html http://blog.csdn.net/hi_kevin/article/d ...
随机推荐
- axios的Get和Post方法封装及Node后端接收数据
最近有做一个Vue的小项目,其中用到了尤大大推荐使用的axios,但是使用的过程中遇到了各种各样的问题,所以这次也是将一些心得分享出来. 安装的流程我就简单说一下下吧,在一个自己新建的文件夹中命令行中 ...
- CUDA driver version is insufficient for CUDA runtime version 解决
配置ubuntu17.1+CUDA9.2的caffe环境,CUDA sample编译完成,执行到./deviceQuery时报错:CUDA driver version is insufficient ...
- python(leetcode)-136只出现一次的数字
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次.找出那个只出现了一次的元素. 说明: 你的算法应该具有线性时间复杂度. 你可以不使用额外空间来实现吗? 示例 1: 输入: [ ...
- [视频]K8飞刀 shellcode loader演示教程
[视频]K8飞刀 shellcode loader演示教程 https://pan.baidu.com/s/1eQ77lPw
- Xamarin.Android 本地数据库 SQLiteDatabase 操作
目的:使用 SQLiteDatabase 创建本地数据库.表,并对数据进行增删改查操作. 引用命名空间: using Android.App; using Android.Widget; using ...
- linux系统中的文件权限详解(转)
一.在<Linux系统中如何查看文件属性>中介绍了通过ls指令来查看文件的属性,具体如下: [命令] letuknowit@ubuntu:/$ cd /tmp letuknowit@ubu ...
- 使用切片拦截Rest服务
使用切片拦截Rest服务 1.使用过滤器Filter: 我们可以在建立的springboot的项目中建立新的类来是先Filter的接口,doFilter是过滤器中的主要方法,用来做处理逻辑,最后我们只 ...
- .NET Core跨平台微服务学习资源
一.Asp.net Core基础 微软英文官网:https://docs.microsoft.com/en-us/aspnet/core/?view=aspnetcore-2.1 .NET Core: ...
- thinkphp的自动生成
第一,在index.php里定义 define('BIND_MODULE','Admin'); //自动生成Admin目录下的结构(默认生成IndexController) define('BUILD ...
- vue 周期函数
先简单说说vue中周期函数 beforeCreate(创建前) created(创建后) beforeMount(载入前) mounted(载入后) beforeUpdate(更新前) updated ...