面向对象JavaScript开发实战
一. JavaScript高级部分
1.1 函数
1.1.1 函数的内存分布
JavaScript中定义的每个函数在被解析的时候都将分配一个prototype属性,该prototype引用一个对象,而这个对象的constructor属性又引用到原函数,如下代码所示:
function Person(_name) { this.name = _name; this.sayHi = function() { console.log('hi, I am '+this.name); }; } Person.prototype.age = 25; Person.prototype.get = function(properName) { return this[properName]; };
函数的prototype对象可以对函数进行方法的扩展,其用法非常灵活。由于JavaScript语言缺乏很多静态语言所拥有的特性,如对象类、继承和多态等,要想在JavaScript中模拟实现这些特性,prototype对象起着非常关键的作用,在后面章节中将会详细提及。
1.1.2 函数的声明方式
函数的声明有如下几种方式。
方式一:new Function(arg1,arg2,arg3,body)
注意:函数也是对象,由Function实例化的,通常,可以通过在Function.prototype对象上定义通用方法,使得所有函数均持有该方法。var func = new Function("name","age", "console.log('name:'+name+', age:'+age);"); func('Peter', 23);
方式二:var func = function() {}
var func = function(name, age) { console.log('name:'+name+', age:'+age); }; func('Peter', 23);
方式三:function func() {}
function func(name, age) { console.log('name:'+name+', age:'+age); } func('Peter', 23);
1.1.3 函数的调用方式
函数被调用的方式有多种,不同的调用方式适合不同的场景。JavaScript最常见的函数调用方式有如下几种:
方式一,作为函数直接调用
function hello() { console.log('hello, world'); } hello();
方式二,作为对象的构造函数调用
function Hello() { console.log('create a Hello object.'); } var hello = new Hello();
方式三,作为对象方法调用
var peter = { name: 'peter', age: 25, showProfile: function() { console.log('name:'+this.name+', age:'+this.age); } }; peter.showProfile();
方式四,采用call或者apply进行调用,用法如下:
method.call(obj, arg1, arg2…);method.apply(obj, [arg1, arg2, arg3]);
var showProfile = function(other) { console.log('name:'+this.name+', age:'+this.age+', other:'+other); }; var peter = { name: 'peter', age: 25 }; showProfile.call(peter, 'call'); showProfile.apply(peter, ['apply']);
运行结果:
name:peter, age:25, other:call name:peter, age:25, other:apply
由此可见,JavaScript函数能够以非常灵活的方式使用,而正因为这种灵活性导致开发人员不易于掌握他们,这要求开发人员要经常实践才能深刻理解他们的精妙。
1.1.4 this的用法
JavaScript中this的用法非常灵活,也是非常难以熟练掌握的。由于其运行期绑定的特性,this含义非常丰富,它可以是全局对象、当前对象或者任意对象,这完全取决于函数的调用方式。JavaScript 中函数的调用有以下几种方式:作为对象方法调用,作为函数调用,作为构造函数调用和使用 apply 或 call 调用。下面我们将按照调用方式的不同,分别讨论 this 的含义。
当作为对象方法被调用的时候,this指的是该对象本身,见下面例子:
当作为函数直接被调用时,this指的是当前上下文:var point = { x: 0, y: 0, move: function(witdh, heigh) { this.x = this.x + width; this.y = this.y + height; } }; point.move(5, 10);//this绑定到当前对象,即point对象
function makeNoSense(x) { this.x = x; } makeNoSense(5);//相当于window.makeNoSense(5); console.log(x);//输出为:5
当作为构造方法调用的时候,this绑定到新创建的对象上:
function Point(x, y) { this.x = x; this.y = y; } var point = new Point(10, 20); console.log(point.x);//输出:10 console.log(point.y);//输出:20
当用call或apply方式调用时,this绑定到第一个参数对象:
function Point(x, y) { this.x = x; this.y = y; this.moveTo = function(x, y) { this.x = x; this.y = y; }; } var p1 = new Point(0, 0); var p2 = {x: 0, y: 0}; p1.moveTo(1, 1); p1.moveTo.apply(p2, [10, 10]);//this绑定到p2对象 console.log(p1.x+', '+p1.y);//输出:1, 1 console.log(p2.x+', '+p2.y);//输出:10, 10
最后来看一个jQuery绑定事件的例子
HTML代码
JavaScript代码<div class="ui-btn ui-active" data-action="showPageFromMenu" data-rel="pageHome"> <img class="ui-btn-tit-pic" src="img/home.png"/>首页 </div> <div class="ui-btn" data-action="showPageFromMenu" data-rel="pageNotice"> <img class="ui-btn-tit-pic" src="img/message.png"/>业务通知 </div> <div class="ui-btn" data-action="showPageFromMenu" data-rel="pageArticle"> <img class="ui-btn-tit-pic" src="img/book.png"/>最新发文 </div>
$('body').on('tap click', '.ui-btn', function() { //this指的是触发事件的dom元素 var action = $(this).attr('data-action'); app[action](this); });
这块代码的目的是要在具有相同class样式的div上面注册click事件,其传入的回调函数最终将会这样被调用:div.onclick = callback;,因此,回调函数中的this便是指被触发事件的div元素,总而言之一句话: this指的是函数的调用者。
1.1.5 callback回调函数
JavaScript回调函数是指将一个函数作为参数传入到另一个函数(主函数),该函数将在主函数内有机会被执行,这样函数称之为回调函数(callback)。callback可以用来模拟实现静态语言中的多态特性,也可以用来实现异步功能。常见的使用到回调函数的情形有setTimeout、Ajax和Event等。
$.ajax({ url: 'test.html', context: document.body }).done(function() { $(this).addClass('done'); }).fail(function() { alert('error'); }).always(function() { alert('complete'); });
1.2 对象
1.2.1 对象的内存分布
对象定义:属性的无序集合,每个属性存放一个原始值、对象或函数。每个对象都是由类定义的,通过类实例化对象,在JavaScript中并没有正式的类,开发人员通过函数作为类来定义对象的属性和方法,与其他静态语言中的类的概念相比,两者是等价的。
1.2.2 对象的创建
我们可以通过以下两种方式创建一个对象:字面量和构造函数。这两种方式分别用于不同的场景。
字面量创建对象
这种方式适合于封装复杂数据结构,创建普通对象,并且将该对象作为全局变量使用,由于这种方式创建对象非常简单明了,是比较常见的创建对象的方式。var obj = { attr1: 'attr1', attr2: 20, print: function() { console.log('attr1:'+this.attr1+', attr2:'+this.attr2); } };
构造函数创建对象
function Person() { this.name = 'Peter'; this.sayHello = function() { console.log('hello, '+this.name); }; } var person = new Person(); person.sayHello();//输出:hello, Peter
通过构造函数创建对象,这种用关键字new的方式跟其他静态语言几乎一样,只不过JavaScript没有原生的对类继承的支持,因此需要我们模拟实现类继承的特性,这将在后面介绍。
构造函数创建对象适合于需要按照类创建多个实例的场景,在这种场景下,每个对象实例都拥有自己独立的属性和方法,为了节省更多的资源,我们往往把对象方法提取到类的prototype中去,这样多个对象实例便可以重用同一个方法,这也是我们常常所推荐的用来定义对象的方式。
function Person(name) { this.name = name; } Person.prototype.sayHello = function() { console.log('hello, '+this.name); }; var p1 = new Person('Peter'); p1.sayHello();//输出:hello, Peter var p2 = new Person('Hans'); p2.sayHello();//输出:hello, Hans
1.3 事件
1.3.1 JavaScript单线程
JavaScript是单线程的,因此当一个程序需要处理多个任务的时候,程序只能在前一个任务执行完毕后才开始执行后一个任务,如果前面的任务执行需要花费很长时间,这将导致后面的任务阻塞和浏览器假死,为了避免这种情况,我们需要应用异步模式对程序进行优化,将耗时比较多的任务放到事件队列,等到主体函数执行完后再执行事件队列里面的任务,常见的异步模式有:回调函数、事件监听、发布订阅和Promises对象等(相关知识点请上网查询)。1.3.2 JavaScript事件机制
以上代码执行的结果并不是预期的500毫秒,而是1000毫秒(可能存在一点误差),这是因为setTimeout参数中的回调函数并未在500毫秒后立即执行,而是被放到一个事件队列中去了,等到主体函数执行完毕之后才开始执行该回调函数。同样的,在DOM元素上触发事件也不是立即执行事件绑定的函数,而是等待DOM所有事件触发后才开始执行绑定到事件的函数。var startTime = new Date(); setTimeout(function() { var endTime = new Date(); console.log(endTime - startTime); }, 500); while(new Date() - startTime < 1000) { }
下面的例子将使用setTimeout和callback来实现异步模式的处理方式。输出结果:function asyn(callback) { setTimeout(callback, 0); } function readFile() { var startTime = new Date(); while(new Date() - startTime < 1000) { //read file will cost 1 second } console.log('finished reading file.'); } console.log('start...'); asyn(readFile); console.log('finished...');
上面的代码中假设读取文件将花费1秒钟时间,那么我们将读取文件的操作放到事件队列中,等到主体函数执行完毕之后再执行文件读取操作,这样便可以防止程序阻塞的问题。读者有兴趣可以研究一下为什么NodeJS并发处理能力要优于其他静态语言程序(如Java),以便更深入了解JavaScript单线程和事件队列机制。start... finished... finished reading file.
1.4 继承
1.4.1 创建类
在JavaScript语言中,函数可以作为类进行对象实例化,但是JavaScript中没有类继承的概念,类继承可以大大降低重复代码,子类继承父类后便拥有父类的方法和属性,我们只能通过编程的方法去模拟实现类继承,这样一来类的创建就不仅仅是定义一个函数那么简单了,创建类可以这样写:
var Parent = new Class(); var Child = Parent.extend();//Child类继承Parent类 var parent = new Parent(); var child = new Child();
1.4.2 类继承
继承即子类继承父类的属性和方法,通过前面的介绍可知,在定义类(函数)的时候,一个类方法既可以声明在类内部,也可以声明在prototype对象里面,那么要实现一个子类,就需要把父类和prototype的属性和方法复制到子类,这才是关键所在。前面已经提到可以通过编程的方式模拟实现类继承的特性,下面介绍几种具体方法。
方式一:prototype链接,即子类的prototype对象链接到父类的prototype对象,这样子类便拥有了父类的prototype所定义的方法:
方式二:类抄写,即子类抄写父类的属性和方法,其实就是在子类构造方法内调用父类的构造方法来实现的,如下示例:
输出结果://declare parent class function Parent(name) { this.name = name; this.showName = function() { console.log('name:'+this.name); }; } Parent.extend = function() { var Child = function() { Parent.apply(this, arguments);//继承父类构造方法 }; var F = function() { this.constructor = Child; }; F.prototype = Parent.prototype; Child.prototype = new F();//间接方法,防止污染父类prototype对象 return Child; }; Parent.prototype.sayHello = function() { console.log('hello, '+this.name); }; var parent = new Parent('Peter'); parent.showName(); parent.sayHello(); var Child = Parent.extend(); var child = new Child('Hans'); child.showName();//该方法继承Person函数内定义的方法 child.sayHello();//该方法继承Person原型对象内定义的方法
读者可以思考一下为什么上面要引用一个中间对象F,其实理由很简单,如果不建立一个中间对象F,让子类prototype直接引用父类的prototype,这样便可以通过修改子类prototype来修改父类prototype,显然是不符合面向对象程序设计原则,建立中间对象是为了防止子类污染父类。这里请读自行描画出上面Parent和Child的内存分布图,以加深对其的理解。name:Peter hello, Peter name:Hans hello, Hans
方式三:prototype属性复制,某些时候你需要扩展子类的属性和方法,你可以在创建子类的时候定义新的属性和方法,写法如下:extend方法传入的参数即为子类扩展的属性和方法,具体实现如下代码:var Child = Parent.extend({ age: 27, showAge: function() { console.log('age:'+this.age); } });
输出结果:function Parent(name){ this.name = name; this.sayHi = function() { console.log('hello, '+this.name); }; } Parent.extend = function(options) { var Child = function() { Parent.apply(this, arguments); }; for(var prop in options) { Child.prototype[prop] = options[prop]; } return Child; }; var Child = Parent.extend({ age: 27, showAge: function() { console.log('age:'+this.age); } }); var child = new Child('bxiao'); child.sayHi(); child.showAge();
hello, bxiao age:27
方式四:圣杯模式,以上的几种继承手段各有其用处,但是都不能单独使用,如果将上面介绍的几种方式合并起来使用,相互弥补各自的缺陷,则可以达到真正的继承效果,具体实现请参考下面代码:
该继承模式综合了上述几种模式,已经较为健壮了,读者可以直接仿照该继承模式来编写自己项目中的框架代码,相信能够给项目带来许多改善。var Class = function(){}; Class.new = function() { var klass = function() { //约定所有函数均申明了init方法,用于初始化 if(typeof this.init === 'function') { this.init.apply(this, arguments); } }; klass.extend = function(options) { var Parent = this; var Child = function(){ Parent.apply(this, arguments);//继承父类的属性和方法 }; var F = function(){ this.constructor = Child; }; F.prototype = Parent.prototype; Child.prototype = new F();//间接方法,防止父类被污染 for(var prop in options){//扩展属性和方法 Child.prototype[prop] = options[prop]; } Child.extend = klass.extend;//让子类可以继续扩展子类 return Child; }; return klass; } var Parent = Class.new();//创建Parent类 Parent.prototype.init = function(name) {//定义初始化方法 this.name = name; this.showName = function() { console.log('name:'+this.name); }; }; Parent.prototype.sayHello = function() {//定义方法 console.log('hello, '+this.name); }; var parent = new Parent('Peter');//实例化Parent类 parent.sayHello();//输出:hello, Peter parent.showName();//输出:name:Peter var Child = Parent.extend({//创建Child类,继承Parent并扩展属性和方法 age : 35, showAge : function() { console.log('age:'+this.age); } }); var child = new Child('Hans');//实例化Child类 child.sayHello();//输出:hello, Hans child.showName();//输出:name:Hans child.showAge();//输出:age:35
1.4.3 对象继承
由于JavaScript动态扩展的特性,可以直接给对象添加和删除属性,在很多情况下需要让一个对象拥有另一个对象的方法和属性,这种做法我们称之为对象继承,其实叫对象扩展更加贴切。ECMAScript5制定了一系列新API,其中一个创建扩展对象的API是Object.create(prototype, descriptors),可以用来扩展对象,这种方式原型链干净,但是浏览器支持有限制。在JQuery中扩展一个对象可以调用如下方法:
extend(dest,src1,src2,src3...srcN)
其中dest为目标对象,该方法将 src1、src2...对象的属性和方法逐一复制给dest对象。对象扩展已经被很多JavaScript库做得很好了,所以我们无须重复发明轮子,放心地使用这些工具库,将大大提高前端开发效率。
1.5 模块
1.5.1 命名空间
命名空间可以用来防止函数命名冲突,大多数静态语言都支持命名空间,虽然JavaScript不支持,但我们可以通过编程的手段来模拟实现命名空间,见如下代码示例:
var cn = cn || {}; cn.hunnu = cn.hunnu || {}; cn.hunnu.edu = cn.hunnu.edu || {}; var Class = cn.hunnu.edu.Class = function(){};
前面讲了命名空间可以防止函数命名冲突,那么模块化则可以防止全局变量冲突,模块化编程要求一个js文件为一个模块,程序模块化可以使得系统设计变得优良,使得代码管理和维护变得轻松。下面通过代码简单地演示如何创建和使用模块:1.5.2 模块创建
编写cn.hunnu.edu.util.js文件(function() { var name = 'Peter'; var sayHello = function() { console.log('hello, '+this.name); }; this.cn = this.cn || {}; this.cn.hunnu = this.cn.hunnu || {}; this.cn.hunnu.edu = this.cn.hunnu.edu || {}; this.cn.hunnu.edu.util = { name: name, sayHello: sayHello }; }).call(global);//在NodeJS环境中传入global全局对象,如果在浏览器中运行则传入window对象
编写app.js文件
console.log(cn.hunnu.edu.util.name);//输出:Peter cn.hunnu.edu.util.sayHello();//输出:hello, Peter
上述代码为什么这么写,这样写有什么优点?由于这些涉及到JavaScript作用域和闭包相关问题,请读者自行研究相关知识点来加深理解。读者不妨参考一些主流的JavaScript框架,几乎都会采用上述的方式进行设计。
1.5.3 AMD和CMD
前面简单介绍了模块创建的基本原理,在实际应用中要使用模块化编程还需要考虑许多问题,如模块书写规范、模块依赖、异步加载等等,要解决这一系列问题就需要有一个统一的规范,目前已形成的两大规范分别是CMD和AMD。
CMD是"Common Module Definition"的缩写,该规范明确了模块的基本书写格式和基本交互规则,该规范是Sea.js推广过程中形成的。
AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行,该规范是在推广RequireJS库形成的。下面将简单介绍一下RequireJS的用法。
首先到相关网站下载最新的require.js和其他js库,然后搭建一个基本工程目录,如下图所示,js/app目录放置应用程序模块,js/lib目录放置第三方js库,js/main.js为程序初始化入口:
在index.html文件中添加:
- www/
- index.html
- js/
- app/
- util.js
- a.js
- lib/
- jquery.js
- backbone.js
- main.js
data-main属性告诉require.js在加载完require.js之后再加载js/main.js,因此一般把main.js作为程序入口,对整个程序进行初始化操作。<script data-main="js/main" src="js/require.js"></script>
编辑main.js,添加如下内容:
requirejs.config({ baseUrl: 'js/lib',//定义模块根路径 paths: { app: '../app'//定义模块子路径,相对于根路径 } }); //开始主函数逻辑 requirejs(['jquery', 'backbone', 'app/util', 'js/app/a.js'], function ($, backbone, util) { //jQuery, backbone 和app/util模块都已全部加载 //并且可以直接在这里使用 });
在util.js中定义模块:
上面介绍了如何在html页面引入require.js,如何定义一个module和如何使用module,这些都非常容易上手,当然,要想了解require.js的更多用法,还请参考官方网站:http://requirejs.org/** * 定义模块 * define函数第一个参数为依赖模块列表,依赖模块加载后作为参数分别传入到回调函数 * define函数第二个参数为模块定义函数,将返回模块对象 */ define(["jquery", "backbone"], function($, Backbone) { //返回一个util模块对象. return { extend: function(dest, src) { return $.extend(dest, src); }, createModel : function() { return Backbone.Model.extend({}); } }; } );
至此你已经可以构建一个结构良好的前端应用程序了。前面已经对类继承、对象扩展和模块化编程的具体实现原理做了简单分析,由于目前已有大量的js库已经实现了这些功能,所以我们只需直接拿来用即可。对于JavaScript编程热爱者,不仅要学会如何使用主流的JavaScript框架,还要理解其中设计原理,方能知其然知其所以然,在具体项目中遇到问题时,才不至于手足无措。
二. NodeJS入门
2.1 开发环境搭建
Node.js是一个基于Chrome JavaScript引擎的运行平台,该平台用来构建高性能、伸缩性强的服务器端应用程序,Node.js是跨平台的轻量级引擎,它采用事件驱动和非阻塞I/O模型使得其适合编写高性能的、数据密集型的和实时性强的应用程序。安装node.js:windows平台直接访问官网http://nodejs.org/直接下载安装,linux和mac平台下可以通过git下载源码编译安装,见下面安装步骤:
git clone git://github.com/joyent//node.git
cd node
./configure
make
sudo make install
安装通用插件。Node.js安装后便可以通过包管理器npm安装其他插件,其官网上https://npmjs.org/有大量的插件供开发者选择,下面将介绍几款比较常用的插件的安装。
- mocha 单元测试框架
> npm install -g mocha
- node-inspector 代码调试器
> npm install -g node-inspector
- supervisor 热部署插件
> npm install -g supervisor
- express 网站构建插件,详细请访问:http://expressjs.jser.us/
> npm install -g express
- 前端应用构建工具Yeoman,详细请访问:http://yeoman.io/
> npm install -g yo
- Karma测试驱动器(Test Runner),详情请访问:https://github.com/vojtajina/karma/
> npm install -g karma
Nodejs的插件非常丰富,如数据库支持、集群、MVC框架、模板工具、图片处理、邮件服务等等,这些插件都可以在互联网获取。
2.2 hello world开始
打开命令行窗口,输入node命令,回车进入运行窗口,输入JavaScript代码回车执行,返回结果,如下所示:>node
>console.log(‘hello, world’);
hello, world>
两次按下ctrl+c即可退出node窗口运行模式。当然,node不限于此,我们可以把JavaScript逻辑写在js文件中,然后通过node命令来运行。
编写hello.js:
function Hello() {
this.sayHello = function() {
console.log('hello, world');
};
}
var hello = new Hello();
hello.sayHello();
运行hello.js
>node hello.js
运行结果:
hello, world
2.3 NodeJS应用模块化
2.4.1 引用外部模块
node既然作为一种服务脚本语言,跟其他服务器静态语言一样,需要一套完善函数库(类库),而组织类库或者编写第三方api均需要一个标准规范,node使用CommonJS规范来具体实施类库编写。CommonJS规范包括模块(module)、包(package)、单元测试等等内容。
下面演示如何引用一个模块:
编写foo.js
var circle = require('./circle.js');
console.log( 'The area of a circle of radius 4 is '+ circle.area(4));
上面代码通过require引入circle模块,并在后面直接调用circle的area方法计算半径为4的圆形面积。
2.4.2 构建自定义模块
接着上面的例子,我们来看看circle模块如何定义,编写circle.js文件:
var PI = Math.PI;
exports.area = function (r) {
return PI * r * r;
};
exports.circumference = function (r) {
return 2 * PI * r;
};
代码中exports向外面公开一个模块,即为circle模块,该模块申明了两个方法分别是area和circumference,一旦模块定义好,便可以在其他模块中通过require方法引用它。模块名称即为js文件名。
2.4 NodeJS常用API
2.5.1 File System
文件系统,即针对系统文件操作所提供的一套函数库,下面简单演示一下文件读取api:
以上程序将读取文件D:/git/nodejs_workspace/oojs/test.js的内容,并将其打印到控制台,注意,readFile方法是异步方法,由于文件读取是需要消耗磁盘I/O时间的,所以上面打印操作放在回调函数中执行,不会造成主体函数的阻塞等待问题,这个特性是其他静态语言无法相比的。var fs = require('fs'); var fileName = 'D:/git/nodejs_workspace/oojs/test.js'; fs.readFile(fileName, 'utf-8', function(err, fc) { console.log(fc); });
2.5.2 Http
一个简单的服务器应用,对所有HTTP请求均返回“hello, world”,编写server.js:
运行该应用:>node server.jsvar http = require('http'); http.createServer(function (req, res) { res.writeHead(200, {'Content-Type': 'text/plain'}); res.end('Hello World\n'); }).listen(1337, '127.0.0.1'); console.log('Server running at http://127.0.0.1:1337/');
打开浏览器并输入http://127.0.0.1:1337,可以看到返回hello, world字符串。更多关于node服务器端编程接口,请访问:http://nodejs.org/api/http.html
三. JavaScript书籍推荐
《JavaScript权威指南》弗兰纳根(David Flanagan)
《JavaScript语言精粹(修订版)》道格拉斯•克罗克福德
《JavaScript高级程序设计(第3版)》尼古拉斯•泽卡斯
作者:肖波日期:2014-06-13QQ:691546285 欢迎交流~~